Flutter for OpenHarmony 三方库适配教学 - image_editor_dove 马赛克功能
三方库开源地址:https://atomgit.com/nutpi/flutter_ohos_image_editor_dove
前言
马赛克是图片编辑器里很实用的功能。想遮住照片里的敏感信息?车牌号、手机号、人脸……用马赛克涂一涂就行了。
image_editor_dove 的马赛克功能和涂鸦功能共用同一套底层架构,但绑制方式完全不同。涂鸦是画线,马赛克是画像素块。这篇文章就来聊聊马赛克的实现原理。
马赛克的原理
马赛克效果的本质是什么?就是把图片的某个区域"像素化",让人看不清原来的内容。
在 image_editor_dove 里,马赛克的实现方式是:在用户手指经过的地方,绑制一堆小矩形,用不同的灰度值来模拟像素化效果。
这种方式有个好处——不需要真的去处理原图的像素,只是在图片上面叠加一层"遮罩"。实现简单,性能也好。
DrawStyle - 绘制模式切换
马赛克和涂鸦共用同一个控制器 SignatureController,通过 DrawStyle 来区分:
enum DrawStyle {
normal, // 普通涂鸦
mosaic, // 马赛克
}
控制器里有一个属性来存储当前的绘制模式:
class SignatureController extends ValueNotifier<List<Point>> {
// ...
DrawStyle drawStyle = DrawStyle.normal;
final double mosaicWidth;
// ...
}
mosaicWidth 控制马赛克像素块的大小,默认是 5.0。值越大,马赛克效果越明显。
切换到马赛克模式
在主编辑器里,用户点击马赛克按钮时会调用:
void switchPainterMode(DrawStyle style) {
if (lastDrawStyle == style) return;
changePainterColor(pColor);
painterController.drawStyle = style;
}
这个方法做了两件事:
- 先保存当前的绘制内容(通过
changePainterColor) - 然后切换绘制模式
为什么要先保存?因为切换模式后,之前画的内容应该保留。如果不保存,切换模式后之前的涂鸦就没了。
SignaturePainter 中的马赛克绘制
SignaturePainter 的 paint() 方法会根据 drawStyle 选择不同的绑制逻辑:
void paint(Canvas canvas, _) {
final List<Point> points = _controller.value;
if (points.isEmpty) {
return;
}
switch(_controller.drawStyle) {
case DrawStyle.normal:
canvas.drawPath(paintPath(), _penStyle);
break;
case DrawStyle.mosaic:
for(int i=0; i < points.length; i+=2) {
paintMosaic(points[i].offset);
}
break;
}
}
注意马赛克模式里的 i+=2。为什么要跳过一半的点?
因为马赛克每个点都要绑制一个 3x3 的像素块矩阵,如果每个点都画,性能会很差,而且效果也会太密集。跳过一半可以在保证效果的同时提高性能。
paintMosaic - 马赛克绑制核心
这是马赛克绘制的核心方法:
void paintMosaic(Offset center) {
final ui.Paint paint = ui.Paint()..color = Colors.black26;
final double size = _controller.mosaicWidth;
final double halfSize = size/2;
final ui.Rect b1 = Rect.fromCenter(
center: center.translate(-halfSize, -halfSize),
width: size,
height: size
);
首先创建一个基准矩形 b1,以手指位置为中心,稍微偏移一点。size 就是 mosaicWidth,控制每个像素块的大小。
接下来绘制 3x3 共 9 个小矩形:
// 第一行
canvas.drawRect(b1, paint); // (0,0) - 黑色 26% 透明度
paint.color = Colors.grey.withOpacity(0.5);
canvas.drawRect(b1.translate(0, size), paint); // (0,1) - 灰色 50% 透明度
paint.color = Colors.black38;
canvas.drawRect(b1.translate(0, size*2), paint); // (0,2) - 黑色 38% 透明度
第一列的三个矩形,用不同的灰度值。
// 第二行
paint.color = Colors.black12;
canvas.drawRect(b1.translate(size, 0), paint); // (1,0)
paint.color = Colors.black26;
canvas.drawRect(b1.translate(size, size), paint); // (1,1)
paint.color = Colors.black45;
canvas.drawRect(b1.translate(size, size*2), paint); // (1,2)
第二列的三个矩形。
// 第三行
paint.color = Colors.grey.withOpacity(0.5);
canvas.drawRect(b1.translate(size*2, 0), paint); // (2,0)
paint.color = Colors.black12;
canvas.drawRect(b1.translate(size*2, size), paint); // (2,1)
paint.color = Colors.black26;
canvas.drawRect(b1.translate(size*2, size*2), paint); // (2,2)
}
第三列的三个矩形。
为什么用不同的灰度值?如果都用同一个颜色,看起来就是一个大色块,没有马赛克的感觉。用不同的灰度值可以模拟出像素的随机感,看起来更像真正的马赛克效果。
马赛克的视觉效果
整个 3x3 矩阵的颜色分布大概是这样的:
[深灰] [浅灰] [中灰]
[浅灰] [深灰] [深灰]
[中灰] [浅灰] [深灰]
这种不规则的灰度分布,加上用户手指移动时产生的重叠,就形成了马赛克的效果。
调整马赛克效果
调整像素块大小
SignatureController(
mosaicWidth: 10.0, // 更大的像素块
// ...
)
mosaicWidth 越大,马赛克效果越明显,但也越粗糙。默认值 5.0 是个比较平衡的选择。
调整采样频率
在 paint() 方法里,我们用 i+=2 跳过了一半的点。如果想要更密集的马赛克:
for(int i=0; i < points.length; i+=1) { // 不跳过
paintMosaic(points[i].offset);
}
如果想要更稀疏的马赛克:
for(int i=0; i < points.length; i+=4) { // 跳过更多
paintMosaic(points[i].offset);
}
自定义颜色
默认的马赛克是灰色系的。如果想要其他颜色,可以修改 paintMosaic 方法:
void paintMosaic(Offset center) {
final baseColor = Colors.blue; // 改成蓝色系
final ui.Paint paint = ui.Paint()..color = baseColor.withOpacity(0.26);
// ...
}
不过灰色系的马赛克是最常见的,因为它能有效遮挡各种颜色的内容。
OpenHarmony 适配:真实马赛克效果
上面介绍的马赛克是"假"马赛克——只是在图片上叠加灰色块。如果想要"真"马赛克效果(真正把图片像素化),需要调用原生 API 处理图片。
Dart 端定义方法
import 'package:flutter/services.dart';
const platform = MethodChannel('image_editor');
Future<Uint8List?> applyRealMosaic(Uint8List imageBytes, List<Rect> regions, int blockSize) async {
try {
final result = await platform.invokeMethod('applyRealMosaic', {
'imageData': imageBytes,
'regions': regions.map((r) => {
'left': r.left,
'top': r.top,
'width': r.width,
'height': r.height,
}).toList(),
'blockSize': blockSize,
});
return result as Uint8List?;
} catch (e) {
print('应用马赛克失败: $e');
return null;
}
}
传入原图数据、需要马赛克的区域列表、像素块大小,返回处理后的图片。
鸿蒙端实现
import {
FlutterPlugin,
FlutterPluginBinding,
MethodCall,
MethodCallHandler,
MethodChannel,
MethodResult,
} from '@ohos/flutter_ohos';
import { image } from '@kit.ImageKit';
export default class ImageEditorPlugin implements FlutterPlugin, MethodCallHandler {
private channel: MethodChannel | null = null;
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.channel = new MethodChannel(binding.getBinaryMessenger(), "image_editor");
this.channel.setMethodCallHandler(this);
}
onMethodCall(call: MethodCall, result: MethodResult): void {
if (call.method == "applyRealMosaic") {
this.applyRealMosaic(call, result);
} else {
result.notImplemented();
}
}
设置 MethodChannel 并分发方法调用。
private async applyRealMosaic(call: MethodCall, result: MethodResult): Promise<void> {
try {
const imageData = call.argument('imageData') as Uint8Array;
const regions = call.argument('regions') as Array<{left: number, top: number, width: number, height: number}>;
const blockSize = call.argument('blockSize') as number;
从参数中获取图片数据、区域列表和像素块大小。
// 创建 ImageSource
const imageSource = image.createImageSource(imageData.buffer);
const pixelMap = await imageSource.createPixelMap();
// 获取图片信息
const imageInfo = await pixelMap.getImageInfo();
const width = imageInfo.size.width;
const height = imageInfo.size.height;
使用鸿蒙的 image 模块创建 PixelMap,这是鸿蒙处理图片的核心对象。
// 对每个区域应用马赛克
for (const region of regions) {
await this.mosaicRegion(pixelMap, region, blockSize, width, height);
}
// 导出处理后的图片
const packer = image.createImagePacker();
const packedData = await packer.packing(pixelMap, { format: 'image/png', quality: 100 });
result.success(new Uint8Array(packedData));
} catch (e) {
result.error("MosaicError", e.toString(), null);
}
}
对每个区域应用马赛克,然后导出处理后的图片。
private async mosaicRegion(
pixelMap: image.PixelMap,
region: {left: number, top: number, width: number, height: number},
blockSize: number,
imageWidth: number,
imageHeight: number
): Promise<void> {
const { left, top, width, height } = region;
// 确保区域在图片范围内
const startX = Math.max(0, Math.floor(left));
const startY = Math.max(0, Math.floor(top));
const endX = Math.min(imageWidth, Math.ceil(left + width));
const endY = Math.min(imageHeight, Math.ceil(top + height));
首先确保区域在图片范围内,避免越界。
// 按块处理
for (let y = startY; y < endY; y += blockSize) {
for (let x = startX; x < endX; x += blockSize) {
// 计算当前块的范围
const blockEndX = Math.min(x + blockSize, endX);
const blockEndY = Math.min(y + blockSize, endY);
// 获取块内第一个像素的颜色(简化处理)
const position: image.PositionArea = { x: x, y: y, size: { width: 1, height: 1 } };
const buffer = new ArrayBuffer(4);
await pixelMap.readPixels(position, buffer);
// 用这个颜色填充整个块
const fillPosition: image.PositionArea = {
x: x,
y: y,
size: { width: blockEndX - x, height: blockEndY - y }
};
await pixelMap.writePixels(fillPosition, buffer);
}
}
}
}
马赛克的核心算法:把区域分成若干个小块,每个块用同一个颜色填充。这里简化处理,用块内第一个像素的颜色来填充整个块。
更精确的做法是计算块内所有像素的平均颜色,但那样性能会差一些。
性能优化
马赛克绑制可能会有性能问题,特别是用户快速滑动的时候。
降低绘制频率
int _lastMosaicTime = 0;
void paintMosaic(Offset center) {
final now = DateTime.now().millisecondsSinceEpoch;
if (now - _lastMosaicTime < 32) { // 约 30fps
return;
}
_lastMosaicTime = now;
// 绘制逻辑...
}
限制马赛克的绘制频率,避免过于频繁的绘制。
使用 RepaintBoundary
RepaintBoundary(
child: CustomPaint(
painter: SignaturePainter(painterController),
child: ConstrainedBox(...),
),
)
把马赛克画布用 RepaintBoundary 包起来,这样重绘时不会影响其他部分。
批量绘制
如果点很多,可以考虑把多个点合并成一次绘制:
case DrawStyle.mosaic:
final path = Path();
for(int i=0; i < points.length; i+=2) {
final offset = points[i].offset;
path.addRect(Rect.fromCenter(
center: offset,
width: _controller.mosaicWidth * 3,
height: _controller.mosaicWidth * 3,
));
}
canvas.drawPath(path, _mosaicPaint);
break;
用 Path 收集所有矩形,然后一次性绘制,比逐个绘制效率高。
常见问题
马赛克效果不明显
调大 mosaicWidth 参数。默认是 5.0,可以试试 10.0 或更大的值。
马赛克太稀疏
减小 paint() 方法里的步进值。把 i+=2 改成 i+=1。
马赛克颜色太深/太浅
修改 paintMosaic 方法里的颜色值。比如把 Colors.black26 改成 Colors.black45 会更深。
性能问题
- 增大步进值(
i+=4或更大) - 增大
mosaicWidth(像素块越大,需要绘制的块越少) - 使用
RepaintBoundary隔离重绘
小结
马赛克功能的核心要点:
- 和涂鸦共用
SignatureController,通过DrawStyle区分模式 paintMosaic方法绘制 3x3 的像素块矩阵- 用不同的灰度值模拟像素化效果
- 跳过部分点来优化性能
- 真实马赛克效果需要调用原生 API 处理图片像素
马赛克功能的实现相对简单,但效果很实用。理解了它的原理,你也可以实现其他类似的效果,比如模糊、高斯模糊等。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)