Path
本篇来学习在 PaintingContext、Canvas、SceneBuilder 中都会用到的 Path。Path 和 Canvas 一样,也是 Flutter engine 层到 Flutter framework 层的桥接,Path 提供的 API 的真正实现在 engine 层,而在 framework 层中我们可以像其它普通的 framework 层的类一样使用 Path 的 API。且 Path 也是完全同 Canvas 一样的套路,在 Path 的同名工厂构造函数中直接返回 _NativePath,我们以为自己使用的是 Flutter framework 中一个 Path 类,其实它已经跑到了 engine 层。
和学习 Canvas 时一样,我们也暂时不用纠结于 engine 是如何实现 Path 的一众 API 的,我们只要把目光聚焦在 Path API 提供了什么即可。看到 Path 我们大脑中估计会立即浮现出:这就是一个描述路径的类吧,估计也是像其它平台一样,提供直线、曲线、虚线以及闭合路径 等功能,也确实是这样的。在 Flutter 中 Path 最主要的作用就是当我们在 Canvas 中绘制自定义路径时,可以直接用 Path 进行描述,然后另外一个就是当我们对 Canvas 进行区域裁剪时,我们也可以使用 Path 参数来指定裁剪的区域。
Path 中的内容整体还是比较简单的,我们快速浏览即可。下面过一遍 Path 的源码,首先是看它的介绍文档。
Path 由多个子路径(sub-paths)和一个当前点(current point)组成。
子路径(sub-paths)由各种类型的段组成,如直线(lines)、弧线(arcs)或贝塞尔曲线(beziers)。子路径(sub-paths)可以是开放的也可以是闭合的,并且可以相互交叉。
闭合子路径(closed sub-paths)根据当前的 fillType,围绕着平面上的一个(可能是不连续的)区域。
当前点(current point)最初位于原点。在每次向子路径(sub-path)添加一个段的操作之后,当前点(current point)被更新到该段的末尾。
Path 可以使用 Canvas.drawPath 绘制在画布(canvases)上,也可以使用于 Canvas.clipPath 中创建裁剪区域(clip regions)。
下面看一下 Path 的源码。
Constructors
_NativePath 是一个针对于当前平台的 base class(base class _NativePath extends NativeFieldWrapperClass1 implements Path { // ... }),_NativePath 类实现了 Path 抽象类中的所有抽象函数,并且在 Path 的同名工厂构造函数中直接返回。
abstract class Path {
factory Path() = _NativePath;
// 创建另一个 Path 的副本。
// 拷贝这个副本快速且不需要额外内存,除非入参 source path 或此构造函数返回的 Path 被修改。
factory Path.from(Path source) {
// 创建一个 _NativePath 变量 clonedPath,然后直接调用 _clone 函数,
// 把入参 source 克隆到 clonedPath。
final _NativePath clonedPath = _NativePath._();
(source as _NativePath)._clone(clonedPath);
return clonedPath;
}
// ...
}
fillType
确定如何计算此 Path 的内部。默认值是:PathFillType.nonZero。
PathFillType get fillType;
set fillType(PathFillType value);
PathFillType 是一个枚举,用来指示 Path 的填充类型,下面看下此枚举都有哪些值:
PathFillType
确定如何计算 Path 内部的环绕规则。这个枚举被 Path.fillType 属性使用。
- Object -> Enum -> PathFillType
nonZero
内部由符号边交叉的 non-zero 和定义。对于给定点,如果从该点到无限远处画一条线,这条线穿过顺时针绕点的线的次数与逆时针绕点的线的次数不同,那么该点被认为在路径的内部。Nonzero-rule
evenOdd
内部是由奇数边交叉定义的。对于给定点,如果从该点到无限远处画出的一条线穿过一条奇数条线,则将该点视为在路径的内部。Even-odd_rule
我们可以直接看参考链接,体会下 PathFillType.nonZero 和 PathFillType.evenOdd 的区别。下面继续看 Path 的内容。
moveTo & relativeMoveTo & lineTo & relativeLineTo
relativeXXX 与无 relative 的函数基本都是成对出现的,相比于无 relative 的函数版本,relativeXXX 就是在目标点上添加一个指定偏移,其它都是一样的。
// 在给定坐标处开始一个新的子路径(sub-path)。
//(可以理解为设定(x, y)为当前点,后续便从这新当前点开始画路径。)
void moveTo(double x, double y);
// 从当前点开始,在给定的偏移量处开始一个新的子路径(sub-path)。
//(可以理解为更新当前点为:旧的当前点 + (dx, dy),后续便从这新当前点开始画路径。)
void relativeMoveTo(double dx, double dy);
// 向当前点添加一条以当前点为起点,以这入参指定点 (x, y) 为终点的线段。
void lineTo(double x, double y);
// 从当前点开始,添加一条直线段到距当前点给定偏移量的点。
// (可以理解为以当前点为起点,然后以 "当前点 + (dx, dy)" 为终点的线段。)
void relativeLineTo(double dx, double dy);
quadraticBezierTo & relativeQuadraticBezierTo
quadraticBezierTo:添加一个二次贝塞尔曲线段,从当前点曲线到给定点(x2,y2),使用控制点(x1,y1)。
void quadraticBezierTo(double x1, double y1, double x2, double y2);
relativeQuadraticBezierTo:添加一个二次贝塞尔曲线段,这个曲线从当前点到从当前点偏移 (x2, y2) 的点("当前点 + (x2, y2)")之间的路径,控制点位于从当前点偏移 (x1, y1) 的位置。
如果当前点是原点,则二者效果相同。
void relativeQuadraticBezierTo(double x1, double y1, double x2, double y2);
cubicTo & relativeCubicTo
cubicTo:添加一个三次贝塞尔曲线段,从当前点曲线到给定点 (x3, y3),使用控制点 (x1, y1) 和 (x2, y2)。
void cubicTo(double x1, double y1, double x2, double y2, double x3, double y3);
relativeCubicTo:添加一个三次贝塞尔曲线段,该曲线从当前点到距当前点偏移为 (x3, y3) 的点,使用从当前点偏移为 (x1,y1) 和 (x2,y2) 的控制点。
void relativeCubicTo(double x1, double y1, double x2, double y2, double x3, double y3);
conicTo & relativeConicTo
conicTo:添加一个贝塞尔曲线段,从当前点曲线到给定点 (x2, y2),使用控制点 (x1, y1) 和权重 w。如果权重大于 1,则曲线是一个双曲线;如果权重等于 1,则是一个抛物线;如果小于 1,则是一个椭圆。
void conicTo(double x1, double y1, double x2, double y2, double w);
relativeConicTo:添加一个贝塞尔曲线段,从当前点曲线到从当前点偏移为 (x2, y2) 的点,使用从当前点偏移为 (x1, y1) 的控制点和权重 w。如果权重大于 1,则曲线是一个双曲线;如果权重等于 1,则是一个抛物线;如果小于 1,则是一个椭圆。
void relativeConicTo(double x1, double y1, double x2, double y2, double w);
arcTo
如果 forceMoveTo 参数为 false,则添加一条直线段和一条弧段。
如果 forceMoveTo 参数为 true,则开始一个由弧段组成的新子路径。
无论哪种情况,弧段由沿着给定矩形边界的椭圆的弧组成,从 startAngle 弧度开始绕椭圆到 startAngle + sweepAngle 弧度结束,其中零弧度是通过矩形中心的水平线穿过椭圆右侧的点,正角度顺时针绕椭圆。
如果 forceMoveTo 为 false,则添加的线段从当前点开始,终止于弧的起点。
void arcTo(Rect rect, double startAngle, double sweepAngle, bool forceMoveTo);
arcToPoint & relativeArcToPoint
追加最多四个圆锥曲线,通过权重描述一个半径为 radius 且沿 rotation(以度为单位,顺时针)旋转的椭圆。
第一条曲线从路径中的上一个点开始,最后一个点以 arcEnd 结束。根据顺时针和 largeArc 确定的方向,这些曲线沿着路径前进,这样扫过的角度始终小于 360 度。
如果两个半径都为零或路径中的上一个点是 arcEnd,则追加一个简单的直线。如果两个半径都大于零但太小而无法描述一条弧线,则将这些半径缩放以适合路径中的最后一个点。
void arcToPoint(Offset arcEnd, {
Radius radius = Radius.zero,
double rotation = 0.0,
bool largeArc = false,
bool clockwise = true,
});
追加最多四个共轭曲线,它们被加权以描述一个半径为 radius 并按照 rotation(以角度表示,顺时针)旋转的椭圆。
最后的路径点由(px,py)描述。
第一条曲线从路径中的最后一个点开始,最后一条曲线以 arcEndDelta.dx + px 和 arcEndDelta.dy + py 结束。曲线沿着由 clockwise 和 largeArc 决定的方向进行,使得扫描角度始终小于 360 度。
如果半径为零,或者 arcEndDelta.dx 和 arcEndDelta.dy 都为零,则追加一条简单的直线。如果两者均大于零但太小以描述一条弧时,则将半径缩放以适应路径中的最后一个点。
void relativeArcToPoint(
Offset arcEndDelta, {
Radius radius = Radius.zero,
double rotation = 0.0,
bool largeArc = false,
bool clockwise = true,
});
addRect & addOval
addRect:添加一个新的子路径(sub-path),由四条线组成,勾勒出给定的矩形。
addOval:添加一个新的子路径(sub-path),该子路径(sub-path)由形成填充给定矩形的椭圆的曲线组成。要添加一个圆,可以将一个适当的矩形作为椭圆。可以使用 Rect.fromCircle 来轻松描述圆的中心偏移和半径
void addRect(Rect rect);
void addOval(Rect oval);
addArc
添加一个新的子路径(sub-path),其中包含一个弧段,该弧段由限定的矩形边界内椭圆的边缘形成,从弧度 startAngle 开始围绕椭圆到弧度 startAngle + sweepAngle 结束,其中 0 弧度是横穿矩形中心的水平线的椭圆右侧点,正角度顺时针围绕椭圆。
void addArc(Rect oval, double startAngle, double sweepAngle);
addPolygon
添加一个新的子路径(sub-path),该子路径(sub-path)由连接给定点的线段序列组成。
&emsp如果 close 参数为 true,则会添加一个最终线段,连接最后一个点和第一个点。
points 参数被解释为相对于原点的偏移量。
void addPolygon(List<Offset> points, bool close);
addRRect
添加一个新的子路径(sub-path),该子路径(sub-path)由组成所描述的圆角矩形所需的直线和曲线组成。
void addRRect(RRect rrect);
addPath
将 Path 的子路径(sub-paths)按指定的偏移量添加到此 Path 中。(可以理解为复制一遍已经绘制的路径,然后整体根据 offset 进行偏移。)
如果指定了 matrix4,那么在通过给定的偏移量平移矩阵后,Path 将通过该矩阵进行变换。该矩阵是按列主序存储的 4x4 矩阵。
void addPath(Path path, Offset offset, {Float64List? matrix4});
extendWithPath
将 Path 的子路径(sub-paths)按照偏移量 offset 添加到此 Path 中。当前子路径(sub-paths)将与 Path 的第一个子路径(sub-paths)延长,并在必要时用 lineTo 连接它们。(与上面的 addPath 类似,只是最后可以会把新增的 Path 已旧的 Path 连接起来。)
如果指定了 matrix4,那么在矩阵被偏移量平移之后,Path 将按照此矩阵进行变换。该矩阵是按列主序存储的 4x4 矩阵。
void extendWithPath(Path path, Offset offset, {Float64List? matrix4});
close & reset
close:关闭最后一个子路径(sub-paths),就好像从当前点(current point)到该子路径的第一个点(Path 的起点)画了一条直线一样。(即把最后一个点和第一个点直接用一条直线连接起来。)
reset:清除 Path 对象中的所有子路径(sub-paths),将其恢复为创建时的初始状态。当前点(current point)将被重置为原点。
void close();
void reset();
contains & shift & transform
contains:测试给定点(Offset point)是否在 Path 内部。(也就是说,如果该 Path 被用于 Canvas.clipPath,该点是否在 Path 的可见部分内,还是说被裁剪掉了。) Offset point 参数被解释为相对于原点的偏移量。如果 point 在 Path 内部则返回 true,否则返回 false。
shift:返回 Path 的副本(一个新的 Path,和之前的函数那种在既有的 Path 添加新的子路径(sub-paths)是不同的。),其中每个子路径的所有线段都按给定的偏移量(Offset offset)平移。
transform:使用给定的矩阵转换所有子路径(sub-paths)的所有线段,返回 Path 的副本。
bool contains(Offset point);
Path shift(Offset offset);
Path transform(Float64List matrix4);
getBounds
计算此 Path 的边界矩形。(可以理解为在 Path 外面画一个矩形,然后刚好把 Path 包围在里面。)
包含仅具有相同直线上的轴对齐点的 Path 将没有 area,因此对于这样的路径,Rect.isEmpty 将返回true。考虑改为检查 rect.width + rect.height > 0.0,或者使用 computeMetrics API 来检查 Path 长度。
对于许多更复杂的路径,bounds 可能会不准确。例如,当 Path 包含一个圆时,用于计算边界的点是圆的隐含控制点,它们形成一个围绕圆的正方形;如果应用了 transform 对圆进行变换,则该正方形会旋转,因此(轴对齐、非旋转的)边界框最终会严重高估圆覆盖的实际 area。
Rect getBounds();
下面是 Path 中最后一部分内容,两个比较不好理解的静态函数,我们一起来看一下。
combine
静态函数:根据给定的操作方式(PathOperation operation)将这两条 Path 合并。
得到的 Path 将由互不重叠的轮廓构成。在可能的情况下,曲线顺序会降低,使得立方曲线可以变为二次曲线,并且二次曲线可以变为直线
static Path combine(PathOperation operation, Path path1, Path path2) {
final _NativePath path = _NativePath();
if (path._op(path1 as _NativePath, path2 as _NativePath, operation.index)) {
return path;
}
throw StateError('Path.combine() failed. This may be due an invalid path; in particular, check for NaN values.');
}
Ok,API 看完了,有哪些不理解的,或者不熟悉的可套用下面的示例代码操作一番:
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(const MyCustomPainterApp());
}
class MyCustomPainterApp extends StatelessWidget {
const MyCustomPainterApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('CustomPainter Example'),
),
body: Center(
child: CustomPaint(
size: MediaQuery.of(context).size,
painter: MyPainter(),
),
),
),
);
}
}
class MyPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final Path path = Path();
path.moveTo(50, 50);
path.lineTo(150, 150);
// path.relativeLineTo(100, 100);
final Paint paint = Paint();
paint.style = PaintingStyle.stroke;
paint.strokeWidth = 5;
paint.color = Colors.redAccent;
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
Path 总结
OK,Path 的内容到这里就看完了,首先是 Path 是一个抽象类,然后它内部的所有函数都是抽象函数,甚至于它的同名构造函数都是返回一个 NativePath(factory Path() = _NativePath;),至于 NativePath 是谁?它定义是这样的:base class _NativePath extends NativeFieldWrapperClass1 implements Path { // ...},可以看到它是完全实现了 Path 抽象函数的一个继承自 NativeFieldWrapperClass1 的类,它的内容主要通过 Flutter engine 在 native 层实现。那里面牵涉的内容比较多,我们后续再学习。目前的话只要我们理解 Path 的功能以及它的一些常见函数,然后后面学习 RenderObject 的绘制时,看到 Path 参数不晕即可。
Path 相关的 API 都比较好理解,快速浏览即可。
- moveTo: 移动 Path 起始点的位置到指定的坐标点。
- relativeMoveTo: 相对移动 Path 起始点的位置到指定的坐标点。
- lineTo: 连接当前 Path 结束点和指定坐标点,绘制直线段。
- relativeLineTo: 相对于当前 Path 结束点的位置,绘制直线段。
- quadraticBezierTo: 绘制二阶贝塞尔曲线。
- relativeQuadraticBezierTo: 绘制相对位置的二阶贝塞尔曲线。
- cubicTo: 绘制三阶贝塞尔曲线。
- relativeCubicTo: 绘制相对位置的三阶贝塞尔曲线。
- conicTo: 绘制圆锥曲线,需要指定 weight 参数。
- arcTo: 通过指定矩形框和起始角度、扫描角度,绘制弧线。
- arcToPoint: 通过指定矩形框的某一点,绘制连接当前点和目标点的弧线。
- addRect: 向 Path 添加一个矩形。
- addOval: 向 Path 添加一个椭圆形。
- addArc: 向 Path 添加一个圆弧。
- addPolygon: 向 Path 添加多边形。
参考链接
参考链接:🔗