三方库开源地址:https://atomgit.com/nutpi/flutter_ohos_image_editor_dove

前言

翻转是图片编辑器里另一个基础功能。自拍的时候前置摄像头拍出来的照片是镜像的,想翻过来变成正常的?翻转一下就行了。

这篇文章就来聊聊 image_editor_dove 是怎么实现翻转功能的。Flutter 提供了 Transform 组件配合 Matrix4,可以实现各种变换效果,翻转只是其中之一。
请添加图片描述


翻转的本质

翻转就是让图片沿着某条轴镜像,就像照镜子一样。在 image_editor_dove 里,翻转是水平翻转——左右镜像。

想象一下,你站在镜子前面,镜子里的你和真实的你是左右相反的。图片翻转就是这个效果。


RotateCanvasBinding - 状态管理

翻转的状态也是通过 RotateCanvasBinding Mixin 来管理的:

mixin RotateCanvasBinding<T extends StatefulWidget> on State<T> {
  int rotateValue = 0;
  double flipValue = 0;

flipValue 是一个浮点数:

  • 0 → 正常显示
  • π(约 3.14159)→ 翻转

为什么用 π?因为后面要用 Matrix4.rotationY() 来实现翻转,这个方法接收的是弧度值。π 弧度就是 180 度,刚好是完全翻转。


翻转方法

void flipCanvas() {
  flipValue = flipValue == 0 ? math.pi : 0;
  setState(() {});
}

逻辑很简单:如果当前是正常状态(0),就翻转(π);如果当前是翻转状态(π),就恢复正常(0)。每次点击都是在两个状态之间切换。

这是一个典型的"开关"逻辑,点一下翻转,再点一下恢复。


Transform - 实现翻转

翻转用的是 Transform 组件配合 Matrix4

Widget _buildImage() {
  return Transform(
    alignment: Alignment.center,
    transform: Matrix4.rotationY(flipValue),
    child: Container(
      alignment: Alignment.center,
      child: Image.file(widget.originImage),
    ),
  );
}

三个关键点:

  1. alignment: Alignment.center - 变换的中心点是组件的中心
  2. transform: Matrix4.rotationY(flipValue) - 绕 Y 轴旋转的变换矩阵
  3. child - 被变换的内容

alignment 的重要性

alignment 指定变换的中心点。如果不设置,默认是左上角。

// 以中心点为轴翻转(正确)
Transform(
  alignment: Alignment.center,
  transform: Matrix4.rotationY(math.pi),
  child: Image(...),
)

// 以左上角为轴翻转(错误,图片会跑到屏幕外面)
Transform(
  // alignment 默认是 Alignment.topLeft
  transform: Matrix4.rotationY(math.pi),
  child: Image(...),
)

如果以左上角为轴翻转,图片会绕着左上角转 180 度,结果就是图片跑到屏幕左边去了,看不见。


Matrix4.rotationY 的原理

你可能好奇,绕 Y 轴旋转怎么就变成水平翻转了?

想象一下三维空间:

  • X 轴是水平的,从左到右
  • Y 轴是垂直的,从下到上
  • Z 轴是垂直于屏幕的,从屏幕里面指向你

绕 Y 轴旋转 180 度,就相当于把图片像翻书一样翻过去。从正面看,图片就是左右镜像的。


数学上,Matrix4.rotationY(π) 生成的变换矩阵是这样的:

| -1   0   0   0 |
|  0   1   0   0 |
|  0   0  -1   0 |
|  0   0   0   1 |

这个矩阵的效果是:X 坐标取反,Y 坐标不变。所以原来在左边的像素跑到右边去了,原来在右边的像素跑到左边去了——这就是镜像翻转。


flipValue 的取值

flipValue 只有两个有意义的值:

  • 0 → 正常显示(单位矩阵,不做任何变换)
  • π → 水平翻转(X 坐标取反)

中间的值(比如 π/2)会产生一个"侧面"的效果,图片会变成一条线。这在翻转动画里可能有用,但作为最终状态没有意义。


翻转只影响图片

注意看代码,Transform 只包裹了图片,没有包裹涂鸦层和文字层:

RotatedBox(
  quarterTurns: rotateValue,
  child: Stack(
    children: [
      InteractiveViewer(child: _buildImage()),  // 只有图片被翻转
      _buildBrushCanvas(),  // 涂鸦不翻转
      buildTextCanvas(),    // 文字不翻转
    ],
  ),
)

这是故意的设计。翻转通常是为了纠正自拍的镜像问题,涂鸦和文字是用户后加的,不应该被翻转。

如果你想让涂鸦和文字也一起翻转,可以把 Transform 移到 Stack 外面:

RotatedBox(
  quarterTurns: rotateValue,
  child: Transform(
    alignment: Alignment.center,
    transform: Matrix4.rotationY(flipValue),
    child: Stack(
      children: [
        InteractiveViewer(child: Image.file(widget.originImage)),
        _buildBrushCanvas(),
        buildTextCanvas(),
      ],
    ),
  ),
)

操作按钮的实现

用户通过底部的按钮来触发翻转:

Widget _buildControlBar() {
  return Container(
    child: Row(
      children: [
        _buildButton(OperateType.flip, 'Flip', onPressed: flipCanvas),
        // 其他按钮...
      ],
    ),
  );
}

点击 Flip 按钮就会调用 flipCanvas() 方法,切换翻转状态。


翻转动画

默认的实现是瞬间翻转的,没有动画效果。如果想要平滑的翻转动画,可以用 AnimationController

late AnimationController _flipController;
late Animation<double> _flipAnimation;


void initState() {
  super.initState();
  _flipController = AnimationController(
    duration: Duration(milliseconds: 300),
    vsync: this,
  );
  _flipAnimation = Tween<double>(begin: 0, end: math.pi).animate(
    CurvedAnimation(parent: _flipController, curve: Curves.easeInOut),
  );
}

创建一个从 0 到 π 的动画。


void flipCanvas() {
  if (_flipController.status == AnimationStatus.completed) {
    _flipController.reverse();
  } else {
    _flipController.forward();
  }
}

点击时,如果当前是翻转状态就反向播放动画(恢复正常),否则正向播放动画(翻转)。



Widget build(BuildContext context) {
  return AnimatedBuilder(
    animation: _flipAnimation,
    builder: (context, child) {
      return Transform(
        alignment: Alignment.center,
        transform: Matrix4.rotationY(_flipAnimation.value),
        child: child,
      );
    },
    child: Image.file(widget.originImage),
  );
}

AnimatedBuilder 监听动画值的变化,每一帧都用新的角度值来变换。

动画效果是图片像翻书一样翻过去,中间会有一个"侧面"的过渡状态。


垂直翻转

image_editor_dove 默认只实现了水平翻转。如果你想要垂直翻转(上下镜像),可以用 Matrix4.rotationX

Transform(
  alignment: Alignment.center,
  transform: Matrix4.rotationX(flipValue),  // 绕 X 轴旋转
  child: Image(...),
)

绕 X 轴旋转 180 度,就是上下翻转。


同时支持水平和垂直翻转

如果想同时支持两种翻转,可以用两个状态变量:

double flipX = 0;  // 垂直翻转
double flipY = 0;  // 水平翻转

Transform(
  alignment: Alignment.center,
  transform: Matrix4.rotationX(flipX)..rotateY(flipY),
  child: Image(...),
)

Matrix4 支持链式调用,可以组合多个变换。


OpenHarmony 适配:检测图片 EXIF 方向

有些图片文件里包含 EXIF 信息,记录了拍摄时的方向。可以读取这个信息来自动翻转图片。

Dart 端定义方法

import 'package:flutter/services.dart';

const platform = MethodChannel('image_editor');

Future<Map<String, dynamic>?> getImageExifInfo(String filePath) async {
  try {
    final result = await platform.invokeMethod('getImageExifInfo', {
      'filePath': filePath,
    });
    return Map<String, dynamic>.from(result);
  } catch (e) {
    print('获取 EXIF 信息失败: $e');
    return null;
  }
}

传入图片路径,返回 EXIF 信息。


鸿蒙端实现

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 == "getImageExifInfo") {
      this.getImageExifInfo(call, result);
    } else {
      result.notImplemented();
    }
  }

设置 MethodChannel 并分发方法调用。


  private async getImageExifInfo(call: MethodCall, result: MethodResult): Promise<void> {
    try {
      const filePath = call.argument('filePath') as string;
      
      // 创建 ImageSource
      const imageSource = image.createImageSource(filePath);
      
      // 获取图片属性
      const properties = await imageSource.getImageProperty(image.PropertyKey.ORIENTATION);

使用鸿蒙的 image 模块读取图片的 EXIF 属性。


      // EXIF 方向值的含义:
      // 1: 正常
      // 2: 水平翻转
      // 3: 旋转 180 度
      // 4: 垂直翻转
      // 5: 顺时针 90 度 + 水平翻转
      // 6: 顺时针 90 度
      // 7: 逆时针 90 度 + 水平翻转
      // 8: 逆时针 90 度
      
      result.success({
        'orientation': properties,
        'needFlip': properties == '2' || properties == '4' || properties == '5' || properties == '7',
        'rotation': this.getRotationFromOrientation(properties),
      });
    } catch (e) {
      result.error("ExifError", e.toString(), null);
    }
  }
  
  private getRotationFromOrientation(orientation: string): number {
    switch (orientation) {
      case '3': return 180;
      case '6': return 90;
      case '8': return 270;
      default: return 0;
    }
  }
}

解析 EXIF 方向值,返回是否需要翻转以及需要旋转多少度。


根据 EXIF 自动调整

Future<void> autoAdjustByExif(String filePath) async {
  final exifInfo = await getImageExifInfo(filePath);
  if (exifInfo == null) return;
  
  // 自动翻转
  if (exifInfo['needFlip'] == true) {
    flipValue = math.pi;
  }
  
  // 自动旋转
  final rotation = exifInfo['rotation'] as int;
  rotateValue = rotation ~/ 90;
  
  setState(() {});
}

在打开图片时调用这个方法,可以自动根据 EXIF 信息调整图片方向。


保存翻转后的图片

翻转后的图片在截图时会被正确保存。因为 RepaintBoundary 截取的是渲染后的内容,包括所有变换效果。

final paintCanvas = Positioned.fromRect(
  rect: Rect.fromLTWH(0, headerHeight, screenWidth, canvasHeight),
  child: RepaintBoundary(
    key: _canvasKey,
    child: RotatedBox(
      quarterTurns: rotateValue,
      child: Stack(
        children: [
          InteractiveViewer(child: _buildImage()),  // _buildImage 里有 Transform
          _buildBrushCanvas(),
          buildTextCanvas(),
        ],
      ),
    ),
  ),
);

截图时,_buildImage() 返回的是经过 Transform 变换后的图片,所以翻转效果会被保留。


常见问题

翻转后图片消失

可能是 Transformalignment 没设置对。确保设置了 alignment: Alignment.center,让翻转以中心点为轴。


翻转后图片位置偏移

检查 Transform 的父组件是否有正确的约束。Transform 不会改变布局大小,如果父组件的约束不对,可能会导致位置偏移。


翻转动画中间有闪烁

可能是动画曲线的问题。试试用 Curves.linear 代替 Curves.easeInOut,看看是否有改善。


想要垂直翻转

Matrix4.rotationY 改成 Matrix4.rotationX


小结

翻转功能的核心要点:

  1. Transform + Matrix4.rotationY 实现水平翻转
  2. alignment: Alignment.center 确保以中心点为轴翻转
  3. flipValue 在 0 和 π 之间切换
  4. 翻转只影响图片,不影响涂鸦和文字
  5. 翻转动画需要用 AnimationController
  6. 可以读取 EXIF 信息自动调整图片方向

TransformMatrix4 是 Flutter 里实现各种变换效果的基础,理解了翻转的原理,旋转、缩放、倾斜等效果也就不难了。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐