4、路径Path:Path的基本操作、直线路径、曲线路径、弧线路径、路径组合
说到自定义绘制,Path 绝对是个绕不开的核心角色。我刚开始学 Flutter 绘制时,总觉得 Canvas 提供的 drawLine、drawCircle 这些方法够用了,直到有一次需要画一个不规则的气泡对话框——嗯,那时候我才意识到,没有 Path 简直寸步难行。
Path 说白了就是一条「可编程的轨迹」。你可以告诉它:从这里开始,走到那里,拐个弯,画个弧,再连回来。它不像那些现成的绘制方法那么死板,你几乎可以画出任何形状。
4.1 Path 的基本操作
先看最基础的操作。Path 就像一张白纸上的铅笔,你控制它移动、落笔、画线。
// 创建一个空的路径
Path path = Path();
// 移动起点(不画线)
path.moveTo(100, 100);
// 画一条直线到指定点
path.lineTo(200, 100);
path.lineTo(200, 200);
// 闭合路径(回到起点)
path.close();
// 在 Canvas 上绘制
canvas.drawPath(path, paint);
这里有个细节我特别想强调:moveTo 只是把笔抬起来挪到新位置,不会留下痕迹。而 lineTo 才是真正画线。我见过不少新手把这两个搞混,结果画出来的图形乱七八糟。
核心方法速查表
| 方法 | 作用 |
|---|---|
| moveTo(x, y) | 移动起点,不绘制 |
| lineTo(x, y) | 从当前点画直线到目标点 |
| close() | 闭合路径,连接终点和起点 |
| reset() | 清空路径 |
4.2 直线路径
直线路径是最简单的,但组合起来能玩出花来。比如画一个多边形:
Path polygonPath = Path();
polygonPath.moveTo(100, 100);
polygonPath.lineTo(200, 50);
polygonPath.lineTo(300, 100);
polygonPath.lineTo(250, 200);
polygonPath.lineTo(150, 200);
polygonPath.close(); // 自动连回起点
我个人习惯在画闭合图形时,最后一定调用 close()。为什么?因为 close() 不只是画一条线,它还会让路径的终点和起点完美连接,填充时不会有缝隙。我曾经在画一个仪表盘背景时忘了调用 close,结果填充颜色漏出去一块,排查了半天才发现。
小技巧:如果你只是画折线而不需要闭合,可以用 addPolygon 方法,第二个参数传 false 就行。
4.3 曲线路径
曲线才是 Path 真正展现魅力的地方。Flutter 提供了两种曲线:二次贝塞尔和三次贝塞尔。
4.3.1 二次贝塞尔曲线
二次贝塞尔需要一个控制点和终点。控制点决定了曲线的弯曲方向。
Path path = Path();
path.moveTo(50, 200);
// 控制点 (150, 50),终点 (250, 200)
path.quadraticBezierTo(150, 50, 250, 200);
canvas.drawPath(path, paint);
你想想看,控制点就像一块磁铁,把直线吸弯了。控制点越远,弯曲幅度越大。我在做聊天气泡的尖角时,就用二次贝塞尔来模拟那个圆润的过渡,效果比硬切角好太多了。
4.3.2 三次贝塞尔曲线
三次贝塞尔有两个控制点,能画出更复杂的 S 形曲线。
Path path = Path();
path.moveTo(50, 200);
// 控制点1 (100, 50),控制点2 (200, 50),终点 (250, 200)
path.cubicTo(100, 50, 200, 50, 250, 200);
canvas.drawPath(path, paint);
说实话,三次贝塞尔刚开始不太好掌握。我建议你先在纸上画个坐标,标出起点、终点和两个控制点的位置,再想象曲线的走向。做多了就有手感了。
注意:贝塞尔曲线的控制点不一定在曲线上!很多新手以为控制点就是曲线经过的点,其实不是。控制点只是「牵引」曲线,曲线本身并不穿过它。
4.4 弧线路径
弧线就是圆的一部分。Flutter 用 arcTo 来画弧,参数稍微有点绕。
Path path = Path();
Rect rect = Rect.fromCircle(center: Offset(150, 150), radius: 100);
// 从 0 度开始,扫过 90 度
path.arcTo(rect, 0, 3.14159 / 2, true);
canvas.drawPath(path, paint);
参数解释一下:
- rect:弧所在的虚拟圆的外接矩形
- startAngle:起始角度(弧度制),0 是三点钟方向
- sweepAngle:扫过的角度,正数顺时针,负数逆时针
- forceMoveTo:是否强制移动起点
这里有个坑:角度单位是弧度,不是角度。我刚开始总忘记转换,画出来的弧完全不对。记住 π = 180°,用 pi / 180 * 度数 来换算。
常用弧线角度速记:
- 90° = π/2 ≈ 1.57
- 180° = π ≈ 3.14
- 270° = 3π/2 ≈ 4.71
- 360° = 2π ≈ 6.28
4.5 路径组合
单个路径能画的有限,但把多个路径组合起来,威力就大了。Flutter 提供了 Path.combine 方法,支持四种操作。
Path path1 = Path()..addOval(Rect.fromCircle(center: Offset(100, 100), radius: 50));
Path path2 = Path()..addOval(Rect.fromCircle(center: Offset(130, 100), radius: 50));
// 取并集
Path unionPath = Path.combine(PathOperation.union, path1, path2);
// 取交集
Path intersectPath = Path.combine(PathOperation.intersect, path1, path2);
// 取差集(path1 减去 path2)
Path differencePath = Path.combine(PathOperation.difference, path1, path2);
// 取异或(重叠部分去掉)
Path xorPath = Path.combine(PathOperation.xor, path1, path2);
四种操作的效果:
| 操作 | 效果 | 应用场景 |
|---|---|---|
| union | 合并两个路径 | 画组合图形 |
| intersect | 只保留重叠部分 | 裁剪、遮罩 |
| difference | 减去重叠部分 | 挖空、镂空效果 |
| xor | 去掉重叠,保留不重叠 | 特殊图形运算 |
我记得有一次做加载动画,需要两个圆环交叉旋转,中间重叠的部分要显示不同的颜色。用 Path.combine 的 intersect 操作,几行代码就搞定了。要是用数学计算交点,那得写到猴年马月去。
个人经验:路径组合的性能开销不小,如果只是简单的图形叠加,用 canvas.drawPath 画两次可能更高效。路径组合适合那些必须精确计算交集的场景。
好了,Path 的基本操作就这些。直线、曲线、弧线、组合,这四个维度基本覆盖了日常开发中 90% 的绘制需求。下一节我们会把这些知识串起来,做一个完整的自定义组件。到时候你会发现,原来那些看起来很炫酷的 UI 效果,底层不过就是这些基础操作的排列组合罢了。