自定义View这块,Path几乎是绕不开的一环。很多刚接触自定义绘制的朋友跟我聊,说Paint的API翻一遍就懂了,颜色、粗细、样式就那么几个参数,但一到Path就卡壳——moveTo、lineTo还好理解,画直线嘛,可quadTo、cubicTo、arcTo这几个一上来就懵了,参数一堆,画出来的东西跟脑子里想的完全对不上。这篇内容我就把Path里这五个最核心的绘制方法掰开揉碎讲一遍,从底层原理到参数计算,再到实际能跑起来的完整实例,全部过一遍。
这篇适合谁看?如果你已经能写一个最简单的自定义View,知道onDraw里拿Canvas画点东西,但对Path的曲线绘制总是靠试参数碰运气,那这篇就是写给你的。如果你连Canvas和Paint都还没用过,建议先把基础过一遍再回来,不然曲线部分会有点吃力。整篇内容我会用大量实际参数和可复现的代码来说明,每个方法都配一个能直接跑的例子,你照着敲一遍基本就能掌握。
1. 先搞清楚Path到底在画什么
1.1 Path的本质:一条可以随时追加的路径记录
很多人把Path想得太神秘,其实它就是一个“路径记录器”。你可以把它理解成一张透明纸上的铅笔轨迹——你告诉它从哪开始、往哪走、怎么拐弯,它就把这些指令按顺序记下来,最后Canvas拿着Paint沿着这条轨迹描边或者填充。Path本身不负责显示,它只负责记录几何形状,真正画出来是Canvas的事。
这个认知很关键,因为它决定了你调试Path时的思路。当你发现画出来的东西不对,问题一定出在“轨迹记录”这个环节,而不是Paint。我见过不少新手,图形画歪了就去调Paint的strokeWidth,完全找错了方向。Path记录的是几何信息,Paint决定的是视觉表现,两者职责分明。
Path内部维护的是一个点序列加上连接指令。moveTo是“抬笔移动到某点”,lineTo是“落笔画直线到某点”,quadTo和cubicTo是“用贝塞尔曲线弯到某点”,arcTo是“沿圆弧走到某点”。每调用一次,路径就多一段。这些段首尾相接,最终形成完整图形。理解了这个“追加”模型,后面所有方法的参数含义就都好推了。
1.2 五个方法的分工与选择逻辑
这五个方法不是随便列的,它们覆盖了从直线到曲线到圆弧的全部基础绘制能力。我整理了一张对照表,先把全局印象建立起来:
| 方法 | 绘制类型 | 关键参数 | 典型场景 |
|---|---|---|---|
| moveTo | 移动起点 | x, y | 分段绘制、抬笔换位置 |
| lineTo | 直线 | x, y | 多边形、折线图、网格 |
| quadTo | 二次贝塞尔 | 控制点x,y + 终点x,y | 简单弧线、波浪、圆角近似 |
| cubicTo | 三次贝塞尔 | 两个控制点 + 终点 | 复杂曲线、S形、精确造型 |
| arcTo | 圆弧 | 矩形范围 + 起止角 + 是否连线 | 圆环进度、扇形、弧形边框 |
选择逻辑其实很直接:能用直线就别用曲线,能用二次就别用三次,能用三次就别硬算圆弧。直线计算量最小,圆弧有专门的数学公式,贝塞尔曲线灵活但控制点需要调。实际项目里,折线图用lineTo,聊天气泡的尾巴用quadTo,手势轨迹平滑用cubicTo,环形进度条用arcTo,各司其职。
注意:arcTo有两个重载,一个带forceMoveTo参数,一个不带。这个参数决定了圆弧起点和当前路径点之间要不要连一条直线,用错了图形会多出一条莫名其妙的线,后面会详细讲。
2. moveTo与lineTo:一切路径的起点
2.1 moveTo不是“画点”,而是“定位笔尖”
初学者最容易误解的就是moveTo。看名字以为是“移动到并画点什么”,实际上它只做一件事:把当前路径点设置到指定坐标,不产生任何可见图形。你可以把它想成拿笔的手抬起来,移到纸上的某个位置准备落笔,但还没画。
Path path = new Path(); path.moveTo(100, 100); // 笔尖移到(100,100),此时什么都没画 path.lineTo(300, 100); // 从(100,100)画直线到(300,100)上面这段代码,最终画出来的只有一条从(100,100)到(300,100)的横线,moveTo那个点本身是不可见的。如果你只调用moveTo不调用任何lineTo或曲线方法,Canvas上什么都不会出现。这一点在调试时特别有用——如果你发现某段路径没画出来,先检查是不是只有moveTo没有后续绘制指令。
moveTo还有一个重要作用是“断开路径”。连续调用lineTo会形成一条折线,但如果你在中间插入一个moveTo,路径就会在这里断开,形成两个独立的子路径。填充时这个区别很明显:断开的子路径会被当作独立区域分别填充,而连续的折线是一个整体。
2.2 lineTo的连续追加与闭合技巧
lineTo就是最直白的“从当前点画直线到目标点”。它的参数只有终点坐标,起点自动取路径的当前点。连续调用会形成折线:
Path path = new Path(); path.moveTo(100, 100); path.lineTo(200, 100); path.lineTo(200, 200); path.lineTo(100, 200); // 此时是一个开口的“匚”形,缺一条边 path.close(); // 自动从(100,200)连回起点(100,100),形成闭合矩形这里要重点说close()。它不是一个独立方法,而是Path的闭合指令,作用是从当前点画一条直线回到该子路径的起始点。画多边形时几乎都要用close(),否则填充会出现缺口或者描边少一条边。但要注意,close()之后如果继续lineTo,新的线段会从闭合点继续,而不是从原来的当前点,这个细节在复杂图形里容易踩坑。
我实际做折线图时有个经验:数据点很多的时候,不要每个点都new一个Path,而是复用一个Path对象,先reset()再重新moveTo和lineTo。频繁创建Path对象在onDraw里会带来GC压力,滑动时容易掉帧。reset()会清空所有路径记录但保留对象本身,比重新new划算得多。
2.3 一个完整的网格绘制实例
光说理论没意思,来个能直接跑的。下面这段代码在自定义View里画一个10x10的网格,用moveTo和lineTo交替实现:
public class GridView extends View { private Paint mPaint; private Path mPath; private int mCellSize = 60; public GridView(Context context) { super(context); init(); } private void init() { mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(2f); mPaint.setColor(0xFF333333); mPath = new Path(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mPath.reset(); int cols = 10, rows = 10; // 先画所有竖线 for (int i = 0; i <= cols; i++) { float x = i * mCellSize; mPath.moveTo(x, 0); mPath.lineTo(x, rows * mCellSize); } // 再画所有横线 for (int j = 0; j <= rows; j++) { float y = j * mCellSize; mPath.moveTo(0, y); mPath.lineTo(cols * mCellSize, y); } canvas.drawPath(mPath, mPaint); } }这段代码的关键在于:每画一条线之前都先moveTo到起点,这样每条线都是独立的子路径,不会互相连接。如果你偷懒省掉moveTo,直接连续lineTo,画出来的会是一条来回穿梭的锯齿线,完全不是网格。这个坑我当年踩过,盯着屏幕看了半天才反应过来是moveTo漏了。
实操心得:网格类图形用“先竖后横”的循环结构,每个方向内部用moveTo+lineTo成对出现,逻辑最清晰。如果线条特别多(比如50x50以上),可以考虑用drawLines批量绘制,性能比drawPath更好,因为drawLines底层走的是更直接的绘制通道。
3. quadTo:二次贝塞尔曲线的入门与实战
3.1 二次贝塞尔的几何直觉
quadTo是二次贝塞尔曲线,参数是控制点(cx, cy)和终点(x, y),起点是路径当前点。它画出来的曲线形状由三个点决定:起点、控制点、终点。控制点像一个“磁铁”,把曲线往自己方向拉,但曲线本身不经过控制点(除非三点共线)。
用生活化的说法:你从起点出发要去终点,控制点是你中途想“绕一下”的那个方向。控制点离起点终点连线越远,曲线弯曲程度越大;控制点越靠近连线,曲线越平直。这个直觉对调参数极其重要——很多人调quadTo就是瞎试,其实只要理解了“控制点是拉力方向”,就能有目的地调整。
数学上,二次贝塞尔的参数方程是:
B(t) = (1-t)²·P0 + 2t(1-t)·P1 + t²·P2, t ∈ [0,1]其中P0是起点,P1是控制点,P2是终点。t从0到1变化,曲线上的点就依次生成。这个公式说明曲线是三个点的加权平均,权重随t变化。你不需要手算这个公式,但知道它的存在能帮你理解为什么控制点的影响是“渐进”的——靠近起点时P0权重大,靠近终点时P2权重大,中间P1影响最大。
3.2 用quadTo画一条自然的波浪线
波浪线是quadTo最典型的应用。思路是:把一段水平距离分成若干份,每份用一段二次贝塞尔,控制点在垂直方向上交替上下偏移,形成波峰波谷。
private void drawWave(Canvas canvas, Paint paint) { Path path = new Path(); float startX = 50, startY = 300; float waveWidth = 100; // 单个波长 float waveHeight = 80; // 波峰到波谷的垂直距离 int waveCount = 5; path.moveTo(startX, startY); for (int i = 0; i < waveCount; i++) { float controlX = startX + waveWidth * i + waveWidth / 2f; float controlY = (i % 2 == 0) ? startY - waveHeight : startY + waveHeight; float endX = startX + waveWidth * (i + 1); float endY = startY; path.quadTo(controlX, controlY, endX, endY); } canvas.drawPath(path, paint); }这段代码里,每个波段的控制点x坐标取波段中点,y坐标交替取startY减waveHeight和加waveHeight。这样曲线就会在水平推进的同时上下摆动,形成正弦波的效果。waveHeight控制振幅,waveWidth控制波长,两个参数独立可调,非常直观。
实测下来,waveHeight取waveWidth的0.6到0.8倍时,波形看起来最自然,太大会显得尖锐,太小又太平缓。这个比例不是死规定,但可以作为调参的起点。另外,波峰波谷的切换用i % 2判断,简单有效,比维护一个布尔变量翻转更简洁。
3.3 quadTo画圆角矩形的近似方案
圆角矩形标准做法是用addRoundRect,但如果你想理解圆角背后的曲线逻辑,用quadTo手动实现一遍很有价值。思路是:四条直边用lineTo,四个角用quadTo,控制点取在角的顶点位置。
private void drawRoundRectByQuad(Canvas canvas, Paint paint, float left, float top, float right, float bottom, float radius) { Path path = new Path(); path.moveTo(left + radius, top); path.lineTo(right - radius, top); path.quadTo(right, top, right, top + radius); // 右上角 path.lineTo(right, bottom - radius); path.quadTo(right, bottom, right - radius, bottom); // 右下角 path.lineTo(left + radius, bottom); path.quadTo(left, bottom, left, bottom - radius); // 左下角 path.lineTo(left, top + radius); path.quadTo(left, top, left + radius, top); // 左上角 path.close(); canvas.drawPath(path, paint); }每个角的quadTo,控制点就是矩形的那个直角顶点,起点和终点分别是两条边上距离顶点radius的位置。这样曲线会“抄近路”绕过直角,形成一个平滑的圆角。注意这个近似圆角在数学上不是正圆弧,而是二次贝塞尔,视觉上足够接近,但如果radius很大,和真正的圆弧会有细微差别。追求精确圆角还是用arcTo或addRoundRect,这里主要是理解曲线构造思路。
注意:手动构造圆角时,radius不能超过矩形短边的一半,否则相邻两个角的曲线会重叠,图形会变形。这个约束在addRoundRect里系统会自动处理,但手写代码时要自己保证。
4. cubicTo:三次贝塞尔的控制力
4.1 两个控制点带来的自由度
cubicTo是三次贝塞尔,参数是两个控制点和一个终点,加上起点共四个点。相比quadTo的一个控制点,cubicTo多了一个控制点,这意味着曲线可以做出更复杂的形状——比如S形、回环、两端切线方向独立控制。
为什么多一个控制点就灵活这么多?回到参数方程:
B(t) = (1-t)³·P0 + 3t(1-t)²·P1 + 3t²(1-t)·P2 + t³·P3, t ∈ [0,1]P0是起点,P3是终点,P1和P2是两个控制点。关键在于:曲线在起点处的切线方向由P0指向P1,在终点处的切线方向由P2指向P3。也就是说,两个控制点分别“掌管”了两端的切线方向。quadTo只有一个控制点,起点和终点的切线方向是耦合的,没法独立调;cubicTo解耦了,所以能做出S形这种两端弯曲方向相反的曲线。
这个“切线方向”的理解是调cubicTo的核心。你想让曲线从起点往哪个方向出发,就把P1放在那个方向上;想让曲线从哪个方向进入终点,就把P2放在终点反方向的那条线上。掌握了这个,调参就不再是碰运气。
4.2 画一条平滑的S形曲线
S形曲线是cubicTo的经典练习。假设起点在左中,终点在右中,要做出先下后上(或先上后下)的S形,两个控制点要放在对角位置:
private void drawSCurve(Canvas canvas, Paint paint) { Path path = new Path(); float startX = 100, startY = 300; float endX = 500, endY = 300; path.moveTo(startX, startY); // 控制点1在起点右下方,控制点2在终点左上方 path.cubicTo( startX + 150, startY + 150, // P1: 拉向右下 endX - 150, endY - 150, // P2: 从左上进入终点 endX, endY ); canvas.drawPath(path, paint); }P1放在起点右下方,曲线出发时会朝右下走;P2放在终点左上方,曲线进入终点时是从左上方向下来的。两个方向一正一反,中间自然形成S形拐点。如果你把P2改成endY + 150(右下方),曲线就变成一条向下弯的弧,不再是S形。这个对比实验强烈建议你亲手跑一遍,对理解控制点方向极有帮助。
控制点的水平偏移量(上面代码里的150)决定了S形的“陡峭程度”。偏移越大,曲线在中段拐得越急;偏移越小,曲线越平缓。垂直偏移量决定了S形的“幅度”,也就是上下摆动的范围。这两个维度独立可调,调起来比quadTo更有掌控感。
4.3 手势轨迹平滑中的cubicTo应用
实际项目里cubicTo用得最多的地方之一是手写板或签名功能的轨迹平滑。手指触摸采集到的点是一系列离散坐标,直接lineTo连起来会有明显折角,用cubicTo做平滑效果好很多。
常见做法是:取相邻的三个采样点,用前一个点和后一个点的中点作为曲线端点,当前点作为控制点附近的位置,构造连续的三次贝塞尔。更简单的工程化方案是用“中点平滑法”:
// points是采集到的手势点列表 private Path buildSmoothPath(List<PointF> points) { Path path = new Path(); if (points.size() < 2) return path; path.moveTo(points.get(0).x, points.get(0).y); for (int i = 1; i < points.size() - 1; i++) { PointF current = points.get(i); PointF next = points.get(i + 1); float midX = (current.x + next.x) / 2f; float midY = (current.y + next.y) / 2f; path.quadTo(current.x, current.y, midX, midY); } // 最后一段连到最后一个点 PointF last = points.get(points.size() - 1); path.lineTo(last.x, last.y); return path; }这段用的是quadTo做中点平滑,是业界很常见的轻量方案。如果追求更高质量的平滑,可以升级成cubicTo,用前后点的差值估算切线方向,构造两个控制点。不过对于大多数签名、涂鸦场景,quadTo中点法已经足够,计算量还小。我实测过,采样点间隔在5到10像素时,中点平滑的视觉效果和cubicTo差别很小,但代码复杂度低不少,性价比更高。
实操心得:手势轨迹平滑有个隐藏坑——采样点太密时(比如高刷新率屏幕),相邻点距离可能只有1到2像素,这时候平滑反而会让线条显得“肉”。我的做法是加一个距离阈值,相邻点距离小于3像素就跳过,既减少计算量又让线条更利落。
5. arcTo:圆弧绘制的参数陷阱
5.1 arcTo的参数到底怎么理解
arcTo是这五个方法里参数最绕的,也是踩坑最多的。它的完整签名是:
public void arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo)四个参数:oval是圆弧所在的椭圆外接矩形,startAngle是起始角度,sweepAngle是扫过的角度,forceMoveTo决定是否连线。前两个角度参数的单位是度,不是弧度,这个先记住。
角度系统是这样的:0度在三点钟方向(正右方),顺时针为正。也就是说startAngle为0、sweepAngle为90,画出来的是从正右方顺时针到正下方的四分之一圆弧。这个“三点钟起点、顺时针为正”的约定和数学课本上的逆时针不同,是Android绘制的惯例,必须适应。
oval参数容易误解的地方是:它定义的是椭圆的外接矩形,圆弧是这个椭圆的一部分。如果oval是正方形,圆弧就是正圆的一部分;如果是长方形,圆弧就是椭圆弧。很多人以为oval直接就是圆的边界,画出来发现是椭圆,就是没理解这层关系。
5.2 forceMoveTo:那个让人抓狂的布尔值
forceMoveTo这个参数是arcTo最大的坑。它的含义是:为true时,圆弧起点成为新的路径起点,不和之前的路径连线;为false时,会从当前路径点画一条直线连到圆弧起点,再开始画弧。
Path path = new Path(); path.moveTo(100, 100); // forceMoveTo = false:会从(100,100)连一条线到圆弧起点 path.arcTo(new RectF(200, 200, 400, 400), 0, 90, false);上面这段代码,因为forceMoveTo是false,最终图形除了圆弧,还会多出一条从(100,100)到圆弧起点(400,300)的直线。如果你没预期到这条线,就会觉得图形“莫名其妙多了一笔”。反过来,forceMoveTo为true时,那条连线不会出现,圆弧独立成段。
我的经验是:画独立圆弧或扇形时用true,避免多余连线;画连续路径(比如圆角矩形的手动实现)时用false,让圆弧自然接上前一段。这个选择没有绝对对错,取决于你要的图形结构,但一定要有意识地选,而不是随手填个false然后被多出来的线搞懵。
5.3 环形进度条的完整实现
arcTo最实用的场景之一是环形进度条。下面这个例子画一个带背景环和进度环的进度条,参数计算过程我详细标注:
public class RingProgressView extends View { private Paint mBgPaint, mProgressPaint; private RectF mOval; private float mProgress = 0.65f; // 65%进度 public RingProgressView(Context context) { super(context); init(); } private void init() { mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mBgPaint.setStyle(Paint.Style.STROKE); mBgPaint.setStrokeWidth(30f); mBgPaint.setColor(0xFFE0E0E0); mProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mProgressPaint.setStyle(Paint.Style.STROKE); mProgressPaint.setStrokeWidth(30f); mProgressPaint.setColor(0xFF2196F3); mProgressPaint.setStrokeCap(Paint.Cap.ROUND); mOval = new RectF(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); float strokeOffset = 30f / 2f; // 描边有一半在矩形外侧,要内缩 float size = Math.min(w, h) - 30f; float left = (w - size) / 2f; float top = (h - size) / 2f; mOval.set(left, top, left + size, top + size); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 背景整圆 canvas.drawArc(mOval, 0, 360, false, mBgPaint); // 进度弧:从-90度(十二点钟)开始,顺时针扫过progress*360度 Path progressPath = new Path(); float sweep = mProgress * 360f; progressPath.arcTo(mOval, -90f, sweep, true); canvas.drawPath(progressPath, mProgressPaint); } }这里有几个关键计算点值得说清楚。第一,onSizeChanged里算oval时为什么要减strokeWidth?因为STROKE样式的描边是以路径为中心向两侧各扩展一半宽度,如果oval直接取整个View尺寸,描边外侧会被裁掉。所以oval要比View小一个strokeWidth,保证描边完整显示。第二,进度弧从-90度开始,是因为-90度对应十二点钟方向,进度条从顶部开始更符合直觉。第三,sweepAngle等于progress乘以360,progress是0到1的归一化值,乘出来就是实际扫过的角度。
注意:drawArc和arcTo都能画弧,区别在于drawArc直接画到Canvas上,arcTo是往Path里追加。如果只是画个静态弧,drawArc更直接;如果弧要参与后续的路径运算(比如裁剪、合并),用arcTo构建Path。另外drawArc的useCenter参数为true时会连到圆心形成扇形,这个和arcTo的forceMoveTo是两套逻辑,别搞混。
6. 五个方法的组合实战与排查手册
6.1 组合案例:带弧角的对话气泡
单独讲每个方法容易,真正考验理解的是组合使用。下面这个对话气泡,圆角用quadTo,尾巴用lineTo,整体轮廓用moveTo串联,是一个很好的综合练习:
private Path buildBubblePath(float left, float top, float right, float bottom, float radius) { Path path = new Path(); float tailWidth = 30f; float tailHeight = 25f; float tailStartX = left + 60f; // 尾巴起始位置 path.moveTo(left + radius, top); path.lineTo(right - radius, top); path.quadTo(right, top, right, top + radius); // 右上圆角 path.lineTo(right, bottom - radius); path.quadTo(right, bottom, right - radius, bottom); // 右下圆角 // 底边走到尾巴起点 path.lineTo(tailStartX + tailWidth, bottom); // 尾巴:向下尖出再回来 path.lineTo(tailStartX + tailWidth / 2f, bottom + tailHeight); path.lineTo(tailStartX, bottom); // 继续底边到左下圆角 path.lineTo(left + radius, bottom); path.quadTo(left, bottom, left, bottom - radius); // 左下圆角 path.lineTo(left, top + radius); path.quadTo(left, top, left + radius, top); // 左上圆角 path.close(); return path; }这个路径的构造顺序是顺时针绕一圈:顶边、右上角、右边、右下角、底边(含尾巴)、左下角、左边、左上角。尾巴部分用两个lineTo形成一个向下的三角尖。整个路径首尾通过close()闭合。这种“绕圈”的构造思路是画复杂图形的通用方法——选定一个起点,沿一个方向把轮廓走完,最后闭合。
实测这个气泡在radius取12到16、tailHeight取20到30之间视觉效果最好。radius太大圆角会吃掉直边,tailHeight太大尾巴显得突兀。这些数值没有标准答案,但有个调参技巧:先把所有参数设成0或最小值,画出最简形状,然后逐个增大,观察每个参数的影响,比一上来就填一堆数字高效得多。
6.2 常见问题速查表
Path绘制的问题大多集中在几个固定类型,我整理了一张速查表,遇到问题先对号入座:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 图形多出一条直线 | arcTo的forceMoveTo为false | 改成true或检查前序路径点 |
| 曲线形状和预期相反 | 控制点放在了错误的一侧 | 检查控制点相对起终点的方位 |
| 填充出现缺口 | 路径未close() | 补上close()或手动连回起点 |
| 圆角处图形变形 | radius超过短边一半 | 限制radius上限 |
| 网格线连成锯齿 | 缺少moveTo分隔 | 每条线前加moveTo |
| 描边被裁切 | oval未内缩strokeWidth一半 | 调整oval尺寸 |
| 曲线有折角不光滑 | 采样点直接lineTo | 改用quadTo/cubicTo平滑 |
这张表覆盖了我实际遇到的大部分情况。其中“曲线形状和预期相反”是最常见的,尤其是quadTo,控制点放反了曲线就朝反方向弯。判断方法很简单:控制点在起点终点连线的哪一侧,曲线就朝哪一侧凸。如果凸的方向不对,把控制点坐标关于连线做对称翻转即可。
6.3 性能与调试的几条经验
Path相关的性能问题,核心就一条:onDraw里不要做重计算和对象创建。Path对象在构造时创建一次,onDraw里用reset()复用。RectF同理,别在onDraw里new。这些是老生常谈,但真正写代码时还是经常忘。
调试Path有个很实用的技巧:把路径的每个关键点用小圆点画出来。比如调cubicTo时,把起点、两个控制点、终点都用drawCircle标出来,再画两条辅助线连接起点到P1、终点到P2,你就能直观看到控制点的“拉力方向”。这个方法我强烈推荐,比盯着代码空想高效十倍。控制点可视化之后,调参就变成了“把点拖到想要的位置”,而不是盲目试数字。
还有一个关于抗锯齿的细节:Path画曲线时,如果Paint没开ANTI_ALIAS_FLAG,曲线边缘会有明显锯齿,尤其是细线条。这个标志在Paint构造时传入,或者在init里setAntiAlias(true)。曲线越多,抗锯齿的视觉收益越明显,但会带来一定的性能开销,静态图形无所谓,高频重绘的动画要权衡。
最后分享一个我踩过的坑:Path的fillType。默认是WINDING,复杂自相交路径填充时可能出现“该填的没填、不该填的填了”的情况。如果遇到填充异常,试试setFillType(Path.FillType.EVEN_ODD),它用奇偶规则判断内外,对自相交图形更符合直觉。这个参数平时用不到,但一旦遇到诡异填充问题,它就是救命稻草。
曲线绘制这东西,看十遍不如动手画一遍。把上面每个例子的参数改一改,观察图形怎么变,比读任何教程都管用。我当初就是靠反复改waveHeight和controlX这些数字,才真正建立起对贝塞尔曲线的直觉。Path的API不多,但这几个方法的组合能玩出的花样远超想象,把基础打牢,后面做复杂自定义View时才有底气。