文章

Path

Path

自定义 View : Path

注意

  • Canvas.drawPath 无效, paint 没有设置 paint.setStyle(Style.STROKE)

Path 介绍

Path 封装了由直线和曲线 (二次,三次贝塞尔曲线) 构成的几何路径。你能用 Canvas 中的 drawPath 来把这条路径画出来 (同样支持 Paint 的不同绘制模式),也可以用于剪裁画布和根据路径绘制文字。我们有时会用 Path 来描述一个图像的轮廓,所以也会称为轮廓线 (轮廓线仅是 Path 的一种使用方法,两者并不等价)

常用方法

作用相关方法备注 
移动起点moveTo移动下一次操作的起点位置 
设置终点setLastPoint重置当前 path 中最后一个点位置,如果在绘制之前调用,效果和 moveTo 相同 
连接直线lineTo添加上一个点到当前点之间的直线到 Path,如果没有调用 moveTo,默认点为原点 
闭合路径close连接第一个点连接到最后一个点,形成一个闭合区域 
添加内容addRect, addRoundRect, addOval, addCircle, addPath, addArc, arcTo添加 (矩形, 圆角矩形, 椭圆, 圆, 路径, 圆弧) 到当前 Path (注意 addArc 和 arcTo 的区别) 
是否为空isEmpty判断 Path 是否为空 
是否为矩形isRect判断 path 是否是一个矩形 
替换路径set用新的路径替换到当前路径所有内容 
偏移路径offset对当前路径之前的操作进行偏移 (不会影响之后的操作) 
贝塞尔曲线quadTo, cubicTo分别为二次和三次贝塞尔曲线的方法 
rXxx 方法rMoveTo, rLineTo, rQuadTo, rCubicTo不带 r 的方法是基于原点的坐标系 (偏移量), rXxx 方法是基于当前点坐标系 (偏移量) 
填充模式setFillType, getFillType, isInverseFillType, toggleInverseFillType设置,获取,判断和切换填充模式 
提示方法incReserve提示 Path 还有多少个点等待加入 (这个方法貌似会让Path优化存储结构) 
布尔操作 (API19)op对两个 Path 进行布尔运算 (即取交集、并集等操作) 
计算边界computeBounds计算 Path 的边界 
重置路径reset, rewind清除 Path 中的内容 
reset 不保留内部数据结构,但会保留 FillType.-- 
rewind 会保留内部的数据结构,但不保留 FillType-- 
矩阵操作transform矩阵变换 

contour 概念

子图形」:官方文档里叫做 contour。但由于在这个场景下我找不到这个词合适的中文翻译(直译的话叫做「轮廓」),所以我换了个便于中国人理解的词:「子图形」。

  • 第一组 addXXX 方法是「添加子图形」,所谓「子图形」,指的就是一次不间断的连线。一个 Path 可以包含多个子图形。当使用第一组方法,即 addCircle() addRect() 等方法的时候,每一次方法调用都是新增了一个独立的子图形;
  • 而如果使用第二组方法,即 lineTo() arcTo()addArc() 等方法的时候,则是每一次断线(即每一次「抬笔」),都标志着一个子图形的结束,以及一个新的子图形的开始。

另外,不是所有的子图形都需要使用 close() 来封闭。当需要填充图形时(即 Paint.Style 为 FILL 或 FILL_AND_STROKE),Path 会自动封闭子图形。

用法

lineTo 未闭合

1
2
3
4
5
canvas.translate(mWidth / 2, mHeight / 2);  // 移动坐标系到屏幕中心(宽高数据在onSizeChanged中获取)
Path path = new Path();                     // 创建Path
path.lineTo(200, 200);                      // lineTo
path.lineTo(200,0);
canvas.drawPath(path, mPaint);              // 绘制Path

moveTo 和 setLastPoint

方法名简介是否影响之前的操作是否影响之后操作
moveTo移动下一次操作的起点位置
setLastPoint设置之前操作的最后一个点位置
  • moveTo
1
2
3
4
5
6
canvas.translate(mWidth / 2, mHeight / 2);  // 移动坐标系到屏幕中心
Path path = new Path();                     // 创建Path
path.lineTo(200, 200);                      // lineTo
path.moveTo(200,100);                       // moveTo
path.lineTo(200,0);                         // lineTo
canvas.drawPath(path, mPaint);              // 绘制Path

moveTo 只改变下次操作的起点,在执行完第一次 LineTo 的时候,本来的默认点位置是 A(200,200),但是 moveTo 将其改变成为了 C(200,100),所以在第二次调用 lineTo 的时候就是连接 C(200,100) 到 B(200,0) 之间的直线 (用蓝色圈 2 标注)。

  • setLastPoint
1
2
3
4
5
6
canvas.translate(mWidth / 2, mHeight / 2);  // 移动坐标系到屏幕中心
Path path = new Path();                     // 创建Path
path.lineTo(200, 200);                      // lineTo
path.setLastPoint(200, 100);                 // setLastPoint
path.lineTo(200, 0);                         // lineTo
canvas.drawPath(path, mPaint);              // 绘制Path

setLastPoint 是重置上一次操作的最后一个点,在执行完第一次的 lineTo 的时候,最后一个点是 A(200,200),而 setLastPoint 更改最后一个点为 C(200,100),所以在实际执行的时候,第一次的 lineTo 就不是从原点 O 到 A(200,200) 的连线了,而变成了从原点 O 到 C(200,100) 之间的连线了。在执行完第一次 lineTo 和 setLastPoint 后,最后一个点的位置是 C(200,100),所以在第二次调用 lineTo 的时候就是 C(200,100) 到 B(200,0) 之间的连线 (用蓝色圈 2 标注)。

close 闭合

close 方法用于连接当前最后一个点和最初的一个点 (如果两个点不重合的话),最终形成一个封闭的图形。

1
2
3
4
5
6
canvas.translate(mWidth / 2, mHeight / 2);  // 移动坐标系到屏幕中心
Path path = new Path();                     // 创建Path
path.lineTo(200, 200);                      // lineTo
path.lineTo(200, 0);                         // lineTo
path.close();                               // close
canvas.drawPath(path, mPaint);              // 绘制Path

注意:close 的作用是封闭路径,与连接当前最后一个点和第一个点并不等价。如果连接了最后一个点和第一个点仍然无法形成封闭图形,则 close 什么 也不做

Path 添加图形和路径

addXXX 添加子图形

1
2
3
4
5
6
7
8
9
10
11
// 第一类(基本形状)
// 圆形
public void addCircle (float x, float y, float radius, Path.Direction dir)
// 椭圆
public void addOval (RectF oval, Path.Direction dir)
// 矩形
public void addRect (float left, float top, float right, float bottom, Path.Direction dir)
public void addRect (RectF rect, Path.Direction dir)
// 圆角矩形
public void addRoundRect (RectF rect, float[] radii, Path.Direction dir)
public void addRoundRect (RectF rect, float rx, float ry, Path.Direction dir)

第一类的方法,无一例外,在最后都有一个 Path.Direction。CW(clockwise) 顺时针;CCW(counter-clockwise) 逆时针

addRect 绘制矩形
1
2
3
4
5
6
7
8
9
// dir是方向,CW是顺时针,CCW是逆时针
addRect(RectF rect, Direction dir)
addRect(float left, float top, float right, float bottom, Direction dir)

// rx是横向,ry是纵向,radii是4对(x,y)
addRoundRect(RectF rect, float rx, float ry, Direction dir)
addRoundRect(float left, float top, float right, float bottom, float rx, float ry, Direction dir)
addRoundRect(RectF rect, float[] radii, Direction dir)
addRoundRect(float left, float top, float right, float bottom, float[] radii, Direction dir)
  • CW,顺时针
1
2
3
4
5
canvas.translate(mWidth / 2, mHeight / 2);  // 移动坐标系到屏幕中心
Path path = new Path();
path.addRect(-200,-200,200,200, Path.Direction.CW);
path.setLastPoint(-300,300);                // <-- 重置最后一个点的位置
canvas.drawPath(path,mPaint);

  • CCW,逆时针
1
2
3
4
5
canvas.translate(mWidth / 2, mHeight / 2);  // 移动坐标系到屏幕中心
Path path = new Path();
path.addRect(-200,-200,200,200, Path.Direction.CCW);
path.setLastPoint(-300,300);                // <-- 重置最后一个点的位置
canvas.drawPath(path,mPaint);


对于上面这个矩形来说,我们采用的是顺时针 (CW),所以记录的点的顺序就是 A -> B -> C -> D。最后一个点就是 D,我们这里使用 setLastPoint 改变最后一个点的位置实际上是改变了 D 的位置。

我们将顺时针改为逆时针 (CCW),则记录点的顺序应该就是 A -> D -> C -> B, 再使用 setLastPoint 则改变的是 B 的位置

参数中点的顺序很重要。尝试交换两个坐标点,或者指定另外两个点来作为参数,虽然指定的是同一个矩形,但实际绘制出来是不同

addOval 椭圆、addCircle 圆
1
2
3
addOval(RectF oval, Direction dir)
addOval(float left, float top, float right, float bottom, Direction dir)
addCircle(float x, float y, float radius, Direction dir)
Direction 类

Direction 类主要用于绘制文字时的方向

  • CW 是顺时针
  • CCW 是逆时针
1
2
3
4
5
6
7
8
9
Path path = new Path();
path.addOval(50, 50, 300, 200, Path.Direction.CW);
canvas.drawPath(path, mRedPaint);
canvas.drawTextOnPath("This is a text", path, 0, 0, mBluePaint);

path.reset();
path.addOval(400, 50, 650, 200, Path.Direction.CCW);
canvas.drawPath(path, mRedPaint);
canvas.drawTextOnPath("This is a text", path, 0, 0, mBluePaint);


addPath 画线(直线或曲线)

1
2
3
4
// path
public void addPath (Path src) // 将两个Path合并成为一个
public void addPath (Path src, float dx, float dy) // 第二个方法比第一个方法多出来的两个参数是将src进行了位移之后再添加进当前path中
public void addPath (Path src, Matrix matrix) // 将src添加到当前path之前先使用Matrix进行变换

将两个 Path 合并成为一个。

1
2
3
4
5
6
7
8
9
canvas.translate(mWidth / 2, mHeight / 2);  // 移动坐标系到屏幕中心
canvas.scale(1,-1);                         // <-- 注意 翻转y坐标轴
Path path = new Path();
Path src = new Path();
path.addRect(-200,-200,200,200, Path.Direction.CW);
src.addCircle(0,0,100, Path.Direction.CW);
path.addPath(src,0,200);
mPaint.setColor(Color.BLACK);           // 绘制合并后的路径
canvas.drawPath(path,mPaint);

addArc 与 arcTo 绘制圆弧 (0° 是在矩形的右边的中点)

通用参数解析
1
2
3
4
5
6
7
8
// addArc
addArc(RectF oval, float startAngle, float sweepAngle)
addArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle)

// arcTo
arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo)
arcTo(RectF oval, float startAngle, float sweepAngle)
arcTo(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo)
  • startAngle 是代表开始的角度,那么 Android 中矩形的 0° 是从哪里开始呢?其实矩形的 0° 是在矩形的右边的中点,按顺时针方向逐渐增大。
  • sweepAngle 扫过的角度就是从起点角度开始扫过的角度,并不是指终点的角度。正数表示顺时针,负数表示逆时针 例如如果你的 startAngle 是 90°,sweepAngle 是 180°。那么这个圆弧的终点应该在 270°,而不是在 180°。
  • forceMoveTo forceMoveTo 参数的意思是,绘制是要「抬一下笔移动过去」,还是「直接拖着笔过去」,区别在于是否留下移动的痕迹。forceMoveTo 意思为 “ 是否强制使用 moveTo”,也就是说,是否使用 moveTo 将变量移动到圆弧的起点位移,也就意味着:
forceMoveTo含义等价方法
true新开一个轮廓,即不连接最后一个点与圆弧起点public void addArc (RectF oval, float startAngle, float sweepAngle)
false不移动,而是连接最后一个点与圆弧起点public void arcTo (RectF oval, float startAngle, float sweepAngle)
弧线是怎么来的?

先画一个椭圆,然后再在这个椭圆上面截取一部分部形。这个图形自然就是一个弧线了。那么这个椭圆是怎么确定的呢?这就是这个 rectF(4 个参数也是一样)参数所起的作用了。

image.png

给出这个矩形后,系统就可以算出这个矩形的中心,然后以这个矩开的中心画一个椭圆。得到这个椭圆后,然后就是截取一部分线了,就得到最终的弧线。这一部分是怎么截取的呢?

这就是后面两个参数共同来表达的。

startAngle 这个参数说的是开始的角度。这个好理解,但哪里是 0 度线呢,又是向哪个方向旋转是正角度数呢?下面由图形来展示:

image.png

图上所示的红线就是 0 度线。

startAngle 是开始度数,那 sweepAngle 是指的什么呢?

sweepAngle 指的是旋转的度数,也就是以 startAngle 开始,旋转多少度,如果 sweepAngle 是正数,那么就是按顺时针方向旋转,如果是负数就是按逆时针方向旋转。

如果示例:startAngle = 0; sweepAngle=90 时:

image.png

addArc

作用: 直接添加一段圆弧(0° 是在矩形的右边的中点)

示例

可以看到 addArc 有 1 个方法 (实际上是两个的,但另一个重载方法是 API21 添加的),

1
2
3
4
5
6
7
8
canvas.translate(mWidth / 2, mHeight / 2);  // 移动坐标系到屏幕中心
canvas.scale(1,-1);                         // <-- 注意 翻转y坐标轴
Path path = new Path();
path.lineTo(100,100);
RectF oval = new RectF(0,0,300,300);
path.addArc(oval,0,270);
// path.arcTo(oval,0,270,true);             // <-- 和上面一句作用等价
canvas.drawPath(path,mPaint);

arcTo  

作用: 添加一段圆弧,如果圆弧的起点与上一次 Path 操作的终点不一样的话,就会在这两个点连成一条直线 (0° 是在矩形的右边的中点)

1
2
3
4
5
6
7
8
canvas.translate(mWidth / 2, mHeight / 2);  // 移动坐标系到屏幕中心
canvas.scale(1,-1);                         // <-- 注意 翻转y坐标轴
Path path = new Path();
path.lineTo(100,100);
RectF oval = new RectF(0,0,300,300);
path.arcTo(oval,0,270);
// path.arcTo(oval,0,270,false);             // <-- 和上面一句作用等价
canvas.drawPath(path,mPaint);

如果你不想这两个点连线的话,arcTo 在一个方法中有 forceMoveTo 的参数,这个参数如果设为 true 就说明将上一次操作的点设为圆弧的起点,也就是说不会将圆弧的起点与上一次操作的点连接起来。如果设为 false 就会连接。

  1. arc 案例 forceMoveTo=true 强制移动到弧形起点(无痕迹)
1
2
3
paint.setStyle(Style.STROKE);
path.lineTo(100, 100);
path.arcTo(100, 100, 300, 300, -90, 90, true); // 强制移动到弧形起点(无痕迹)

  1. arc 案例 forceMoveTo=false 直接连线连到弧形起点(有痕迹)
1
2
3
paint.setStyle(Style.STROKE);
path.lineTo(100, 100);
path.arcTo(100, 100, 300, 300, -90, 90, false); // 直接连线连到弧形起点(有痕迹)

addArc 和 arcTo 区别?
  • addArc 可以直接加入一段椭圆弧。使用 arcTo 还需要使用 moveTo 指定当前点的坐标。
  • arcTo 如果当前点坐标和曲线的起始点不是同一个点的话,还会自动添加一条直线补齐路径。

rMoveTo/rLineTo

rXxx 方法的坐标使用的是相对位置 (基于当前点的位移),而 moveTo/liveTo 的坐标是绝对位置 (基于当前坐标系的坐标)。

1
2
3
4
5
6
Path path = new Path();

path.moveTo(100,100);
path.rLineTo(100,200);

canvas.drawPath(path,mDeafultPaint);

辅助的设置或计算

Path.op 方法 布尔操作 (API19)

1
2
boolean op(Path path, Op op)
boolean op(Path path1, Path path2, Op op)

布尔操作是两个 Path 之间的运算,主要作用是用一些简单的图形通过一些规则合成一些相对比较复杂,或难以直接得到的图形。

原始:

DIFFERENCE path1 减去 path1 和 path2 相交部分
1
2
3
4
5
6
7
8
private fun DIFFERENCE(canvas: Canvas) {
    path1.reset()
    path2.reset()
    path1.addCircle(150F.dp(), 150F.dp(), 75F.dp(), Path.Direction.CW)
    path2.addCircle(250F.dp(), 150F.dp(), 75F.dp(), Path.Direction.CW)
    path1.op(path2, Path.Op.DIFFERENCE)
    canvas.drawPath(path1, mRedPaint)
}

INTERSECT 取 path1 和 path2 相交部分
1
2
3
4
5
6
7
8
9
// path1和path2相交部分
private fun INTERSECT(canvas: Canvas) {
    path1.reset()
    path2.reset()
    path1.addCircle(150F.dp(), 150F.dp(), 75F.dp(), Path.Direction.CW)
    path2.addCircle(250F.dp(), 150F.dp(), 75F.dp(), Path.Direction.CW)
    path1.op(path2, Path.Op.INTERSECT)
    canvas.drawPath(path1, mRedPaint)
}

UNION path1 和 path2 并集(逻辑或)
1
2
3
4
5
6
7
8
private fun UNION(canvas: Canvas) {
    path1.reset()
    path2.reset()
    path1.addCircle(150F.dp(), 150F.dp(), 75F.dp(), Path.Direction.CW)
    path2.addCircle(250F.dp(), 150F.dp(), 75F.dp(), Path.Direction.CW)
    path1.op(path2, Path.Op.UNION)
    canvas.drawPath(path1, mRedPaint)
}

XOR 与 INTERSECT 刚好相反, path1 和 path2 非交集(逻辑 xor)
1
2
3
4
5
6
7
8
private fun XOR(canvas: Canvas) {
    path1.reset()
    path2.reset()
    path1.addCircle(150F.dp(), 150F.dp(), 75F.dp(), Path.Direction.CW)
    path2.addCircle(250F.dp(), 150F.dp(), 75F.dp(), Path.Direction.CW)
    path1.op(path2, Path.Op.XOR)
    canvas.drawPath(path1, mRedPaint)
}

REVERSE_DIFFERENCE 与 DIFFERENCE 刚好相反, path2 减去 path1 和 path2 相交部分
1
2
3
4
5
6
7
8
private fun REVERSE_DIFFERENCE(canvas: Canvas) {
    path1.reset()
    path2.reset()
    path1.addCircle(150F.dp(), 150F.dp(), 75F.dp(), Path.Direction.CW)
    path2.addCircle(250F.dp(), 150F.dp(), 75F.dp(), Path.Direction.CW)
    path1.op(path2, Path.Op.REVERSE_DIFFERENCE)
    canvas.drawPath(path1, mRedPaint)
}

Path.FillType 填充模式 (只对 Paint 为 FILL 或 FILL_STROKE 有效)


https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/[07]Path_Over.md#填充模式

isEmpty、 isRect、isConvex、 set 和 offset

public boolean isEmpty ()

1
Returns true if the path is empty (contains no lines or curves)

public boolean isRect (RectF rect)

判断 path 是否是一个矩形,如果是一个矩形的话,会将矩形的信息存放进参数 rect 中。

public void set (Path src)

将新的 path 赋值到现有 path。

offset

1
2
public void offset (float dx, float dy)
public void offset (float dx, float dy, Path dst) // dst不为空,将当前path平移后的状态存入dst中,不会影响当前path;dst为空(null),平移将作用于当前path,相当于第一种方法

对 path 进行一段平移,它和 Canvas 中的 translate 作用很像,但 Canvas 作用于整个画布,而 path 的 offset 只作用于当前 path。

1
2
3
4
5
6
7
8
9
10
canvas.translate(mWidth / 2, mHeight / 2);  // 移动坐标系到屏幕中心
canvas.scale(1,-1);                         // <-- 注意 翻转y坐标轴
Path path = new Path();                     // path中添加一个圆形(圆心在坐标原点)
path.addCircle(0,0,100, Path.Direction.CW);
Path dst = new Path();                      // dst中添加一个矩形
dst.addRect(-200,-200,200,200, Path.Direction.CW);
path.offset(300,0,dst);                     // 平移
canvas.drawPath(path,mPaint);               // 绘制path
mPaint.setColor(Color.BLUE);                // 更改画笔颜色
canvas.drawPath(dst,mPaint);                // 绘制dst

虽然我们在 dst 中添加了一个矩形,但是并没有表现出来,所以,当 dst 中存在内容时,dst 中原有的内容会被清空,而存放平移后的 path。

reset/rewind 重置路径

重置 Path 有两个方法,分别是 resetrewind,两者区别主要有以下两点:

方法是否保留 FillType 设置是否保留原有数据结构
reset
rewind

选择权重: FillType > 数据结构;因为 “FillType” 影响的是显示效果,而 “ 数据结构 “ 影响的是重建速度。

getFillPath

1
2
// 根据原始Path(src)获取预处理后的Path(dst)
paint.getFillPath(Path src, Path dst);

在 PathEffect 一开始有这样一句介绍:”PathEffect 在绘制之前修改几何路径… “ 这句话表示的意思是,我们在绘制内容时,会在绘制之前对这些内容进行处理,最终进行绘制的内容实际上是经过处理的,而不是原始的。

PathEffect 路径效果

见 Paint 的 PathEffect

quardTo/cubicTo 贝塞尔曲线

见:贝塞尔曲线Bezier

Path 坑

addRect 一大一小同方向只显示一个

1
2
3
4
5
canvas.translate(mViewWidth/2, mViewHeight/2);
Path path = new Path();
path.addRect(-100, -100, 100, 100, Path.Direction.CW);
path.addRect(-200, -200, 200, 200, Path.Direction.CW);
canvas.drawPath(path, paint);

将 画布移动到中心点 ,绘制两个矩形 一大一小 ,但是运行结果是 只显示大的那个矩形
目前问题原因没有找到:涉及到 native  ,猜想是 NDK 给 抹掉了
解决方案 :

1
 将 其中一个 绘画改为逆时针   Path.Direction.CCW   (保证两个矩形绘制顺序不一样)

Path Ref

Path 训练

详细

PathMeasure

什么是 PathMeasure?

PathMeasure 是用来对 Path 进行测量的,一般 PathMeasure 是和 Path 配合使用的,通过 PathMeasure,我们可以知道 Path 路径上某个点的坐标、Path 的长度等等。

PathMeasure API

构造方法

PathMeasure 有两个构造函数:

1
2
3
4
// 构建一个空的PathMeasure
PathMeasure() 
// 构建一个PathMeasure并关联一个指定的创建好的Path
PathMeasure(Path path, boolean forceClosed)
  1. 无参构造函数
    用这个构造函数可创建一个空的 PathMeasure,但是使用之前需要先调用 setPath 方法来与 Path 进行关联。被关联的 Path 必须是已经创建好的,如果关联之后 Path 内容进行了更改,则需要使用 setPath 方法重新关联。
  2. 有参构造函数
    创建一个 PathMeasure 并关联一个 Path, 其实和创建一个空的 PathMeasure 后调用 setPath 进行关联效果是一样的,同样,被关联的 Path 也必须是已经创建好的,如果关联之后 Path 内容进行了更改,则需要使用 setPath 方法重新关联。

Path 与 PathMeasure 进行关联并不会影响 Path 状态。

setPath(Path path, boolean forceClosed)   关联一个 Path

  1. 第一个参数自然就是被关联的 Path 了。
  2. 第二个参数是用来确保 Path 闭合,如果设置为 true, 则不论之前 Path 是否闭合,都会自动闭合该 Path(如果 Path 可以闭合的话,不能闭合的就闭合不了)。并且关联的 Path 未闭合时,测量的 Path 长度可能会比 Path 的实际长度长一点,因为测量的是 Path 闭合的长度,但关联的 Path 不会有任何变化。
  1. 不论 forceClosed 设置为何种状态 (true 或者 false), 都不会影响原有 Path 的状态,即 Path 与 PathMeasure 关联之后,之前的的 Path 不会有任何改变。
  2. forceClosed 的设置状态可能会影响测量结果,如果 Path 未闭合但在与 PathMeasure 关联的时候设置 forceClosed 为 true 时,测量结果可能会比 Path 实际长度稍长一点,获取到到是该 Path 闭合时的状态。(forceClosed 为 false 测量的是当前 Path 状态的长度, forceClosed 为 true,则不论 Path 是否闭合测量的都是 Path 的闭合长度。)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
canvas.translate(mViewWidth/2,mViewHeight/2);

Path path = new Path();

path.lineTo(0,200);
path.lineTo(200,200);
path.lineTo(200,0);

PathMeasure measure1 = new PathMeasure(path,false);
PathMeasure measure2 = new PathMeasure(path,true);

Log.e("TAG", "forceClosed=false---->"+measure1.getLength());
Log.e("TAG", "forceClosed=true----->"+measure2.getLength());

canvas.drawPath(path,mDeafultPaint);

log 如下:

1
2
com.gcssloop.canvas E/TAG: forceClosed=false---->600.0
com.gcssloop.canvas E/TAG: forceClosed=true----->800.0

image.png

float getLength() 获取 Path 的长度

返回已关联 Path 的总长度,若 setPath() 时设置的 forceClosed 为 true,则返回值可能会比实际长度长。

boolean isClosed()  是否闭合

用于判断 Path 是否闭合,但是如果你在关联 Path 的时候设置 forceClosed 为 true 的话,这个方法的返回值则一定为 true。

boolean nextContour() 跳转到下一个轮廓

Path 可以由多条曲线构成,但不论是 getLength , getSegment 或者是其它方法,都只会在其中第一条线段上运行。nextContour 就是用于跳转到下一条曲线到方法,如果跳转成功,则返回 true, 如果跳转失败,则返回 false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
canvas.translate(mViewWidth / 2, mViewHeight / 2);      // 平移坐标系

Path path = new Path();
path.addRect(-100, -100, 100, 100, Path.Direction.CW);  // 添加小矩形,改成CCW可以同时显示,不然只能显示一个
path.addRect(-200, -200, 200, 200, Path.Direction.CW);  // 添加大矩形
canvas.drawPath(path,mDeafultPaint);                    // 绘制 Path

PathMeasure measure = new PathMeasure(path, false);     // 将Path与PathMeasure关联

float len1 = measure.getLength();                       // 获得第一条路径的长度

measure.nextContour();                                  // 跳转到下一条路径

float len2 = measure.getLength();                       // 获得第二条路径的长度
Log.i("LEN","len1="+len1);                              // 输出两条路径的长度
Log.i("LEN","len2="+len2);

同方向的 addRect,只能显示一个,原因未知;改成一个 CW,一个 CCW 就可以都显示 image.png

  1. 曲线的顺序与 Path 中添加的顺序有关。
  2. getLength 获取到到是当前一条曲线分长度,而不是整个 Path 的长度。
  3. getLength 等方法是针对当前的曲线

getSegment

boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)  截取片段

截取关联 Path 的一段 (startD-stopD) 存到 dst,如果截取成功,返回 true;反之返回 false。(起始点为 path 第一个点)

参数作用备注
返回值 (boolean)判断截取是否成功true 表示截取成功,结果添加到 dst 中(不是替换),false 截取失败,不会改变 dst 中内容。如果截取的 Path 的长度为 0,则返回 false,大于 0 则返回 true;startD、stopD 必须为合法值 (0,getLength()),如果 startD>=stopD,则返回 false;
startD开始截取位置距离 Path 起点的长度取值范围: 0 <= startD < stopD <= Path 总长度
stopD结束截取位置距离 Path 起点的长度取值范围: 0 <= startD < stopD <= Path 总长度
dst截取的 Path 将会添加到 dst 中注意: 是添加,而不是替换
startWithMoveTo起始点是否使用 moveTostartWithMoveTo 为 true 时,起始点保持不变,被截取的 path 片段会保持原状;startWithMoveTo 为 false 时,会将截取的 path 片段的起始点移动到 dstPath 的终点以保持 dstPath 的连续性

在 4.4 或之前的版本,在开启硬件加速时,绘制可能会不显示,请关闭硬件加速或者给 dst 添加一个简单操作,如: dst.rLineTo(0, 0)

  • 案例 1:dst 没有内容时,startWithMoveTo 为 true
1
2
3
4
5
6
7
8
canvas.translate(mViewWidth / 2, mViewHeight / 2);          // 平移坐标系
Path path = new Path();                                     // 创建Path并添加了一个矩形
path.addRect(-200, -200, 200, 200, Path.Direction.CW);
Path dst = new Path();                                      // 创建用于存储截取后内容的 Path
PathMeasure measure = new PathMeasure(path, false);         // 将 Path 与 PathMeasure 关联
// 截取一部分存入dst中,并使用 moveTo 保持截取得到的 Path 第一个点的位置不变
measure.getSegment(200, 600, dst, true);                    
canvas.drawPath(dst, mDeafultPaint);                        // 绘制 dst

image.png

  • 案例 2:dst 中有内容时,startWithMoveTo 为 true
1
2
3
4
5
6
7
8
canvas.translate(mViewWidth / 2, mViewHeight / 2);          // 平移坐标系
Path path = new Path();                                     // 创建Path并添加了一个矩形
path.addRect(-200, -200, 200, 200, Path.Direction.CW);
Path dst = new Path();                                      // 创建用于存储截取后内容的 Path
dst.lineTo(-300, -300);                                     // <--- 在 dst 中添加一条线段
PathMeasure measure = new PathMeasure(path, false);         // 将 Path 与 PathMeasure 关联
measure.getSegment(200, 600, dst, true);                   // 截取一部分 并使用 moveTo 保持截取得到的 Path 第一个点的位置不变
canvas.drawPath(dst, mDeafultPaint);                        // 绘制 Path

image.png

被截取的 Path 片段会添加到 dst 中,而不是替换 dst 中到内容。

  • 案例 3:dst 有内容,且 startWithMoveTo 为 false
1
2
3
4
5
6
7
8
canvas.translate(mViewWidth / 2, mViewHeight / 2);          // 平移坐标系
Path path = new Path();                                     // 创建Path并添加了一个矩形
path.addRect(-200, -200, 200, 200, Path.Direction.CW);
Path dst = new Path();                                      // 创建用于存储截取后内容的 Path
dst.lineTo(-300, -300);                                     // 在 dst 中添加一条线段
PathMeasure measure = new PathMeasure(path, false);         // 将 Path 与 PathMeasure 关联
measure.getSegment(200, 600, dst, false);                   // <--- 截取一部分 不使用 startMoveTo, 保持 dst 的连续性
canvas.drawPath(dst, mDeafultPaint);                        // 绘制 Path

image.png

如果 startWithMoveTo 为 true, 则被截取出来到 Path 片段保持原状,如果 startWithMoveTo 为 false,则会将截取出来的 Path 片段的起始点移动到 dst 的最后一个点,以保证 dst 的连续性。

getPosTan

boolean getPosTan(float distance, float pos[], float tan[])  获取指定长度的位置坐标及该点切线值

参数作用备注
返回值 (boolean)判断获取是否成功true 表示成功,数据会存入 pos 和 tan 中,false 表示失败,pos 和 tan 不会改变
distance距离 Path 起点的长度取值范围: 0 <= distance <= getLength
pos该点的坐标值当前点在画布上的位置,有两个数值,分别为 x,y 坐标。
tan该点的正切值当前点在曲线上的方向,使用 Math.atan2(tan[1], tan[0]) 获取到正切角的弧度值。tan[0] 对应角度的 cos 值,对应 X 坐标。tan[1] 对应角度的 sin 值,对应 Y 坐标。当前点的切线与 x 轴夹角的 tan 值,我们通常结合 Math.atan2 方法来计算旋转角度。

其他 2 个参数好理解,主要看 tan 这个参数:

tan

tan 在数学中被称为正切,在直角三角形中,一个锐角的正切定义为它的对边 (Opposite side) 与邻边 (Adjacent side) 的比值 (来自维基百科):

tan 来描述 Path 上某一点的切线方向,主要用了两个数值 tan[0] 和 tan[1] 来描述这个切线的方向(切线方向与x轴夹角),看上面公式可知 tan 既可以用 对边/邻边 来表述,也可以用 sin/cos 来表述,此处用两种理解方式均可以 (注意下面等价关系):

tan[0] = cos = 邻边 (单位圆 x 坐标)
tan[1] = sin = 对边 (单位圆 y 坐标)

Math.atan2 方法,是根据传入的 x,y 坐标,计算与原点之间的夹角弧度。即:弧度 A = Math.atan2(P.y,P.x),使用 Math.atan2(tan[1], tan[0]) 将 tan 转化为角 (单位为弧度) 的时候要注意参数顺序。

getPosTan tan 参数

image.png

1
2
3
4
5
6
7
说明:
角 A 的正切值 tanA = P.y/P.x
P 点坐标为(P.x,P.y)
P 点的横坐标 P.x = cosA * 斜边
P 点的纵坐标 P.y = sinA * 斜边
所以最后我们的 tanA 的值为tanA=sinA/cosA
所以我们 tan[]数组中存放的分别是 tan[0]=cosA和 tan[1]=sinA的值。

对于 tan[] 数组,描述的是当前点的切线与 x 轴夹角的 tan 值,我们通常结合 Math.atan2 方法来计算旋转角度。

image.png

1
2
3
4
5
6
下图说明:绿色箭头表示切线,当处于点(2,0)时,切线于水平方向夹角为90°,tan[0]=cos90=0,tan[1]=sin90=1。
所以,getPosTan 方法中 返回的tan[]数组描述的是当前点的切线与水平线夹角的 tan 值。tan[0]为角 A的cos值 , tan[1]为角 A 的 sin 值。
对于 Math.atan2方法,是根据传入的x,y 坐标,计算与原点之间的夹角弧度。
即:弧度 A = Math.atan2(P.y,P.x),
P.x 和 P.y 又可以分别用 cosA 和 sinA 来表示。结合 getPosTan 方法我们就可以改成:弧度 A = Math.atan2(tan[1],tan[0])
角度 A = 弧度 A*180/Math.PI

案例 1:箭头在 path 圆转圈圈

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
32
33
34
35
36
37
38
39
40
41
private float currentValue = 0;     // 用于纪录当前的位置,取值范围[0,1]映射Path的整个长度

private float[] pos;                // 当前点的实际位置
private float[] tan;                // 当前点的tangent值,用于计算图片所需旋转的角度
private Bitmap mBitmap;             // 箭头图片
private Matrix mMatrix;             // 矩阵,用于对图片进行一些操作

private void init(Context context) {
    pos = new float[2];
    tan = new float[2];
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inSampleSize = 2;       // 缩放图片
    mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.arrow, options);
    mMatrix = new Matrix();
}

canvas.translate(mViewWidth / 2, mViewHeight / 2);      // 平移坐标系

Path path = new Path();                                 // 创建 Path

path.addCircle(0, 0, 200, Path.Direction.CW);           // 添加一个圆形

PathMeasure measure = new PathMeasure(path, false);     // 创建 PathMeasure

currentValue += 0.005;                                  // 计算当前的位置在总长度上的比例[0,1]
if (currentValue >= 1) {
  currentValue = 0;
}

measure.getPosTan(measure.getLength() * currentValue, pos, tan);        // 获取当前位置的坐标以及趋势

mMatrix.reset();                                                        // 重置Matrix
float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI); // 计算图片旋转角度

mMatrix.postRotate(degrees, mBitmap.getWidth() / 2, mBitmap.getHeight() / 2);   // 旋转图片
mMatrix.postTranslate(pos[0] - mBitmap.getWidth() / 2, pos[1] - mBitmap.getHeight() / 2);   // 将图片绘制中心调整到与当前点重合

canvas.drawPath(path, mDeafultPaint);                                   // 绘制 Path
canvas.drawBitmap(mBitmap, mMatrix, mDeafultPaint);                     // 绘制箭头

invalidate();

案例 2:模拟一个圆上的点和点的运动趋势

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class PathTan extends View implements View.OnClickListener {

    private Path mPath;
    private float[] pos;
    private float[] tan;
    private Paint mPaint;
    float currentValue = 0;
    private PathMeasure mMeasure;

    public PathTan(Context context) {
        super(context);
    }

    public PathTan(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPath = new Path();
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(4);
        mMeasure = new PathMeasure();
        mPath.addCircle(0, 0, 200, Path.Direction.CW);
        mMeasure.setPath(mPath, false);
        pos = new float[2];
        tan = new float[2];
        setOnClickListener(this);
    }

    public PathTan(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mMeasure.getPosTan(mMeasure.getLength() * currentValue, pos, tan);
        float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);

        canvas.save();
        canvas.translate(400, 400);
        canvas.drawPath(mPath, mPaint);
        canvas.drawCircle(pos[0], pos[1], 10, mPaint);
        canvas.rotate(degrees);
        canvas.drawLine(0, -200, 300, -200, mPaint);
        canvas.restore();
    }

    @Override
    public void onClick(View view) {
        ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                currentValue = (float) valueAnimator.getAnimatedValue();
                invalidate();
            }
        });
        animator.setDuration(3000);
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.start();
    }
}

image.png

getMatrix

boolean getMatrix(float distance, Matrix matrix, int flags) 获取指定长度的位置坐标及该点 Matrix,相当于对 getPosTan 的一个矩阵封装

这个方法是用于得到路径上某一长度的位置以及该位置的正切值的矩阵:

参数作用备注
返回值 (boolean)判断获取是否成功true 表示成功,数据会存入 matrix 中,false 失败,matrix 内容不会改变
distance距离 Path 起点的长度取值范围: 0 <= distance <= getLength
matrix根据 flags 封装好的 matrix会根据 flags 的设置而存入不同的内容
flags规定哪些内容会存入到 matrix 中可选择 POSITION_MATRIX_FLAG(位置)
ANGENT_MATRIX_FLAG(正切)

flags 选项可以选择 位置 或者 正切,两个选项都想选择,可以将两个选项之间用 | 连接起来

1
measure.getMatrix(distance, matrix, PathMeasure.TANGENT_MATRIX_FLAG | PathMeasure.POSITION_MATRIX_FLAG);

其实这个方法就相当于我们 getPosTan 例子中封装 matrix 的过程由 getMatrix 替我们做了,我们可以直接得到一个封装好到 matrix

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Path path = new Path();                                 // 创建 Path

path.addCircle(0, 0, 200, Path.Direction.CW);           // 添加一个圆形

PathMeasure measure = new PathMeasure(path, false);     // 创建 PathMeasure

currentValue += 0.005;                                  // 计算当前的位置在总长度上的比例[0,1]
if (currentValue >= 1) {
    currentValue = 0;
}

// 获取当前位置的坐标以及趋势的矩阵
measure.getMatrix(measure.getLength() * currentValue, mMatrix, PathMeasure.TANGENT_MATRIX_FLAG | PathMeasure.POSITION_MATRIX_FLAG);

mMatrix.preTranslate(-mBitmap.getWidth() / 2, -mBitmap.getHeight() / 2);   // <-- 默认为图片的左上角,将图片绘制中心调整到与当前点重合(注意:此处是前乘pre)

canvas.drawPath(path, mDeafultPaint);                                   // 绘制 Path
canvas.drawBitmap(mBitmap, mMatrix, mDeafultPaint);                     // 绘制箭头

invalidate();

2019-04-29-071808 (2).gif

  1. 对 matrix 的操作必须要在 getMatrix 之后进行,否则会被 getMatrix 重置而导致无效。
  2. 矩阵对旋转角度默认为图片的左上角,我们此处需要使用 preTranslate 调整为图片中心。
  3. pre(矩阵前乘) 与 post(矩阵后乘) 的区别

PathMeasure Ref

  • Path 之玩出花样 (PathMeasure)

GcsSloop https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/[08]Path_Play.md

贝塞尔曲线 Bezier

了解 Bezier 曲线

如何表示一条曲线,能够精确地控制曲线的路径,一直以来是一个很困难的问题。Bezier 曲线就是利用数学公式,能够精确描述一条我们想要的曲线。主要是由起始点、终止点和控制点三个部分组成,其中控制点是控制曲线的关键。

贝塞尔曲线是用一系列点来控制曲线状态的,我将这些点简单分为两类:

  1. 数据点: 确定曲线的起始和结束位置
  2. 控制点: 确定曲线的弯曲程度

一阶 Bezier 曲线

bezier_first.gif

一阶 Bezier 曲线,没有控制点的,仅有两个数据点 (A 和 B),是一条直线。

二阶 Bezier 曲线

  • 二阶贝塞尔曲线原理:

连接 AB BC,并在 AB 上取点 D,BC 上取点 E,使其满足条件,连接 DE,取点 F,使得:

这样获取到的点 F 就是贝塞尔曲线上的一个点

  • 二阶贝塞尔曲线动图:

其中的 p0 和 p2 分别是起始点和终止点,p1 即是控制点。在三个点形成的两条线段上,选取各自的起始位置然后向各自的终点位置移动,并且将两个点连接成一条辅助线,在这条辅助线上同样有一个点从起始位置移动到重点位置,这个点与 p0 点的连线就是一条二阶 Bezier 曲线。

三阶 Bezier 曲线

  • 三阶贝塞尔曲线原理:
    三阶曲线由两个数据点 (A 和 D),两个控制点 (B 和 C) 来描述曲线状态,如下: image.png

三阶曲线计算过程与二阶类似,

可以看出与二阶 Bezier 曲线类似,只不过是控制点变成了 2 个,形成的三条线段构成了两条辅助线,在这两条辅助线上又构造了一条辅助线,并且其运动的点与 p0 的连线构成一条三阶 Bezier 曲线。

Android 中的 Bezier

二阶 quardTo/rQuardTo

1
2
3
4
5
6
// 让Path恢复,养成良好的习惯
mPath.reset();
// 将Path移动到初始位置点
mPath.moveTo(mStartPointX, mStartPointY);
// quadTo即二阶Bezier曲线的Android API方法,前两个参数是控制点坐标,后两个参数是终止点坐标
mPath.quadTo(mFlagPointX, mFlagPointY, mEndPointX, mEndPointY);

三阶 cubicTo/rCubicTo

1
2
3
4
mPath.reset();
mPath.moveTo(mStartPointX, mStartPointY);
// 三阶Bezier曲线的API是cubicTo,同样地前四个参数对应两个控制点的坐标,后两个参数对应终止点坐标
mPath.cubicTo(mFlagPointOneX, mFlagPointOneY, mFlagPointTwoX, mFlagPointTwoY, mEndPointX, mEndPointY);

贝塞尔曲线使用实例

事先不知道曲线状态,需要实时计算时

天气预报气温变化的平滑折线图

显示状态会根据用户操作改变时

QQ 小红点,仿真翻书效果

一些比较复杂的运动状态 (配合 PathMeasure 使用)

复杂运动状态的动画效果

贝塞尔工具

Ref

https://blog.csdn.net/xiangzhihong8/article/details/78278931/

本文由作者按照 CC BY 4.0 进行授权