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.combineintersect 操作,几行代码就搞定了。要是用数学计算交点,那得写到猴年马月去。

个人经验:路径组合的性能开销不小,如果只是简单的图形叠加,用 canvas.drawPath 画两次可能更高效。路径组合适合那些必须精确计算交集的场景。

好了,Path 的基本操作就这些。直线、曲线、弧线、组合,这四个维度基本覆盖了日常开发中 90% 的绘制需求。下一节我们会把这些知识串起来,做一个完整的自定义组件。到时候你会发现,原来那些看起来很炫酷的 UI 效果,底层不过就是这些基础操作的排列组合罢了。