Flutter开发笔记 —— 矩阵应用在CustomPainter的基础应用

前言

好久没有写文章了,最近在学习手势绘制时开发了一个简易的电子白板demo,后续会单独写一个电子白板的项目开发实战,在开发当中处理白板中各种手势时,增加矩阵的效果,但是在使用矩阵时碰到了发生矩阵变换后,需要重新计算当前相对的点位从而不影响当前手势的正确绘制,所以在我踩坑后,特地写下这篇文章,希望对你有帮助

效果视频

逻辑

这里不做详细的绘制逻辑,单提供绘制以及矩阵相对点位思想和示例代码

画布

class CustomWhiteBoard extends CustomPainter {
  WhiteBoardHandle whiteBoardHandle = WhiteBoardHandle();

  CustomWhiteBoard(this.whiteBoardHandle) : super(repaint: whiteBoardHandle);

  @override
  void paint(Canvas canvas, Size size) {
    Rect rect = Rect.fromLTWH(0, 0, size.width, size.height);
    canvas.saveLayer(rect, Paint());
    onMatrix4(canvas, size);
    //...绘制
    canvas.restore();
    // TODO: implement paint
  }

  @override
  bool shouldRepaint(covariant CustomWhiteBoard oldDelegate) {
    return true;
  }
  // ...这里是绘制矩阵
  void onMatrix4(Canvas canvas,Size size){
    final Matrix4 result = Matrix4.identity();

    result.multiply(whiteBoardHandle.matrix4.value);
    canvas.transform(result.storage);
  }
}

画板这里就是一个很简单的绘制矩阵而已,不过多讲解具体画板绘制

功能实现

我当前使用手势为:ScaleGestureRecognizer (缩放手势识别器) 非使用 PanGestureRecognizer

ScaleGestureRecognizer 和PanGestureRecognizer 手势不能一起使用,但是ScaleGestureRecognizer 中包含的功能已经足够使用了,不需要PanGestureRecognizer

这里是手势工厂基础代码


  Map<Type, GestureRecognizerFactory> getGesture(){

    var scaleGesture = GestureRecognizerFactoryWithHandlers<ScaleGestureRecognizer>(() => ScaleGestureRecognizer(), (instance) {
      instance.onStart = onScaleStart;
      instance.onUpdate = onScaleUpdate;
      instance.onEnd = onScaleEnd;
    });

    return {
      ScaleGestureRecognizer: scaleGesture
    };
  }

绑定在视图的RawGestureDetector的gesture参数中即可

我们基础的手势和绘制逻辑好了,接下来着重一下手势的相关实现代码Start -> End

定义矩阵

在你的State中定义两个基础矩阵 Matrix4 matrix 和 Matrix4 tempMatrix

matrix用于全局矩阵,tempMatrix用于临时矩阵,用于保留矩阵状态

手势实现逻辑

我们先来思考一下矩阵在每个手势状态需要负责的逻辑

  • Start(开始状态) 这里主要负责当前手势中的触点,一般用于平移的起始点缓存
  • Update(修改状态) 这里主要负责矩阵三个核心使用点(平移、缩放、旋转)的相关逻辑处理
  • End(结束状态) 这里主要负责tempMatrix(缓存矩阵)的缓存

既然基础思路有了,那我们具体看看实现代码

Start

  void onScaleStart(ScaleStartDetails details){
 
        //校验手势触点
        if(details.pointerCount == 1){
        //缓存手势起始点
          state.handle.translateStartOffset = details.focalPoint;
        }
  }

Update


  void onScaleUpdate(ScaleUpdateDetails details){
    //当前画笔状态 这里是个枚举用来区分矩阵处理
    switch(state.paintType.value){
      case PaintType.scale:
        state.handle.changeMatrix4(state.handle.tempMatrix.multiplied(Matrix4.diagonal3Values(scale,scale,1)));
        break;
      case PaintType.rotate:
        state.handle.changeMatrix4(state.handle.recordMatrix.multiplied(Matrix4.rotationZ(details.rotation)));
        break;
      case PaintType.translate:
        state.handle.changeMatrix4(Matrix4.translationValues(
            details.focalPoint.dx - state.handle.scaleOffset.dx,
            details.focalPoint.dy - state.handle.scaleOffset.dy,
            1
        ).multiplied(state.handle.recordMatrix4));
        break;
      case PaintType.paint || PaintType.setting || PaintType.clear:
        //绘制逻辑
        break;
    }
  }

这里主要提要两点

①、代码中的changeMatrix4(Matrix4 newMatrix4) 核心逻辑就是更新matrix矩阵的状态而已

②、multiplied(Matrix4 newMatrix4)方法 为当前矩阵matrix4乘法(复制)新矩阵到当前的矩阵中

diagonal3Values(矩阵缩放 x ,y,z) 和 rotationZ(旋转z) 和 translationValues(平移 x,y,z)

end

void onScaleEnd(ScaleEndDetails details){

//这里没什么好讲的,就是tempMatrix = matrix ,手势停止后缓存当前手势以保留矩阵状态
state.handle.saveRecordMatrix4();

}

补充

你或许也会有疑问,当画布应用了矩阵变化之后,整个画布会被矩阵影响,从而导致当前手势的点位信息会有偏差,而这种情况下怎么处理呢?

我们需要 获取矩阵偏移量后的相对点位 ,在获取相对点位之前,需要考虑到平移和旋转的逻辑计算,经过我的测试,先计算旋转后的偏移量再计算平移的偏移量会导致点位偏差,缩放而不影响。

所以正确点位计算思想为:先计算平移后的偏移量之后再获取旋转的偏移量,这样拿到的点位则是正确的

旋转值的获取和处理

可能你会问,ScaleUpdateDetails类的参数下不是有rotation获取旋转值吗?

但是它的旋转值偏移量是基于你手势中心点得到旋转值,这个值对于我们相对点位计算来说是有误的,我们应该获取矩阵自身的旋转值 这个值Matrix4中没有直接提供,但是提供了 Matrix3 Matrix4.getRotation()的方法,我们需要从这个方法中重新获取可用的矩阵角度值

获取Matrix3的旋转角度方法

  // 获取矩阵的旋转角度(以度数为单位)
  double getRotationAngle(vector.Matrix3 matrix) {
    // 提取旋转部分
    double sinTheta = matrix.entry(1, 0); // 获取旋转矩阵的 sin(θ) 值
    double cosTheta = matrix.entry(0, 0); // 获取旋转矩阵的 cos(θ) 值

    // 计算旋转角度(弧度)
    double rotationAngle = math.atan2(sinTheta, cosTheta);

    // 将弧度转换为度数
    double rotationDegrees = rotationAngle * (180 / math.pi);
    return rotationDegrees;
  }

除此之外,我们还需要根据点位信息和角度,转化为相对点位

首先先将当前矩阵的角度转化为弧度

    // 将角度转换为弧度
    var radians = angle * (math.pi / 180.0);

我们利用旋转矩阵公式

rotatedX=offset.dx×cos(angle)−offset.dy×sin(angle)

rotatedY=offset.dx×sin(angle)+offset.dy×cos(angle)

获取到相对矩阵偏移量

  Offset _rotateOffset(Offset offset, double angle) {
    // 将角度转换为弧度
    var radians = angle * (math.pi / 180.0);
    var rotatedX = offset.dx * math.cos(radians) - offset.dy * math.sin(radians);
    var rotatedY = offset.dx * math.sin(radians) + offset.dy * math.cos(radians);
    return Offset(rotatedX, rotatedY);
  }

实现获取相对点位

直接贴代码吧,一看就明白

getRealOffset(Offset offset){
    var matrix = matrix4.value;
    // 获取矩阵平移的x,y
    var translationX = matrix.getTranslation().x;
    var translationY = matrix.getTranslation().y;
    // 获取当前矩阵的缩放值
    double scale = matrix.getMaxScaleOnAxis();
    var matrix3 = matrix.getRotation();
    double rotate = getRotationAngle(matrix3);
    // 这里的Offset为,当前手势点位的Offset - 平移后的Offset
    var rotatedOffset = _rotateOffset(offset - Offset(translationX, translationY), -rotate);
    // 将偏移量向量旋转回原始坐标系
    print('平移参数: (${translationX},${translationY}),缩放参数: ${scale},旋转角度: ${rotate},旋转偏移量: ${rotatedOffset}');
    return rotatedOffset / scale;
  }

至此结束。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇