本文来探讨一下路径的变换,我们知道 Canvas
本身也支持变换,那 Path
的变换有什么必要性吗?和 Canvas
变换又有什么区别呢?如何在一次变换中叠加多种变换效果,如何修改变换中心?这些都是绘制的基本技能。本文将作为 《Flutter 绘制指南 - 妙笔生花》的补充内容,被同步到小册中。本文源码见 【idraw/extra_03_path】
1. 绘制路径测试
如下,通过 PathPainter
作为画板,绘制如下图案:左上角是一个三角形路径。坐标系以画布中心为原点,右
和下
方为正方向,只起到辅助查看作用。通过之前封装的 Coordinate
类进行绘制,详见 coordinate_pro.dart
。
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
进行变换,将画板的左上角平移到中心,如下浅蓝色区域:
---->[extra_03_path/01]----
canvas.translate(size.width/2, size.height/2,);
如下,不改变画布,通过对 Path
处理,也可以完成同样的显示。 此时画布的原点仍在屏幕左上角,如下浅蓝色区域:
---->[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
只是在绘制时进行了视觉上的偏移,它本身还在红色虚线所示的区域。这样的话,如果路径需要校验触点,就需要额外的运算处理。可能有人会说,不就是加加减减,简单计算一下吗,也不麻烦。但这里只是平移,如果是缩放、旋转、斜切等变换,你还算得过来吗?
---->[extra_03_path/03]----
print(path.contains(Offset(30,10))); //true
3. 路径变换
其实前面只是通过对路径进行移动处理而已,并没有真正用到变换。但有些场景通过计算会非常麻烦,这时路径的变换就会非常实用。比如需要旋转 10°
,如下通过 Matrix4
进行变换,rotationZ
表示沿 Z
轴旋转,也就是说在 XOY
水平面上旋转。可以看出,默认情况下,是以屏幕左上角为变换中心的。
---->[extra_03_path/05]----
Matrix4 m4 = Matrix4.rotationZ(10*pi/180);
path = path.transform(m4.storage);
那如何指定某点为变换中心呢?在一次变换中,通过平移,可以改变变换中心。比如下面左上角的红色虚线路径,通过 平移变换
,形成如下黑线路径。
---->[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
的值。
---->[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
矩阵,平移会取即可。
---->[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】
---->[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#multiply
和 Matrix4#multiplied
两个方法的区别。从源码中可以看出 multiplied
本质上是通过 multiply
实现功能的,只不过它会克隆对象,对新对象进行 multiply
操作。也就是说这个方法会返回一个新的 Matrix4
对象,不会影响调用者的内部数据。
而 multiply
方法,如下所示:是根据矩阵的乘法,来修改自身的数据。
所以如果调用者需要在后续被使用,可以通过 Matrix4#multiplied
返回个新的。如果不需要被使用,通过 Matrix4#multiply
方法直接修改自身数据即可。了解其原理之后,就不会用起来稀里糊涂的。
那本文就到这里,谢谢观看 ~