HarmonyOS 鸿蒙Next中图片预览效果

HarmonyOS 鸿蒙Next中图片预览效果 怎么实现类似聊天消息图片视频预览功能,图片可以缩放,拖动等,长按图片或者视频可以保存本地相册功能,视频禁止缩放。

9 回复

【解决方案】

  • 监听到双击手势后,若当前图片处于放大状态,则将图片恢复至原大小;若当前图片未缩放,则将其放大至设定的两倍。
TapGesture({ count: StyleConstants.TAPGESTURE_FINGERS_COUNT })
  .onAction(() => {
    if (this.activeImage.scale > 1) {
      this.activeImage.scale = 1
      this.activeImage.offsetX = 0
      this.activeImage.offsetY = 0
      this.activeImage.offsetStartX = 0
      this.activeImage.offsetStartY = 0
      this.disabledSwipe = false
    } else {
      this.activeImage.scale = 2
      this.disabledSwipe = true
    }
  })
  • 在onActionStart中获取初始缩放比例值,在onActionUpdate中通过scale获取相对比例变化量并计算目标缩放比。在双指捏合缩放的过程中,通过调整图片的offsetX和offsetY保障视觉中心稳定。
PinchGesture({ fingers: StyleConstants.PINCHGESTURE_FINGERS_COUNT })
  .onActionStart((event) => {
    this.defaultScale = this.activeImage.scale
  })
  .onActionUpdate((event) => {
    let scale = event.scale * this.defaultScale
    if (scale <= 4 && scale >= 1) {
      this.activeImage.offsetX = this.activeImage.offsetX / (this.activeImage.scale - 1) * (scale - 1) || 0
      this.activeImage.offsetY = this.activeImage.offsetY / (this.activeImage.scale - 1) * (scale - 1) || 0
      this.activeImage.scale = scale
    }
    this.disabledSwipe = this.activeImage.scale > 1
  })
  • 在onActionStart中获取初始触点的坐标信息,在onActionUpdate中根据当前触点坐标、初始触点坐标以及初始坐标偏移计算出当前的坐标偏移。在滑动过程中计算图片的最大可移动范围作为边界约束,当图片的坐标偏移在最大可移动范围内时,可实现图片的滑动。
PanGesture()
  .onActionStart(event => {
    this.activeImage.dragOffsetX = event.fingerList[0].globalX
    this.activeImage.dragOffsetY = event.fingerList[0].globalY
  })
  .onActionUpdate((event) => {
    if (this.activeImage.scale === 1) {
      return
    }
    let offsetX = event.fingerList[0].globalX - this.activeImage.dragOffsetX +this.activeImage.offsetStartX
    let offsetY = event.fingerList[0].globalY - this.activeImage.dragOffsetY +this.activeImage.offsetStartY
    if (this.activeImage.width * this.activeImage.scale > this.containerWidth &&
      (this.activeImage.width * this.activeImage.scale - this.containerWidth) / 2 >=
      Math.abs(offsetX)) {
      this.activeImage.offsetX = offsetX
    }
    if (this.activeImage.height * this.activeImage.scale >
    this.containerHeight &&
      (this.activeImage.height * this.activeImage.scale - this.containerHeight) / 2 >=
      Math.abs(offsetY)) {
      this.activeImage.offsetY = offsetY
    }
  })

更多关于HarmonyOS 鸿蒙Next中图片预览效果的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


图片/视频容器搭建:

使用 Swiper + Grid 组合实现多图切换与布局:

Swiper() {

  ForEach(this.mediaList, (item: MediaItem) => {

    // 图片类型处理

    if (item.type === 'image') {

      Image(item.uri)

        .gesture(this.buildImageGesture()) // 绑定手势

    } else { 

      Video({ src: item.uri })

        .controls(false) // 视频禁止缩放

    }

  })

}

通过组合手势实现缩放与拖动:

private buildImageGesture(): GestureGroup {

  const panGesture = new PanGesture({ fingers: 1 })

    .onActionUpdate((event: GestureEvent) => {

      // 拖动偏移量处理

      this.offsetX = event.offsetX

      this.offsetY = event.offsetY

    })

  const pinchGesture = new PinchGesture()

    .onActionUpdate((event: PinchGestureEvent) => {

      // 双指缩放计算

      this.scale = event.scale * this.lastScale

      this.scale = Math.min(Math.max(this.scale, 0.5), 3) // 限制缩放范围

    })

  return GestureGroup.parallel(panGesture, pinchGesture)

}

长按保存功能

长按事件监听

Image(item.uri)

  .onLongPress(() => {

    this.showSaveDialog(item.uri) // 弹出保存选项

  })

保存到相册实现

private async saveToAlbum(uri: string) {

  const phAccessHelper = photoAccessHelper.getPhotoAccessHelper(this.context)

  const photoAsset = await phAccessHelper.createAsset(uri)

  phAccessHelper.addAssetsToAlbum([photoAsset], albumId) // 需获取相册ID

}

视频处理

禁用缩放逻辑:通过条件判断区分媒体类型

Video({ src: item.uri })

  .gesture(

    item.type === 'video' ? 

    GestureGroup.parallel() : // 空手势组

    this.buildImageGesture()

  )

视频播放控制:添加播放/暂停按钮覆盖层

Stack() {

  Video({ src: item.uri })

  Button()

    .onClick(() => this.videoController.start()) 

}

楼主的需求和这个示例很像 可以参考一下:图片预览器-布局与弹窗 - 华为HarmonyOS开发者
图片预览器是常见的开发应用场景

图片拖拽放大缩小

@Entry
@Component
struct Page2 {
  @State scaleValue: number = 1;
  @State pinchValue: number = 1;
  @State count: number = 0;
  @State offsetX: number = 0;
  @State offsetY: number = 0;
  @State positionX: number = 0;
  @State positionY: number = 0;
  @State boxWidth: number = 200;
  @State boxHeight: number = 200;

  maxW:number = 0
  minW:number = 0
  maxH:number = 0
  minH:number = 0
  dwidth:number= 0
  dheight:number= 0
  fWidth:number = 0
  fHeight:number = 0
  x:number = 0
  y:number = 0

  build() {
    Column() {
      Column() {
        Text("1231231231223132112321")
        Text("1231231231223132112321")
        Text("1231231231223132112321")
        Text("1231231231223132112321")
        Text("1231231231223132112321")

        Column()
          .width(20)
          .aspectRatio(1)
          .backgroundColor(Color.Red)
          .scale({ x: 1/ this.scaleValue, y: 1/this.scaleValue, z: 1 })
          .position({
            right: -10,
            bottom: -10
          })
          .gesture(
            PanGesture()
              .onActionUpdate((event?: GestureEvent) => {
                if (event) {
                  //旧的大小
                  let oldArea = this.pinchValue * this.boxWidth
                  //计算在旧的大小上放大了多少倍
                  this.scaleValue = (oldArea + event.offsetX) / oldArea
                  //旧的放大倍数乘以新的放大倍数得到全新的放大倍数
                  this.scaleValue *= this.pinchValue

                  //重新计算位置偏移
                  //1.计算this.scaleValue * this.boxWidth 得到新的宽
                  //2.计算this.pinchValue * this.boxWidth 得到旧的宽
                  //3. (this.scaleValue * this.boxWidth - this.pinchValue * this.boxWidth) / 2 得到偏移量,这样才能保证左上角位置不变
                  this.offsetX =
                    this.positionX + (this.scaleValue * this.boxWidth - this.pinchValue * this.boxWidth) / 2;
                  this.offsetY =
                    this.positionY + (this.scaleValue * this.boxWidth - this.pinchValue * this.boxWidth) / 2;

                  this.changeCheckBoundaryValue();
                }
              })
              .onActionEnd(() => {
                this.pinchValue = this.scaleValue
                this.positionX = this.offsetX;
                this.positionY = this.offsetY;
              })
          )
      }
      .width(this.boxWidth)
      .height(this.boxHeight)
      .border({
        width: 1,
        color: Color.Black
      })
      .onAreaChange((oldValue: Area, newValue: Area)=>{
        this.dwidth = newValue.width as number
        this.dheight = newValue.height as number
        this.x = newValue.position.x as number
        this.y = newValue.position.y as number

        this.minW = -this.x
        this.maxW = this.fWidth as number - this.dwidth - this.x
        this.minH = -this.y
        this.maxH = this.fHeight as number - this.dheight - this.y
      })
      .draggable(false)
      .scale({ x: this.scaleValue, y: this.scaleValue, z: 1 })
      .translate({ x: this.offsetX, y: this.offsetY, z: 0 })
      .gesture(
        GestureGroup(GestureMode.Parallel,
          PinchGesture({ fingers: 2 })
            .onActionUpdate((event?: GestureEvent) => {
              if (event) {
                this.scaleValue = this.pinchValue * event.scale;
                this.changeCheckBoundaryValue();
              }
            })
            .onActionEnd(() => {
              this.pinchValue = this.scaleValue;
            }),
          PanGesture()
            .onActionUpdate((event?: GestureEvent) => {
              if (event) {
                console.log("xxxx:",this.scaleValue)
                this.offsetX = this.positionX + event.offsetX * this.scaleValue;
                this.offsetY = this.positionY + event.offsetY * this.scaleValue;
                this.checkBoundaryValue()
              }
            })
            .onActionEnd(() => {
              this.positionX = this.offsetX;
              this.positionY = this.offsetY;
              this.pinchValue = this.scaleValue
            })
        )

      )
    }
    .onSizeChange((oldValue: SizeOptions, newValue: SizeOptions)=>{
      //父组件容器宽高
      this.fWidth = newValue.width as number
      this.fHeight = newValue.height as number
    })
    .margin({top:40,left:20})
    .alignItems(HorizontalAlign.Center)
    .justifyContent(FlexAlign.Center)
    .width(310)
    .height(320)
    .backgroundColor("#29000000")
  }


  /**
   * 重新计算边界值
   */
  changeCheckBoundaryValue() {
    this.minW = -this.x + ((this.scaleValue - 1) * this.dwidth / 2)
    this.maxW = this.fWidth as number - this.dwidth * this.scaleValue - (this.x - (this.scaleValue - 1) * this.dwidth / 2);
    this.minH = -this.y + ((this.scaleValue - 1) * this.dheight / 2)
    this.maxH = this.fHeight as number - this.dheight* this.scaleValue - (this.y - (this.scaleValue - 1) * this.dheight / 2);
  }

  //处理边界值
   checkBoundaryValue() {
     if (this.offsetY > this.maxH) {
       this.offsetY = this.maxH
     }
     if (this.offsetY < this.minH) {
       this.offsetY = this.minH
     }
     if (this.offsetX > this.maxW) {
       this.offsetX = this.maxW
     }
     if (this.offsetX < this.minW) {
       this.offsetX = this.minW
     }
     //拖动到右下角边缘
     if( this.offsetX === this.maxW &&  this.offsetY === this.maxH){
       //缩小0.2倍
       this.scaleValue = this.pinchValue * 0.2
       //重新计算边界值
       this.changeCheckBoundaryValue()
       this.positionX = this.maxW;
       this.positionY = this.maxH;
     }
  }
}

没有默认的,自己实现吧,也不复杂

鸿蒙Next的图片预览功能基于ArkUI框架实现,支持常见图片格式(JPG、PNG、WEBP等)。通过Image组件可实现基础预览,结合Gallery组件可支持多图滑动浏览、缩放及手势操作。系统提供PixelMap进行底层图片数据处理,支持EXIF信息读取。预览性能通过异步加载和内存优化保障流畅体验。

在HarmonyOS Next中,可以通过以下方式实现图片和视频预览功能:

  1. 使用ImagePreview组件实现图片预览,支持缩放和拖动操作。通过设置enableZoomenableDrag属性控制交互行为。

  2. 视频预览使用VideoPlayer组件,通过设置scalingModeScalingMode.FIT禁止缩放,保持原始比例播放。

  3. 长按保存功能可通过onLongPress事件监听实现,调用MediaLibrary接口将文件保存至相册。注意处理运行时权限申请(READ_MEDIA和WRITE_MEDIA)。

示例代码片段:

// 图片预览
ImagePreview.show({
  images: [imageUrl],
  enableZoom: true,
  enableDrag: true
})

// 长按保存
onLongPress(() => {
  const mediaLib = mediaLibrary.getMediaLibrary(context)
  mediaLib.createAsset(mediaLibrary.MediaType.IMAGE, 'image.jpg', (err, asset) => {
    // 保存逻辑
  })
})

视频组件需单独处理交互限制,避免缩放操作。

回到顶部