以flutter为例的path基础讲解:
路径形成的基础方法:
图源Flutter 绘制指南 - 妙笔生花,下面案例同,
flutter的圆弧都是以矩形的内接椭圆截取绘制而来的,所以下面的画弧的方法会传入Rect
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| //rect: 圆弧所在矩形 //startAngle : 开始弧度 //sweepAngle : 需要绘制的弧度大小 //forceMoveTo : 如果“forceMoveTo”参数为false,则添加一条直线段和一条弧段。 //如果“forceMoveTo”参数为true,则启动一个新的子路径,其中包含一个弧段。
Path path = Path(); Paint paint = Paint() ..color = Colors.purpleAccent ..strokeWidth = 2 ..style = PaintingStyle.stroke;
// 绘制左侧 //首先绘制外接矩形 var rect = Rect.fromCenter(center: Offset(0, 0), width: 160, height: 100); path.lineTo(30, 30);//画线,注意向下方向是y轴 //forcemoveTo为true,使得绘制圆弧时起始点强制moveTo圆弧的起始点 path..arcTo(rect, 0, pi * 1.5, true);//注意用的是级联 canvas.drawPath(path, paint);
path.reset(); //移动画布 canvas.translate(200, 0); // 绘制右侧 path.lineTo(30, 30); //forcemoveTo为true,使得绘制圆弧时起始点强制lineTo圆弧的起始点 path..arcTo(rect, 0, pi * 1.5, false); canvas.drawPath(path, paint);
|
arcToPoint
:当想要画圆弧到某个点,用 arcToPoint
会非常方便
- 接受一个
点位入参 Offset
,是圆弧的终点,可指定圆弧半径radius、是否使用优弧、是否顺时针
。 - 半径默认是0,即:不指定半径的话就会绘制直线,如果半径小于直径的一半则会以该半径做圆进行弧度近似(拼接)
relativeArcToPoint
:与arcToPoint
不同之处在于传入的offset代表dx、dy。
conicTo
:conicTo
接收五个参数用于绘制圆锥曲线,包括椭圆线
、抛物线
和双曲线
- 其中前两参是
控制点
,三四参是结束点
,第五参是权重。(下图已画出辅助点)- 当
权重< 1
时,圆锥曲线是椭圆线
,如下左图 - 当
权重= 1
时,圆锥曲线是抛物线
,如下中图 - 当
权重> 1
时,圆锥曲线是双曲线
,如下右图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| final Offset p1 = Offset(80, -100); final Offset p2 = Offset(160, 0);
Path path = Path(); Paint paint = Paint() ..color = Colors.purpleAccent ..strokeWidth = 2 ..style = PaintingStyle.stroke;
//抛物线 path.conicTo(p1.dx, p1.dy, p2.dx, p2.dy, 1); canvas.drawPath(path, paint);
path.reset(); canvas.translate(-180, 0); //椭圆线 path.conicTo(p1.dx, p1.dy, p2.dx, p2.dy, 0.5); canvas.drawPath(path, paint);
path.reset(); canvas.translate(180+180.0, 0); //双曲线 path.conicTo(p1.dx, p1.dy, p2.dx, p2.dy, 1.5); canvas.drawPath(path, paint);
|
relativeConicTo
:使用相对位置来加入圆锥曲线路径,参数含义与上面一致。
以上大部分引自小册,有些注释是我自己加的。大佬讲的挺好(绝对不是因为我平时根本不用才去cv的)
PS.
安卓原生也可以画弧但不能画双曲线,画弧的策略与flutter一样,
1
| void addArc (RectF oval, float startAngle, float sweepAngle)
|
float startAngle:开始的角度,X轴正方向为0度,float sweepAngel:持续的度数;而且好像只能顺时针画
贝塞尔函数:
感谢启舰大佬的动图
一阶:
虽然一阶贝塞尔函数没啥用,但它是认识高阶贝塞尔函数的基础。
对于一阶贝赛尔曲线,我们可以理解为在起始点和终点形成的这条直线上,匀速移动的点取值形成的轨迹,其实与这条直线没有区别。
二阶:
有一说一不研究真的看不明白,好在发现了大佬做的动画:
可以发现二阶其实就是两个一阶拼成的直线中又取了一次一阶贝塞尔函数
quadraticBezierTo
:假设P0是起始点,那么前两个参数代表P1,后两个参数代表P2。
relativeQuadraticBezierTo
:同上,不过参数都代表dx、dy
三阶:
其实就是贝塞尔曲线逐渐降阶的过程。
cubicTo
:三阶,两个控制点,一个结束点
relativeCubicTo
:同理
PS.安卓除了方法名外,参数并无不同。
贝塞尔函数的使用
作用:
一般来说最常见的应用是关于绘制时的优化(抗锯齿)以及波纹的相关动画。
绘制优化:
如果按照最基础的点动成线:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()){ case MotionEvent.ACTION_DOWN: { mPath.moveTo(event.getX(), event.getY()); return true; } case MotionEvent.ACTION_MOVE: mPath.lineTo(event.getX(), event.getY()); postInvalidate(); break; default: break; } return super.onTouchEvent(event); }
|
注意path只有调用canvas.paint
的时候才会进行绘制嗷
至于效果嘛,感觉锯齿比较严重,而且特别生硬:
然后,我的策略是很简单的暴力贝塞尔:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| float x,y; @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()){ case MotionEvent.ACTION_DOWN: { x = event.getX(); y = event.getY(); mPath.moveTo(event.getX(), event.getY()); return true; } case MotionEvent.ACTION_MOVE: mPath.quadTo(x,y,event.getX(), event.getY()); x = event.getX(); y = event.getY(); postInvalidate(); break; default: break; } return super.onTouchEvent(event); }
|
效果嘛,emmm,怎么感觉好像没起作用?
那我来告诉你答案,就是没起作用!
问题出在哪呢?可以看看启舰大佬给的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| private float mPreX,mPreY; @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()){ case MotionEvent.ACTION_DOWN:{ mPath.moveTo(event.getX(),event.getY()); mPreX = event.getX(); mPreY = event.getY(); return true; } case MotionEvent.ACTION_MOVE:{ float endX = (mPreX+event.getX())/2; float endY = (mPreY+event.getY())/2; mPath.quadTo(mPreX,mPreY,endX,endY); mPreX = event.getX(); mPreY =event.getY(); invalidate(); } break; default: break; } return super.onTouchEvent(event);
|
区别就在于MotionEvent.ACTION_MOVE
上:首先将上次的坐标与这次的坐标取了中点,作为结束点,用上次坐标作为控制点进行路径添加,一个quadTo
的终点,是下一个quadTo
的起始点。所以这里的起始点,就是上一个线段的中间点。那么显然我给的代码起始点和控制点是一个点,那不就是一阶贝塞尔函数嘛,那摆明了就是直线相连,能有啥作用。
图示:
下面是更正后的效果图,要好很多:
PS:
flutter写起来相对麻烦一些,因为无法直接调用监听,所以就需要手势监听获取坐标
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| GestureDetector( onPanDown: (details) { imageDrawLogic.isClear = false; imageDrawLogic.points[imageDrawLogic.curFrame].color = imageDrawLogic.selectedColor; imageDrawLogic.points[imageDrawLogic.curFrame].strokeWidth = imageDrawLogic.strokeWidth; }, onPanUpdate: (details) { RenderBox referenceBox = context.findRenderObject() as RenderBox; Offset localPosition = referenceBox.globalToLocal(details.globalPosition); state(() { imageDrawLogic.points[imageDrawLogic.curFrame].points.add(localPosition); }); }, onPanEnd: (details) { imageDrawLogic.points.add( Point( imageDrawLogic.selectedColor, imageDrawLogic.strokeWidth, [] )); imageDrawLogic.curFrame++; }, )
|
然后在绘制时,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| @override void paint(Canvas canvas, Size size) { final Path linePath = new Path() ; final rect = Rect.fromLTRB(0.0, 0.0, size.width, size.height); canvas.clipRect(rect); if (isClear || points.length == 0) { return; }
for (int i = 0; i < points.length; i++) { _linePaint..color = points[i].color; _linePaint..strokeWidth = points[i].strokeWidth; List<Offset> curPoints = points[i].points; if (curPoints.length == 0) { break; } double x = curPoints[0].dx-1; double y = curPoints[0].dy-1; linePath.moveTo(x, y); for (int i = 1; i < curPoints.length; i++) { double endX = (x + curPoints[i].dx)/2; double endY = (y + curPoints[i].dy)/2; linePath.quadraticBezierTo(x,y,endX,endY); x = curPoints[i].dx; y = curPoints[i].dy; } canvas.drawPath(linePath, _linePaint); } }
|
水波纹效果
(坑)