Android自定义控件学习笔记(四)

自定义控件系列的读书笔记,整理自下列资料,不代表博主个人观点 :GcsSloop/AndroidNote


六、Path基本操作

6.1 Path常用方法表

不包括 API21以上才添加的方法

作用 相关方法 备注
移动起点 moveTo 移动下一次操作的起点位置
设置终点 setLastPoint 重置当前path中最后一个点位置,如果在绘制之前调用,效果和moveTo相同
连接直线 lineTo 添加上一个点到当前点之间的直线到Path
闭合路径 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 矩阵变换

6.2 Path详解

请关闭硬件加速,以免引起不必要的问题!

6.2.1 Path作用

Path在2D绘图中是一个很重要的东西,使用Path不仅能够绘制简单图形,也可以绘制这些比较复杂的图形。另外,根据路径绘制文本和剪裁画布都会用到Path。

6.2.2 Path含义

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

另外路径有开放和封闭的区别。

图像 名称 备注
封闭路径 首尾相接形成了一个封闭区域
开放路径 没有首位相接形成封闭区域

6.2.3 Path使用方法详解

(1)第1组:moveTo、 setLastPoint、 lineTo 和 close

先创建一个通用的画笔:

Paint mPaint = new Paint();             // 创建画笔
mPaint.setColor(Color.BLACK);           // 画笔颜色 - 黑色
mPaint.setStyle(Paint.Style.STROKE);    // 填充模式 - 描边
mPaint.setStrokeWidth(10);              // 边框宽度 - 10

lineTo:

public void lineTo (float x, float y)

lineTo是指从某个点到参数坐标点之间连一条线,这里的某个点就是上次操作结束的点,如果没有进行过操作则默认点为坐标原点:

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

在示例中我们调用了两次lineTo,第一次由于之前没有过操作,所以默认点就是坐标原点O,结果就是坐标原点O到A(200,200)之间连直线(用蓝色圈1标注)。

第二次lineTo的时候,由于上次的结束位置是A(200,200),所以就是A(200,200)到B(200,0)之间的连线(用蓝色圈2标注)。

moveTo 和 setLastPoint:

// moveTo
public void moveTo (float x, float y)

// setLastPoint
public void setLastPoint (float dx, float dy)
方法名 简介 是否影响之前的操作 是否影响之后操作
moveTo 移动下一次操作的起点位置
setLastPoint 设置之前操作的最后一个点位置

moveTo示例代码:

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的示例:

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:

public void close ()

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

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

很明显,两个lineTo分别代表第1和第2条线,而close在此处的作用就算连接了B(200,0)点和原点O之间的第3条线,使之形成一个封闭的图形。

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

(2)第2组:addXxx与arcTo

这次内容主要是在Path中添加基本图形,重点区分addArc与arcTo。

第一类(基本形状)

// 圆形
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中添加一个基本形状,基本形状部分和前面所讲的绘制基本形状并无太大差别。

仔细观察一下第一类的方法,无一例外,在最后都有一个Path.Direction。Direction的意思是方向,趋势,是一个枚举(Enum)类型,里面只有两个枚举常量,如下:

类型 解释 翻译
CW clockwise 顺时针
CCW counter-clockwise 逆时针

它们的作用如下有:一是在添加图形时确定闭合顺序(各个点的记录顺序);二是对图形的渲染结果有影响(是判断图形渲染的重要条件) 。

先研究确定闭合顺序的问题,添加一个矩形:

canvas.translate(mWidth / 2, mHeight / 2);  // 移动坐标系到屏幕中心
Path path = new Path();
path.addRect(-200,-200,200,200, Path.Direction.CW);
canvas.drawPath(path,mPaint);

将上面代码的CW改为CCW再运行一次,会发现两次运行结果一模一样!

想要让它现出原形,就要用到刚刚学到的setLastPoint(重置当前最后一个点的位置)。

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);

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

理解了上面的原理之后,假设我们将顺时针改为逆时针(CCW),则记录点的顺序应该就是 A - D -> C -> B, 再使用setLastPoint则改变的是B的位置,如下:

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);

参数中点的顺序很重要!

第二类(Path)

// path
public void addPath (Path src)
public void addPath (Path src, float dx, float dy)
public void addPath (Path src, Matrix matrix)

这个相对比较简单,也很容易理解,就是将两个Path合并成为一个。

第三个方法是将src添加到当前path之前先使用Matrix进行变换。

第二个方法比第一个方法多出来的两个参数是将src进行了位移之后再添加进当前path中。

示例:

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);

首先我们新建的两个Path(矩形和圆形)中心都是坐标原点,我们在将包含圆形的path添加到包含矩形的path之前将其进行移动了一段距离,最终绘制出来的效果就如上面所示。

第三类(addArc与arcTo)

// addArc
public void addArc (RectF oval, float startAngle, float sweepAngle)
// arcTo
public void arcTo (RectF oval, float startAngle, float sweepAngle)
public void arcTo (RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo)

从名字就可以看出,这两个方法都是与圆弧相关的,作用都是添加一个圆弧到path中,但既然存在两个方法,两者之间肯定是有区别的:

名称 作用 区别
addArc 添加一个圆弧到path 直接添加一个圆弧到path中
arcTo 添加一个圆弧到path 添加一个圆弧到path,如果圆弧的起点和上次最后一个坐标点不相同,就连接两个点

可以看到addArc有1个方法(实际上是两个的,但另一个重载方法是API21添加的), 而arcTo有2个方法,其中一个最后多了一个布尔类型的变量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)

示例(addArc):

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):

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);
(3)第3组:isEmpty、 isRect、isConvex、 set 和 offset

isEmpty:

public boolean isEmpty ()

判断path中是否包含内容。

Path path = new Path();
Log.e("1",path.isEmpty()+"");
path.lineTo(100,100);
Log.e("2",path.isEmpty()+"");

log输出结果:

03-02 14:22:54.770 12379-12379/com.sloop.canvas E/1: true
03-02 14:22:54.770 12379-12379/com.sloop.canvas E/2: false

isRect:

public boolean isRect (RectF rect)

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

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

RectF rect = new RectF();
boolean b = path.isRect(rect);
Log.e("Rect","isRect:"+b+"| left:"+rect.left+"| top:"+rect.top+"| right:"+rect.right+"| bottom:"+rect.bottom);

log 输出结果:

03-02 16:48:39.669 24179-24179/com.sloop.canvas E/Rect: isRect:true| left:0.0| top:0.0| right:400.0| bottom:400.0

set:

public void set (Path src)

将新的path赋值到现有path。

canvas.translate(mWidth / 2, mHeight / 2);  // 移动坐标系到屏幕中心
canvas.scale(1,-1);                         // <-- 注意 翻转y坐标轴

Path path = new Path();                     // path添加一个矩形
path.addRect(-200,-200,200,200, Path.Direction.CW);
Path src = new Path();                      // src添加一个圆
src.addCircle(0,0,100, Path.Direction.CW);
path.set(src);                              // 大致相当于 path = src;

canvas.drawPath(path,mPaint);

offset:

public void offset (float dx, float dy)
public void offset (float dx, float dy, Path dst)

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

第二个方法中最后的参数dst是存储平移后的path的。

dst状态 效果
dst不为空 将当前path平移后的状态存入dst中,不会影响当前path
dst为空(null) 平移将作用于当前path,相当于第一种方法

示例:

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。

(4)rXxx方法

此类方法可以看到和前面的方法看起来很像,只是在前面多了一个r,r代表的是相对坐标。rXxx方法的坐标使用的是相对位置(基于当前点的位移),而之前方法的坐标是绝对位置(基于当前坐标系的坐标)。

举个例子:

Path path = new Path();
path.moveTo(100,100);
path.lineTo(100,200);
canvas.drawPath(path,mDeafultPaint);

改成r方法后:

Path path = new Path();
path.moveTo(100,100);
path.rLineTo(100,200);
canvas.drawPath(path,mDeafultPaint);

6.3 PathMeasure

常用方法如下:

返回值 方法名 释义
void setPath(Path path, boolean forceClosed) 关联一个Path
boolean isClosed() 是否闭合
float getLength() 获取Path的长度
boolean nextContour() 跳转到下一个轮廓
boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) 截取片段
boolean getPosTan(float distance, float[] pos, float[] tan) 获取指定长度的位置坐标及该点切线值
boolean getMatrix(float distance, Matrix matrix, int flags) 获取指定长度的位置坐标及该点Matrix

6.3.1 构造函数

(1)无参构造函数
PathMeasure ()

用这个构造函数可创建一个空的 PathMeasure,但是使用之前需要先调用 setPath 方法来与 Path 进行关联。被关联的 Path 必须是已经创建好的,如果关联之后 Path 内容进行了更改,则需要使用 setPath 方法重新关联

(2)有参构造函数
PathMeasure (Path path, boolean forceClosed)

用这个构造函数是创建一个 PathMeasure 并关联一个 Path, 其实和创建一个空的 PathMeasure 后调用 setPath 进行关联效果是一样的,同样,被关联的 Path 也必须是已经创建好的,如果关联之后 Path 内容进行了更改,则需要使用 setPath 方法重新关联

该方法有两个参数,第二个参数是用来确保 Path 闭合,如果设置为 true, 则不论之前Path是否闭合,都会自动闭合该 Path(如果Path可以闭合的话)。

在这里有两点需要明确:

  • 1、不论 forceClosed 设置为何种状态(true 或者 false), 都不会影响原有Path的状态,即 Path 与 PathMeasure 关联之后,之前的的 Path 不会有任何改变。
  • 2、forceClosed 的设置状态可能会影响测量结果,如果 Path 未闭合但在与 PathMeasure 关联的时候设置 forceClosed 为 true 时,测量结果可能会比 Path 实际长度稍长一点,获取到到是该 Path 闭合时的状态。

用一个例子来验证一下:

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如下:

forceClosed=false---->600.0
forceClosed=true----->800.0

绘制在界面上的效果如下:

通过上面的示例能验证以上两个问题,另外还有:

  • 1、将 Path 与两个的 PathMeasure 进行关联,并给 forceClosed 设置了不同的状态,之后绘制再绘制出来的 Path 没有任何变化,所以与 Path 与 PathMeasure进行关联并不会影响 Path 状态。
  • 2、可以看到,设置 forceClosed 为 true 的方法比设置为 false 的方法测量出来的长度要长一点,这是由于 Path 没有闭合的缘故,多出来的距离正是 Path 最后一个点与最开始一个点之间点距离。forceClosed 为 false 测量的是当前 Path 状态的长度, forceClosed 为 true,则不论Path是否闭合测量的都是 Path 的闭合长度。

6.3.2 setPath、 isClosed 和 getLength

setPath 是 PathMeasure 与 Path 关联的重要方法,效果和 构造函数 中两个参数的作用是一样的。

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

getLength 用于获取 Path 的总长度,在之前的测试中已经用过了。

6.3.3 getSegment

getSegment 用于获取Path的一个片段,方法如下:

boolean getSegment (float startD, float stopD, Path dst, boolean startWithMoveTo)

方法各参数释义:

参数 作用 备注
返回值(boolean) 判断截取是否成功 true 表示截取成功,结果存入dst中,false 截取失败,不会改变dst中内容
startD 开始截取位置距离 Path 起点的长度 取值范围: 0 <= startD < stopD <= Path总长度
stopD 结束截取位置距离 Path 起点的长度 取值范围: 0 <= startD < stopD <= Path总长度
dst 截取的 Path 将会添加到 dst 中 注意: 是添加,而不是替换
startWithMoveTo 起始点是否使用 moveTo 用于保证截取的 Path 第一个点位置不变
  • 如果 startD、stopD 的数值不在取值范围 [0, getLength] 内,或者 startD == stopD 则返回值为 false,不会改变 dst 内容。
  • 如果在安卓4.4或者之前的版本,在默认开启硬件加速的情况下,更改 dst 的内容后可能绘制会出现问题,请关闭硬件加速或者给 dst 添加一个单个操作,例如: dst.rLineTo(0, 0)

看看这个方法如何使用:

创建了一个 Path, 并在其中添加了一个矩形,现在想截取矩形中的一部分,就是下图中红色的部分。

代码如下:

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

结果如下:

从上图可以看到我们成功到将需要到片段截取了出来,然而当 dst 中有内容时会怎样呢?

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

结果如下:

从上面的示例可以看到 dst 中的线段保留了下来,可以得到结论:被截取的 Path 片段会添加到 dst 中,而不是替换 dst 中到内容。

前面两个例子中 startWithMoveTo 均为 true, 如果设置为false会怎样呢?

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

结果如下:

从该示例我们又可以得到一条结论:如果 startWithMoveTo 为 true, 则被截取出来到Path片段保持原状,如果 startWithMoveTo 为 false,则会将截取出来的 Path 片段的起始点移动到 dst 的最后一个点,以保证 dst 的连续性。

6.3.4 nextContour

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

如下,我们创建了一个 Path 并使其中包含了两个闭合的曲线,内部的边长是200,外面的边长是400,现在我们使用 PathMeasure 分别测量两条曲线的总长度。

代码:

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,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);

log输出结果:

len1=800.0
len2=1600.0

通过测试,我们可以得到以下内容:

  • 1、曲线的顺序与 Path 中添加的顺序有关。
  • 2、getLength 获取到到是当前一条曲线分长度,而不是整个 Path 的长度。
  • 3、getLength 等方法是针对当前的曲线(其它方法请自行验证)。

6.3.5 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 是用来判断 Path 上趋势的,即在这个位置上曲线的走向,请看下图示例,注意箭头的方向:

点击这里下载箭头图片

可以看到 上图中箭头在沿着 Path 运动时,方向始终与 Path 走向保持一致,保持方向主要就是依靠 tan

下面来看看代码是如何实现的,首先需要定义几个必要的变量:

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();                                                           // 重绘页面

核心要点:

  • 1、图片需要旋转的角度应该跟圆上改点的切线斜率有关,而切线夹角的tan值可以通过getPosTan得到,而Math中 atan2 方法是根据正切是数值计算出该角度的大小,得到的单位是弧度(取值范围是 -pi 到 pi),所以上面又将弧度转为了角度。
  • 2、通过 Matrix 来设置图片对旋转角度和位移,这里使用的方法与前面讲解过对 canvas操作有些类似。
  • 3、页面刷新,页面刷新此处是在 onDraw 里面调用了 invalidate 方法来保持界面不断循环刷新,但并不提倡这么做,正确对做法应该是使用 线程 或者 ValueAnimator 来控制界面的刷新。

6.3.6 getMatrix

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

boolean getMatrix (float distance, Matrix matrix, int flags)

各参数释义:

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

其实这个方法就相当于在前一个例子中封装 matrix 的过程,上面的过程由 getMatrix 来做了,可以直接得到一个封装好到 matrix

最后到 flags 选项可以选择 位置 或者 正切,如果两个选项都想选择,可以将两个选项之间用 | 连接起来,如下:

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

试试将上面都例子中 getPosTan 替换为 getMatrix, 这样一来就会显得简单很多:

具体绘制:

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();                                                           // 重绘页面

使用 getMatrix 方法的确可以节省一些代码,不过这里依旧需要注意一些内容:

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

6.4 Path & SVG

当图形过于复杂时,用代码写就不现实了,在绘制复杂的图形时一般是将 SVG 图像转换为 Path。

SVG 是一种矢量图,内部用的是 xml 格式化存储方式存储这操作和数据,可以将 SVG 看作是 Path 的各项操作简化书写后的存储格式。

Path 和 SVG 结合通常能诞生出一些奇妙的东西,如下:

该图片来自这个开源库 :PathView
SVG 转 Path 的解析可以用这个库: AndroidSVG


6.5 Path使用技巧

先放一个效果图,然后分析一下实现过程:

这是一个搜索的动效图,通过分析可以得到它应该有四种状态,分别如下:

状态 概述
初始状态 初始状态,没有任何动效,只显示一个搜索标志 🔍
准备搜索 放大镜图标逐渐变化为一个点
正在搜索 围绕这一个圆环运动,并且线段长度会周期性变化
准备结束 从一个点逐渐变化成为放大镜图标

这些状态是有序转换的,转换流程以及转换条件如下:

6.5.1 Path 划分

为了制作对方便,此处整个动效用了两个 Path, 一个是中间对放大镜, 另一个则是外侧的圆环,将两者全部画出来是这样的。

其中 Path 的走向要把握好,如下(只是一个放大镜,并不是♂):

其中圆形上面的点可以用 PathMeasure 测量,无需计算。

6.5.2 动画状态与时间关联

此处使用的是 ValueAnimator,它可以将一段时间映射到一段数值上,随着时间变化不断的更新数值,并且可以使用插值器来控制数值变化规律。

6.5.3 具体绘制

绘制部分是根据 当前状态以及从 ValueAnimator 获得的数值来截取 Path 中合适的部分绘制出来。

6.5.4 最终效果

6.5.5 源码

戳这里查看源码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容