HarmonyOS 鸿蒙Next中需要实现对图片的旋转、缩放,裁剪出指定的区域,如何实现?

HarmonyOS 鸿蒙Next中需要实现对图片的旋转、缩放,裁剪出指定的区域,如何实现? 需要实现对图片的旋转、缩放,裁剪出指定的区域,如何实现?

4 回复

【背景知识】

  • PixelMap:图像像素类,可以有效地存储图像的原始数据,使其可以方便地进行图像变换,如裁剪、缩放、偏移、旋转、翻转等。
  • PinchGesture:捏合手势事件,用于图片放大缩小。
  • PanGesture:滑动手势事件,用于图片平移。
  • Canvas:画布组件,用于自定义绘制裁剪图形。

【解决方案】

  1. 使用Canvas组件绘制裁剪框,自定义裁剪区域大小。
Canvas(this.context)
  .width('100%')
  .height('100%')
  .backgroundColor(Color.Transparent)
  .onReady(() => {
    if (this.context == null) {
      return;
    }
    let height = this.context.height;
    let width = this.context.width;
    this.context.fillStyle = this.model.maskColor;
    this.context.fillRect(0, 0, width, height);
    // 把中间的取景框透出来
    this.context.globalCompositeOperation = 'destination-out';
    this.context.fillStyle = 'white';
    let frameWidthInVp = this.getUIContext().px2vp(this.model.frameWidth);
    let frameHeightInVp = this.getUIContext().px2vp(this.model.getFrameHeight());
    let x = (width - this.getUIContext().px2vp(this.model.frameWidth)) / 2;
    let y = (height - this.getUIContext().px2vp(this.model.getFrameHeight())) / 2;
    this.context.fillRect(x, y, frameWidthInVp, frameHeightInVp);
    // 画出取景框边线,使边界明显一点
    this.context.globalCompositeOperation = 'source-over';
    this.context.strokeStyle = this.model.strokeColor;
    this.context.strokeRect(x, y, frameWidthInVp, frameHeightInVp);
  })
  .enabled(false);
  1. 给图片添加平移、捏合手势,用于移动图片指定区域到裁剪框中。
.gesture(
  GestureGroup(GestureMode.Parallel,
    // 拖动手势
    PanGesture({})
      .onActionStart(() => {
        this.startOffsetX = this.model.offsetX;
        this.startOffsetY = this.model.offsetY;
      })
      .onActionUpdate((event: GestureEvent) => {
        if (event) {
          if (this.model.panEnabled) {
            let distanceX: number = this.startOffsetX;
            let distanceY: number = this.startOffsetY;
            // 根据旋转角度调整移动方向
            const rotation = this.model.rotation;
            if (rotation === 0) {
              // 0度旋转:正常移动
              distanceX += this.getUIContext().vp2px(event.offsetX) / this.model.scale;
              distanceY += this.getUIContext().vp2px(event.offsetY) / this.model.scale;
              this.model.offsetX = distanceX;
              this.model.offsetY = distanceY;
            } else if (rotation === 90) {
              // 90度旋转:X和Y方向交换,Y方向取反
              distanceX += this.getUIContext().vp2px(event.offsetY) / this.model.scale;
              distanceY -= this.getUIContext().vp2px(event.offsetX) / this.model.scale;
              this.model.offsetX = distanceX;
              this.model.offsetY = distanceY;
            } else if (rotation === 180) {
              // 180度旋转:X和Y方向都取反
              distanceX -= this.getUIContext().vp2px(event.offsetX) / this.model.scale;
              distanceY -= this.getUIContext().vp2px(event.offsetY) / this.model.scale;
              this.model.offsetX = distanceX;
              this.model.offsetY = distanceY;
            } else if (rotation === 270) {
              // 270度旋转:X和Y方向交换,X方向取反
              distanceX -= this.getUIContext().vp2px(event.offsetY) / this.model.scale;
              distanceY += this.getUIContext().vp2px(event.offsetX) / this.model.scale;
              this.model.offsetX = distanceX;
              this.model.offsetY = distanceY;
            }
            this.model.offsetX = distanceX;
            this.model.offsetY = distanceY;
            this.updateMatrix();
          }
        }
      })
      .onActionEnd(() => {
        this.checkImageAdapt();
      }),
    // 缩放手势处理
    PinchGesture({ fingers: 2 })
      .onActionStart(() => {
        this.tempScale = this.model.scale;
      })
      .onActionUpdate((event) => {
        if (event) {
          if (!this.model.zoomEnabled) {
            return;
          }
          this.zoomTo(this.tempScale * event.scale, this.model.rotation);
        }
      })
      .onActionEnd(() => {
        this.checkImageAdapt();
      }),
    RotationGesture({ fingers: 3 })
      .onActionStart(() => {
        this.tempRotate = this.model.rotation;
      })
      .onActionUpdate((event) => {
        if (event) {
          if (!this.model.zoomEnabled) {
            return;
          }
          this.zoomTo(1, this.tempRotate + event.angle);
        }
      })
      .onActionEnd(() => {
        this.checkImageAdapt();
      })
  )
);
  1. 根据手势结果,调整图片位置,确保图片填满裁剪框。
private checkImageAdapt() {
  let offsetX = this.model.offsetX;
  let offsetY = this.model.offsetY;
  let scale = this.model.scale;
  hilog.info(0, 'CropPage', `offsetX: ${offsetX}, offsetY: ${offsetY}, scale: ${scale}`);

  // 图片适配控件的时候也进行了缩放,计算出这个缩放比例
  let widthScale = this.model.componentWidth / this.model.imageWidth;
  let heightScale = this.model.componentHeight / this.model.imageHeight;
  let adaptScale = Math.min(widthScale, heightScale);
  hilog.info(0, 'CropPage',
    `Image scale ${adaptScale} while attaching the component[${this.model.componentWidth}, ${this.model.componentHeight}]`);

  // 经过两次缩放(适配控件、手势)后,图片的实际显示大小
  let showWidth = this.model.imageWidth * adaptScale * this.model.scale;
  let showHeight = this.model.imageHeight * adaptScale * this.model.scale;

  if (this.model.rotation === 0) {
    showWidth = this.model.imageWidth * adaptScale * this.model.scale;
    showHeight = this.model.imageHeight * adaptScale * this.model.scale;
  } else if (this.model.rotation === 90) {
    showWidth = this.model.imageHeight * adaptScale * this.model.scale;
    showHeight = this.model.imageWidth * adaptScale * this.model.scale;
  } else if (this.model.rotation === 180) {
    showWidth = this.model.imageWidth * adaptScale * this.model.scale;
    showHeight = this.model.imageHeight * adaptScale * this.model.scale;
  } else if (this.model.rotation === 270) {
    showWidth = this.model.imageHeight * adaptScale * this.model.scale;
    showHeight = this.model.imageWidth * adaptScale * this.model.scale;
  }
  let imageX = (this.model.componentWidth - showWidth) / 2;
  let imageY = (this.model.componentHeight - showHeight) / 2;
  hilog.info(0, 'CropPage', `Image left top is (${imageX}, ${imageY})`);

  // 取景框的左上角坐标
  let frameX = (this.model.componentWidth - this.model.frameWidth) / 2;
  let frameY = (this.model.componentHeight - this.model.getFrameHeight()) / 2;
  hilog.info(0, 'CropPage', `Frame left top is (${frameX}, ${frameY})`);

  // 图片左上角坐标
  let showX = imageX + offsetX * scale;
  let showY = imageY + offsetY * scale;
  hilog.info(0, 'CropPage', `Image show at (${showX}, ${showY})`);

  if (this.model.rotation === 0) {
    showX = imageX + offsetX * scale;
    showY = imageY + offsetY * scale;
  } else if (this.model.rotation === 90) {
    showX = imageX - offsetY * scale;
    showY = imageY + offsetX * scale;
  } else if (this.model.rotation === 180) {
    showX = imageX - offsetX * scale;
    showY = imageY - offsetY * scale;
  } else if (this.model.rotation === 270) {
    showX = imageX + offsetY * scale;
    showY = imageY - offsetX * scale;
  }

  // 图片缩放后,大小不足以填满取景框
  if (this.model.frameWidth > showWidth || this.model.getFrameHeight() > showHeight) {
    let xScale = this.model.frameWidth / showWidth;
    let yScale = this.model.getFrameHeight() / showHeight;
    let newScale = Math.max(xScale, yScale);
    this.model.scale = this.model.scale * newScale;
    showX *= newScale;
    showY *= newScale;
  }

  // 调整x轴方向位置,使图像填满取景框
  if (showX > frameX) {
    showX = frameX;
  } else if (showX + showWidth < frameX + this.model.frameWidth) {
    showX = frameX + this.model.frameWidth - showWidth;
  }
  // 调整y轴方向位置,使图像填满取景框
  if (showY > frameY) {
    showY = frameY;
  } else if (showY + showHeight < frameY + this.model.getFrameHeight()) {
    showY = frameY + this.model.getFrameHeight() - showHeight;
  }
  this.model.offsetX = (showX - imageX) / scale;
  this.model.offsetY = (showY - imageY) / scale;

  if (this.model.rotation === 0) {
    this.model.offsetX = (showX - imageX) / scale;
    this.model.offsetY = (showY - imageY) / scale;
  } else if (this.model.rotation === 90) {
    this.model.offsetX = (showY - imageY) / scale;
    this.model.offsetY = -(showX - imageX) / scale;
  } else if (this.model.rotation === 180) {
    this.model.offsetX = -(showX - imageX) / scale;
    this.model.offsetY = -(showY - imageY) / scale;
  } else if (this.model.rotation === 270) {
    this.model.offsetX = -(showY - imageY) / scale;
    this.model.offsetY = (showX - imageX) / scale;
  }
  this.updateMatrix();
}

public updateMatrix(): void {
  this.matrix = Matrix4.identity()
    .translate({ x: this.model.offsetX, y: this.model.offsetY })
    .rotate({ z: 1, angle: this.model.rotation })
    .scale({ x: this.model.scale, y: this.model.scale });
}
  1. 根据缩放比例、平移位置和裁剪区域坐标裁剪图片。
public async crop(): Promise<image.PixelMap> {
  if (!this.src || this.src === '') {
    throw new Error('Please set src first');
  }
  if (this.imageWidth === 0 || this.imageHeight === 0) {
    throw new Error('The image is not loaded');
  }

  // 打开图片文件
  let file = fs.openSync(this.src, fs.OpenMode.READ_ONLY);
  let imageSource: image.ImageSource = image.createImageSource(file.fd);
  let decodingOptions: image.DecodingOptions = {
    editable: true,
    desiredPixelFormat: image.PixelMapFormat.RGBA_8888,
  };

  // 创建pixelMap
  let pm = await imageSource.createPixelMap(decodingOptions);

  // 获取原始图像尺寸
  let originalWidth = this.imageWidth;
  let originalHeight = this.imageHeight;

  // 第一步:先旋转整个图像(注意:rotateSync 会修改图像尺寸)
  if (this.rotation !== 0) {
    pm.rotateSync(this.rotation);

    // 更新图像尺寸(90度和270度旋转会交换宽高)
    if (this.rotation === 90 || this.rotation === 270) {
      let tmp_wh = originalWidth;
      originalWidth = originalHeight;
      originalHeight = tmp_wh;
    }
  }

  // 第二步:计算缩放比例与裁剪区域
  // 图片适配控件时的缩放比例
  let widthScale = this.componentWidth / this.imageWidth;
  let heightScale = this.componentHeight / this.imageHeight;
  let adaptScale = Math.min(widthScale, heightScale);

  // 总缩放比例 = 适配缩放 × 手势缩放
  let totalScale = adaptScale * this.scale;

  // 旋转 90° 或 270° 时,图像宽高交换,缩放比例应基于新方向
  let totalRotateScale = totalScale;

  // 仅在 90°/270° 时,使用 totalRotateScale 作为裁剪坐标换算比例
  if (this.rotation === 90 || this.rotation === 270) {
    totalRotateScale = totalScale;
  }

  // 计算图片在组件中的实际显示大小
  let showWidth = originalWidth * totalScale;
  let showHeight = originalHeight * totalScale;

  // 图片左上角坐标(基于组件坐标系)
  let imageX = (this.componentWidth - showWidth) / 2;
  let imageY = (this.componentHeight - showHeight) / 2;

  // 取景框左上角坐标(基于组件坐标系)
  let frameX = (this.componentWidth - this.frameWidth) / 2;
  let frameY = (this.componentHeight - this.getFrameHeight()) / 2;

  // 图片在组件中实际显示的位置(考虑旋转后的偏移)
  let showX = 0, showY = 0;

  if (this.rotation === 0) {
    showX = imageX + this.offsetX * this.scale;
    showY = imageY + this.offsetY * this.scale;
  } else if (this.rotation === 90) {
    showX = imageX - this.offsetY * this.scale;
    showY = imageY + this.offsetX * this.scale;
  } else if (this.rotation === 180) {
    showX = imageX - this.offsetX * this.scale;
    showY = imageY - this.offsetY * this.scale;
  } else if (this.rotation === 270) {
    showX = imageX + this.offsetY * this.scale;
    showY = imageY - this.offsetX * this.scale;
  }

  // 计算裁剪区域坐标(基于旋转后图像的像素坐标)
  let x = 0, y = 0, cropWidth = 0, cropHeight = 0;
  if (this.rotation === 0 || this.rotation === 180) {
    // 0° 和 180°:宽高不变,使用 totalScale
    x = (frameX - showX) / totalScale;
    y = (frameY - showY) / totalScale;
    cropWidth = this.frameWidth / totalScale;
    cropHeight = this.getFrameHeight() / totalScale;
  } else if (this.rotation === 90 || this.rotation === 270) {
    // 90° 和 270°:宽高交换,使用 totalRotateScale(即 totalScale)
    x = (frameX - showX) / totalRotateScale;
    y = (frameY - showY) / totalRotateScale;
    cropWidth = this.frameWidth / totalRotateScale;
    cropHeight = this.getFrameHeight() / totalRotateScale;
  }

  // 确保裁剪区域不超出图像边界
  x = Math.max(0, Math.min(x, originalWidth - cropWidth));
  y = Math.max(0, Math.min(y, originalHeight - cropHeight));
  cropWidth = Math.min(cropWidth, originalWidth);
  cropHeight = Math.min(cropHeight, originalHeight);

  // 裁剪图像(基于旋转后图像的像素坐标)
  let region: image.Region = {
    x: x,
    y: y,
    size: {
      width: cropWidth,
      height: cropHeight
    }
  };
  pm.cropSync(region);
  return pm;
};

完整示例如下:

import { image } from '[@kit](/user/kit).ImageKit';
import fs from '[@ohos](/user/ohos).file.fs';

interface ImageLoadedEvent {
  width: number;
  height: number;
  componentWidth: number;
  componentHeight: number;
  loadingStatus: number;
  contentWidth: number;
  contentHeight: number;
  contentOffsetX: number;
  contentOffsetY: number;
}

export interface ImageLoadEventListener {

  onImageLoaded(msg: ImageLoadedEvent): void;

  onImageLoadError(error: ImageError): void;
}

export class CropModel {
  /**
   * 图片uri
   * 目前支持string,其他形式需要先转换成路径
   */
  src: string = '';
  /**
   * 图片预览
   */
  previewSource: string | Resource = '';
  /**
   * 是否可以拖动
   */
  panEnabled: boolean = true;
  /**
   * 是否可以缩放
   */
  zoomEnabled: boolean = true;

更多关于HarmonyOS 鸿蒙Next中需要实现对图片的旋转、缩放,裁剪出指定的区域,如何实现?的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


在HarmonyOS Next中,使用@ohos.multimedia.image模块的Image类处理图片。通过createPixelMap()获取图像数据,利用ImagePacker打包操作。旋转使用rotation属性设置角度;缩放通过调整目标尺寸调用scale()方法;裁剪需定义Region区域,结合cropRect方法提取指定部分。操作完成后用pack()生成处理后的图片。

在HarmonyOS Next中,可以通过Image组件和图形处理能力实现图片的旋转、缩放和裁剪。以下是具体实现方法:

  1. 旋转图片
    使用Image组件的rotate属性设置旋转角度(单位:度):

    Image($r('app.media.example'))
      .rotate(45) // 旋转45度
    
  2. 缩放图片
    通过scale属性控制缩放比例(x/y轴可独立设置):

    Image($r('app.media.example'))
      .scale({ x: 0.5, y: 0.8 }) // x轴缩放到50%,y轴缩放到80%
    
  3. 裁剪指定区域
    使用clip属性结合Shape(如矩形、圆形)定义裁剪区域:

    Image($r('app.media.example'))
      .clip(
        Rectangle() // 矩形裁剪区域
          .width(100)
          .height(100)
          .offset({ x: 50, y: 50 }) // 从坐标(50,50)开始裁剪
      )
    

组合使用示例

Image($r('app.media.example'))
  .rotate(30)
  .scale({ x: 1.2, y: 1.2 })
  .clip(Rectangle().width(150).height(150).offset({ x: 20, y: 20 }))

注意事项

  • 裁剪区域超出图片边界时自动截断。
  • 变换操作按声明顺序执行(如先旋转后缩放)。
  • 可通过position调整裁剪后的显示位置。

对于动态交互(如手势控制),可结合@State变量实时更新属性值。

回到顶部