【Flutter 绘制技巧】Path 路径变换

2022-09-20 10:14:25 浏览数 (1)

本文来探讨一下路径的变换,我们知道 Canvas 本身也支持变换,那 Path 的变换有什么必要性吗?和 Canvas 变换又有什么区别呢?如何在一次变换中叠加多种变换效果,如何修改变换中心?这些都是绘制的基本技能。本文将作为 《Flutter 绘制指南 - 妙笔生花》的补充内容,被同步到小册中。本文源码见 【idraw/extra_03_path】


1. 绘制路径测试

如下,通过 PathPainter 作为画板,绘制如下图案:左上角是一个三角形路径。坐标系以画布中心为原点,方为正方向,只起到辅助查看作用。通过之前封装的 Coordinate 类进行绘制,详见 coordinate_pro.dart

代码语言:javascript复制
void main() {
  runApp(CustomPaint(
    painter: PathPainter(),
  ));
}

可以看出默认情况下,以画布的左上角为原点。

代码语言:javascript复制
class PathPainter extends CustomPainter {
  
  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()..style = PaintingStyle.stroke;
    Path path = Path()
      ..lineTo(40, 40)
      ..relativeLineTo(0, -40)
      ..close();
    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(covariant PathPainter oldDelegate) => true;
}

2.画板的变换和路径的变换

现在,如果想让这个三角形绘制时以 画布中心 为原点,实现这个需求的方式有很多。总得来说有两个方向,一者是对 Canvas 进行处理,一者是对 Path 进行处理。

如下是对 canvas 进行变换,将画板的左上角平移到中心,如下浅蓝色区域:

代码语言:javascript复制
---->[extra_03_path/01]----
canvas.translate(size.width/2, size.height/2,);

如下,不改变画布,通过对 Path 处理,也可以完成同样的显示。 此时画布的原点仍在屏幕左上角,如下浅蓝色区域:

代码语言:javascript复制
---->[extra_03_path/02]----
Path path = Path()
  ..moveTo(size.width/2, size.height/2,)
  ..relativeLineTo(40, 40)
  ..relativeLineTo(0, -40)
  ..close();

优劣党开始发问,那这两种方式有什么区别,用哪种更好呢。还是那句话,抛开场景谈优劣,就是纸上谈兵,两者各有各的好处,各有各的不足。如果对 canvas 进行变换,那么接下来的所有绘制都会在该变换的基础上;如果是对 Path 进行处理,不会影响 canvas

另外有个非常重要的注意点,如果是对 Path 进行处理,它的真实位置是发生变化的,对 canvas 进行变换,Path 的真实位置不变。Path 中有个 contains 方法,用于校验点是否在路径内。比如下面的红点是 30,10 ,通过 canvas 平移实现的。

此时通过输出可以看出 30,10 点仍在 path 路径下,这就说明 path 只是在绘制时进行了视觉上的偏移,它本身还在红色虚线所示的区域。这样的话,如果路径需要校验触点,就需要额外的运算处理。可能有人会说,不就是加加减减,简单计算一下吗,也不麻烦。但这里只是平移,如果是缩放、旋转、斜切等变换,你还算得过来吗?

代码语言:javascript复制
---->[extra_03_path/03]----
print(path.contains(Offset(30,10))); //true

3. 路径变换

其实前面只是通过对路径进行移动处理而已,并没有真正用到变换。但有些场景通过计算会非常麻烦,这时路径的变换就会非常实用。比如需要旋转 10° ,如下通过 Matrix4 进行变换,rotationZ 表示沿 Z 轴旋转,也就是说在 XOY 水平面上旋转。可以看出,默认情况下,是以屏幕左上角为变换中心的。

代码语言:javascript复制
---->[extra_03_path/05]----
Matrix4 m4 = Matrix4.rotationZ(10*pi/180);
path = path.transform(m4.storage);

 那如何指定某点为变换中心呢?在一次变换中,通过平移,可以改变变换中心。比如下面左上角的红色虚线路径,通过 平移变换 ,形成如下黑线路径。

代码语言:javascript复制
---->[extra_03_path/06]----
Path path = Path()
  ..moveTo(0, 0)
  ..relativeLineTo(40, 40)
  ..relativeLineTo(0, -40)
  ..close();

Matrix4 m4 = Matrix4.translationValues(size.width/2, size.height/2, 0);
path = path.transform(m4.storage);

这时只要在 m4 的基础上 叠加 旋转变换,这样对于 旋转变换 来说,变换中心就是上面红点所示,如下图所示。变换效果的叠加,本质上就是两个 4*4 矩阵的乘法,通过 multiply 方法实现。注意这个方法无返回值,会改变 m4 的值。

代码语言:javascript复制
---->[extra_03_path/07]----
Matrix4 m4 = Matrix4.translationValues(size.width/2, size.height/2, 0);
Matrix4 rotateM4 = Matrix4.rotationZ(10*pi/180);
m4.multiply(rotateM4);
path = path.transform(m4.storage);

在一次变换中,我们可以叠加多个变换,比如下面在旋转的基础上,再叠加缩放变换。这个变换中心依然是红点,也就是说,在一次变换中,通过平移变换可以用来修改变中心。

代码语言:javascript复制
---->[extra_03_path/08]----
Matrix4 m4 = Matrix4.translationValues(size.width/2, size.height/2, 0);
Matrix4 rotateM4 = Matrix4.rotationZ(10*pi/180);
Matrix4 scaleM4 = Matrix4.diagonal3Values(2,2,1);
m4.multiply(rotateM4);
m4.multiply(scaleM4);
path = path.transform(m4.storage);

那接下来思考一个问题,如何以任意点为变换中心呢,比如以 20,20 点为变换中心,进行旋转和缩放操作。实现思路也非常简单,定义两个偏移的矩阵,在旋转和缩放前,先叠加 center ,让变换中心变为 20,20 。在最后为了不影响结果,在通过 back 矩阵,平移会取即可。

代码语言:javascript复制
---->[extra_03_path/09]----
Matrix4 m4 = Matrix4.translationValues(size.width/2, size.height/2, 0);
Matrix4 center = Matrix4.translationValues(20, 20, 0);
Matrix4 back = Matrix4.translationValues(-20, -20, 0);

Matrix4 rotateM4 = Matrix4.rotationZ(10*pi/180);
Matrix4 scaleM4 = Matrix4.diagonal3Values(2,2,1);

m4.multiply(center);// tag1
m4.multiply(rotateM4);
m4.multiply(scaleM4);
m4.multiply(back); // tag2
path = path.transform(m4.storage);
canvas.drawPath(path, paint);

4. 路径变换与命中

路径的变换操作是对 路径 本身的真实操作,通过 contains 方法,判断点是否在路径之内。这个点是相对于组件左上角的,也就是说通过手势事件,可以很方便地校验触点是否在路径之内。比如下面的效果,当在区域内时,路径加粗且为橙色,实现代码详见 : 【extra_03_path/10】

代码语言:javascript复制
---->[extra_03_path/10]----
bool contains = path.contains(pos.value);
double strokeWidth = contains ? 2 : 1;
Color color = contains ? Colors.orange : Colors.black;
canvas.drawPath(path, paint..strokeWidth = strokeWidth..color = color);

通过不对路径变换,而是通过 canvas 变换,那么在校验时,就需要进行复杂的点位计算。这就是两者之间最大的区别,另外 canvas 变换本质上也是通过 Matrix4 实现的,上面所说的叠加特性对 canvas 也使用。


最后简单说一下 Matrix4#multiplyMatrix4#multiplied 两个方法的区别。从源码中可以看出 multiplied 本质上是通过 multiply 实现功能的,只不过它会克隆对象,对新对象进行 multiply 操作。也就是说这个方法会返回一个新的 Matrix4 对象,不会影响调用者的内部数据。

multiply 方法,如下所示:是根据矩阵的乘法,来修改自身的数据。

所以如果调用者需要在后续被使用,可以通过 Matrix4#multiplied 返回个新的。如果不需要被使用,通过 Matrix4#multiply 方法直接修改自身数据即可。了解其原理之后,就不会用起来稀里糊涂的。 那本文就到这里,谢谢观看 ~

0 人点赞