背景引入

在现代移动应用开发中,图片编辑功能已成为众多应用的标配功能之一。特别是在社交、内容创作、电商等场景中,用户经常需要对图片进行编辑处理,其中马赛克功能是保护隐私、突出重点内容的重要工具。在HarmonyOS应用开发中,很多开发者面临如何在ArkUI中实现灵活、高性能的图片马赛克功能的问题。

问题现象

在实际开发中,开发者需要实现以下功能场景:

  1. 用户从手机相册中选择图片

  2. 在图片上通过触摸手势添加马赛克效果

  3. 支持撤销操作,能够回退到之前的绘制状态

  4. 马赛克效果需要平滑、自然,且性能良好

关键的技术挑战在于:

  • 如何准确获取触摸点的位置信息

  • 如何高效地处理像素数据

  • 如何实现撤销/重做功能

  • 如何保证绘制性能不卡顿

解决方案

1. 核心技术原理

根据HarmonyOS官方文档,实现图片马赛克效果主要依赖以下几个核心技术:

1.1 Canvas绘制技术
// 创建Canvas绘图上下文
private myPen: CanvasRenderingContext2D = new CanvasRenderingContext2D(new RenderingContextSettings(true));

// 绘制图片
private clearAndDraw() {
  this.myPen.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
  if (this.maskUri) {
    this.myPen.drawImage(new ImageBitmap(this.maskUri), 0, 0, this.canvasWidth, this.canvasHeight);
  }
}
1.2 像素数据获取

通过getImageData方法获取触摸点处的像素颜色数据,这是实现马赛克效果的关键:

private draw(x: number, y: number) {
  // 获取触摸点1x1像素区域的颜色数据
  const data = this.myPen.getImageData(x, y, 1, 1).data;
  // 提取RGB通道值
  const color = `rgb(${data[0]}, ${data[1]}, ${data[2]})`;
  this.myPen.fillStyle = color;
  // 用获取的颜色填充矩形区域,形成马赛克块
  this.myPen.fillRect(x, y, this.maskRadius, this.maskRadius);
}
1.3 触摸事件处理

通过Canvas的onTouch事件监听用户触摸操作:

.onTouch((event) => {
  const x = event.touches[0].x, y = event.touches[0].y;
  switch (event.type) {
    case TouchType.Down:
      // 开始新的绘制路径
      const path = new DrawingPath();
      path.points.push({ x: x, y: y });
      this.paths.push(path);
      break;
    case TouchType.Move:
      // 记录移动路径点
      this.paths[this.paths.length-1].points.push({ x: x, y: y });
      this.draw(x, y);
      break;
  }
})

2. 关键实现步骤

步骤1:图片选择与加载
// 选择图片
private async selectImage() {
  const picker = new photoAccessHelper.PhotoViewPicker();
  const result = await picker.select({
    maxSelectNumber: 1,
    MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE
  });
  if (!result.photoUris.length) {
    return;
  }
  this.processImage(result.photoUris[0]);
}
步骤2:图片尺寸适配

根据屏幕尺寸和图片原始尺寸计算合适的显示比例:

private calculateScaleFactor() {
  const widthRatio = this.screenWidth / this.canvasWidth;
  const heightRatio = this.screenHeight / this.canvasHeight;
  this.scaleFactor = Math.min(widthRatio, heightRatio);
  if (this.scaleFactor > 1) {
    this.scaleFactor = 1; // 不放大图片
  }
}
步骤3:撤销功能实现

通过数组记录所有绘制路径,实现撤销功能:

Button('撤销').onClick(() => {
  if (this.paths.length >= 1) {
    this.paths.pop()!!; // 移除最后一条路径
  }
  if (this.maskUri) {
    // 清空画布重新绘制
    this.clearAndDraw();
    // 重绘所有剩余的路径
    this.paths.forEach((path) => {
      for (let i = 0; i < path.points.length; i++) {
        this.draw(path.points[i].x, path.points[i].y);
      }
    });
  }
});

3. 性能优化技巧

3.1 减少重绘范围

只重绘变化的区域,而不是整个画布:

// 优化版本:只绘制受影响的区域
private drawOptimized(x: number, y: number) {
  const data = this.myPen.getImageData(x, y, 1, 1).data;
  const color = `rgb(${data[0]}, ${data[1]}, ${data[2]})`;
  this.myPen.fillStyle = color;
  
  // 只绘制受影响的马赛克块区域
  const affectedArea = {
    x: x - this.maskRadius/2,
    y: y - this.maskRadius/2,
    width: this.maskRadius * 2,
    height: this.maskRadius * 2
  };
  
  this.myPen.fillRect(x, y, this.maskRadius, this.maskRadius);
}
3.2 防抖处理触摸事件

避免频繁触发重绘导致的性能问题:

private lastDrawTime: number = 0;
private drawThrottle(x: number, y: number) {
  const now = Date.now();
  if (now - this.lastDrawTime < 16) { // 约60fps
    return;
  }
  this.lastDrawTime = now;
  this.draw(x, y);
}

完整示例

以下是完整的图片马赛克实现代码:

import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { display, window } from '@kit.ArkUI';
import image from '@ohos.multimedia.image';
import { fileIo } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';

interface Point {
  x: number;
  y: number;
}

export class DrawingPath {
  points: Point[] = [];
}

@Entry
@Component
struct ImageAddMaskCase {
  @State @Watch('clearAndDraw')
  maskUri: string = '';
  
  // 画布尺寸
  @State private canvasWidth: number = 0;
  @State private canvasHeight: number = 0;
  
  // 展示区域的尺寸
  private screenWidth: number = 0;
  private screenHeight: number = 0;
  
  // 缩放比例
  @State private scaleFactor: number = 1;
  
  // 马赛克像素大小
  private maskRadius: number = 30;
  private paths: DrawingPath[] = [];
  private myPen: CanvasRenderingContext2D = new CanvasRenderingContext2D(new RenderingContextSettings(true));

  // 选择图片
  private async selectImage() {
    const picker = new photoAccessHelper.PhotoViewPicker();
    const result = await picker.select({
      maxSelectNumber: 1,
      MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE
    });
    if (!result.photoUris.length) {
      return;
    }
    this.processImage(result.photoUris[0]);
  }

  // 图片处理
  private async processImage(uri: string) {
    try {
      const file = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY);
      const imageSource = image.createImageSource(file.fd);
      const decodingOptions: image.DecodingOptions = {
        editable: true,
        desiredPixelFormat: 3,
      };
      
      imageSource.createPixelMap(decodingOptions, (err: BusinessError, pixelMap: image.PixelMap) => {
        if (err) {
          console.error(`Failed to create pixelMap. Code: ${err.code}, Message: ${err.message}`);
          return;
        }
        pixelMap.getImageInfo().then(info => {
          // 更新画布尺寸
          this.updateCanvasSize(info.size);
          this.maskUri = uri;
          // 计算缩放比例
          this.calculateScaleFactor();
        });
      });
    } catch (error) {
      console.error('Error processing image:', error);
    }
  }

  private updateCanvasSize(size: image.Size) {
    this.canvasWidth = this.getUIContext().px2vp(size.width);
    this.canvasHeight = this.getUIContext().px2vp(size.height);
  }

  // 计算缩放比例
  private calculateScaleFactor() {
    const widthRatio = this.screenWidth / this.canvasWidth;
    const heightRatio = this.screenHeight / this.canvasHeight;
    this.scaleFactor = Math.min(widthRatio, heightRatio);
    if (this.scaleFactor > 1) {
      this.scaleFactor = 1;
    }
  }

  // 绘制图片
  private clearAndDraw() {
    this.myPen.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
    if (this.maskUri) {
      this.myPen.drawImage(new ImageBitmap(this.maskUri), 0, 0, this.canvasWidth, this.canvasHeight);
    }
  }

  // 获取屏幕尺寸
  private getScreenDimensions() {
    window.getLastWindow(this.getUIContext().getHostContext()).then(win => {
      const topSafeHeight = 
        this.getUIContext().px2vp(win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM).topRect.height);
      const bottomSafeHeight = this.getUIContext()
        .px2vp(win.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR).bottomRect.height);
      const displayInfo = display.getDefaultDisplaySync();
      this.screenWidth = this.getUIContext().px2vp(displayInfo.width);
      this.screenHeight = this.getUIContext().px2vp(displayInfo.height) - topSafeHeight - bottomSafeHeight - 50;
    });
  }

  aboutToAppear() {
    this.getScreenDimensions();
  }

  build() {
    Column() {
      Row({ space: 20 }) {
        Button('选择图片').onClick(() => this.selectImage());
        
        Button('撤销').onClick(() => {
          if (this.paths.length >= 1) {
            this.paths.pop()!!;
          }
          if (this.maskUri) {
            this.clearAndDraw();
            this.paths.forEach((path) => {
              for (let i = 0; i < path.points.length; i++) {
                this.draw(path.points[i].x, path.points[i].y);
              }
            });
          }
        });
        
        Button('清空').onClick(() => {
          if (this.maskUri) {
            this.clearAndDraw();
          }
        });
      }
      .width('100%')
      .height(50)
      .justifyContent(FlexAlign.Center);

      Canvas(this.myPen)
        .onReady(() => this.clearAndDraw())
        .width(this.canvasWidth)
        .height(this.canvasHeight)
        .scale({
          x: this.scaleFactor,
          y: this.scaleFactor,
          centerX: 0,
          centerY: 0
        })
        .onTouch((event) => {
          const x = event.touches[0].x, y = event.touches[0].y;
          switch (event.type) {
            case TouchType.Down:
              const path = new DrawingPath();
              path.points.push({ x: x, y: y });
              this.paths.push(path);
              break;
            case TouchType.Move:
              this.paths[this.paths.length-1].points.push({ x: x, y: y });
              this.draw(x, y);
              break;
          }
        })
        .backgroundColor(Color.Pink);
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Start)
    .alignItems(HorizontalAlign.Start);
  }

  private draw(x: number, y: number) {
    const data = this.myPen.getImageData(x, y, 1, 1).data;
    const color = `rgb(${data[0]}, ${data[1]}, ${data[2]})`;
    this.myPen.fillStyle = color;
    this.myPen.fillRect(x, y, this.maskRadius, this.maskRadius);
  }
}

常见问题与解决方案

问题1:马赛克效果不自然

原因:马赛克块大小设置不合理

解决方案

// 根据图片尺寸动态调整马赛克块大小
private calculateMaskRadius(imageWidth: number, imageHeight: number) {
  const minDimension = Math.min(imageWidth, imageHeight);
  // 马赛克块大小为图片最小尺寸的1/30
  this.maskRadius = Math.max(10, Math.floor(minDimension / 30));
}

问题2:绘制时性能卡顿

原因:每次触摸都触发完整的重绘

解决方案

// 使用双缓冲技术优化绘制性能
private bufferCanvas: CanvasRenderingContext2D = new CanvasRenderingContext2D(new RenderingContextSettings(true));

private drawWithBuffer(x: number, y: number) {
  // 在缓冲区绘制
  const data = this.myPen.getImageData(x, y, 1, 1).data;
  const color = `rgb(${data[0]}, ${data[1]}, ${data[2]})`;
  this.bufferCanvas.fillStyle = color;
  this.bufferCanvas.fillRect(x, y, this.maskRadius, this.maskRadius);
  
  // 一次性将缓冲区内容绘制到主画布
  this.myPen.drawImage(this.bufferCanvas.getImageData(), 0, 0);
}

问题3:撤销功能无法完全恢复

原因:路径记录不完整

解决方案

// 记录完整的绘制状态
export class DrawingState {
  points: Point[] = [];
  color: string = '';
  timestamp: number = Date.now();
  
  constructor(x: number, y: number, color: string) {
    this.points.push({ x, y });
    this.color = color;
  }
}

总结

实现HarmonyOS图片马赛克功能的核心在于:

  1. 正确使用Canvas API:掌握getImageDatafillRect等核心绘图方法

  2. 合理处理触摸事件:准确记录用户的绘制路径

  3. 实现状态管理:通过数组记录绘制历史,支持撤销功能

  4. 性能优化:合理控制重绘范围,避免性能问题

在实际开发中,还可以进一步扩展功能:

  • 支持不同马赛克形状(圆形、菱形等)

  • 实现多种马赛克强度级别

  • 添加保存和分享功能

  • 支持多图层编辑

通过本文提供的解决方案,开发者可以快速在HarmonyOS应用中实现高效、灵活的马赛克功能,为用户提供更好的图片编辑体验。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐