(4.1.36.1)Graphics图形学解析补充篇:路径Path

本篇内容基本都来自参考文献,有兴趣的可自行阅读查看

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

在AndroidMenifest文件中application节点下添上 android:hardwareAccelerated=”false”以关闭整个应用的硬件加速。
更多请参考这里:Android的硬件加速及可能导致的问题

本次特地开了一篇详细讲解Path,为什么要单独摘出来呢,这是因为Path在2D绘图中是一个很重要的东西。

在前面我们讲解的所有绘制都是简单图形(如 矩形 圆 圆弧等),而对于那些复杂一点的图形则没法去绘制(如绘制一个心形 正多边形 五角星等),而使用Path不仅能够绘制简单图形,也可以绘制这些比较复杂的图形,另外,根据路径绘制文本和剪裁画布都会用到Path

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

以下方法中:xxxto是真的画出来一条线(moveTo除外),而addxxx仅是在路径中加入某个图形并不绘制出来

作用相关方法备注
移动起点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矩阵变换

一、基本操作

1.1 moveTo、 setLastPoint、 lineTo 和 close

moveTo、 setLastPoint、 close都无法直接看到效果,借助有具现化效果的lineTo才能让这些方法现出原形

//向目标点处直线绘制:默认点就是坐标原点O
public void lineTo (float x, float y)

// 移动下一次操作的起点位置:不影响之前的,只影响之后的操作
public void moveTo (float x, float y)

// 设置之前操作的最后一个点位置:会影响之前的,也会影响之后的操作
public void setLastPoint (float dx, float dy)

//闭合路径:用于连接当前最后一个点和最初的一个点(如果两个点不重合的话),最终形成一个封闭的图形
//close的作用是封闭路径,与连接当前最后一个点和第一个点并不等价。如果连接了最后一个点和第一个点仍然无法形成封闭图形,则close什么 也不做
public void close ()

我们还是直接看示例吧:
这里写图片描述

//1 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标注)

//2
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标注)

//3
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条线,使之形成一个封闭的图形。

1.2 addXxx与arcTo

addXXX方法只是在路径中加入,只有在调用canvas.drawPath(path,mPaint)后才能显现

// 第一类(基本形状)
//这一类就是在path中添加一个基本形状,基本形状部分和前面所讲的绘制基本形状并无太大差别,详情参考<(4.1.36)android Graphics 图形学解析>
// 圆形
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)
// 圆弧
//oval  圆弧的外切矩形。
//startAngle    开始角度
//sweepAngle    扫过角度。取值范围是 [-360, 360),不包括360,当 >= 360 或者 < -360 时将不会绘制任何内容, 对于360,你可以用一个接近的值替代,例如: 359.99
public void addArc (RectF oval, float startAngle, float sweepAngle)

// 第二类(Path),将两个Path合并成为一个
public void addPath (Path src)
//将src进行了位移之后再添加进当前path中,具体见下文示例
public void addPath (Path src, float dx, float dy)
public void addPath (Path src, Matrix matrix)

// 第三类(arcTo)
// 添加一个圆弧到path:添加一个圆弧到path,如果圆弧的起点和上次最后一个坐标点不相同,就连接两个点
public void arcTo (RectF oval, float startAngle, float sweepAngle)
//forceMoveTo: true 将最后一个点移动到圆弧起点,即不连接最后一个点与圆弧起点。 等价于 addArc 
//forceMoveTo:false 不移动,而是连接最后一个点与圆弧起点。 等价于arcTo 
public void arcTo (RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo)
  • addPath (Path src, float dx, float dy)示例:

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

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

Path path = new Path();
path.addRect(-200,-200,200,200, Path.Direction.CW);

Path src = new Path();
src.addCircle(0,0,100, Path.Direction.CW);
path.addPath(src,0,200);

mPaint.setColor(Color.BLACK);           // 绘制合并后的路径
canvas.drawPath(path,mPaint);

这里写图片描述

  • addArc 与 arcto
    这里写图片描述
anvas.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);

//1
path.addArc(oval,0,270);
// path.arcTo(oval,0,270,true);             // <-- 和上面一句作用等价

//2
path.arcTo(oval,0,270);
// path.arcTo(oval,0,270,false);             // <-- 和上面一句作用等价

canvas.drawPath(path,mPaint);

1.3 路径方向Path.Direction

Path.Direction用于指明路径的方向,在添加图形时确定闭合顺序(各个点的记录顺序),对图形的渲染结果有影响(是判断图形渲染的重要条件)
- CW clockwise 顺时针
- CCW counter-clockwise 逆时针

具体Path的影响还是看示例:
这里写图片描述

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

绘制一个矩形(仅绘制边线),实际上只需要进行四次lineTo操作就行了,也就是说,只需要知道4个点的坐标,然后使用moveTo到第一个点,之后依次lineTo就行了(从上面的测试可以看出,在实际绘制中也确实是这么干的)

  • 我们采用的是顺时针(CW),所以记录的点的顺序就是 A -> B -> C -> D. 最后一个点就是D,我们这里使用setLastPoint改变最后一个点的位置实际上是改变了D的位置。
  • 如果我们将顺时针改为逆时针(CCW),则记录点的顺序应该就是 A -> D -> C -> B, 再使用setLastPoint则改变的是B的位置

1.4 isEmpty、 isRect、isConvex、 set 和 offset

//判断path中是否包含内容
public boolean isEmpty ()

//判断path是否是一个矩形,如果是一个矩形的话,会将矩形的信息存放进参数rect中
public boolean isRect (RectF rect)

//将新的path赋值到现有path,大致相当于 path = src;
public void set (Path src)

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

1.5 rXxx方法

rMoveTo(float dx, float dy)

rLineTo(float dx, float dy)

rQuadTo(float dx1, float dy1, float dx2, float dy2) 

rCubicTo(float x1, float y1, float x2, float y2,
                         float x3, float y3)

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

譬如:
这里写图片描述

//1
//在这个例子中,先移动点到坐标(100,100)处,之后再连接 点(100,100) 到 (100,200) 之间点直线,非常简单,画出来就是一条竖直的线
Path path = new Path();
path.moveTo(100,100);
path.lineTo(100,200);
canvas.drawPath(path,mDeafultPaint);

//2
//在屏幕上原本是竖直的线变成了倾斜的线。这是因为最终我们连接的是 (100,100) 和 (200, 300) 之间的线段
//在使用rLineTo之前,当前点的位置在 (100,100) , 使用了 rLineTo(100,200) 之后,下一个点的位置是在当前点的基础上加上偏移量得到的,即 (100+100, 100+200) 这个位置
Path path = new Path();
path.moveTo(100,100);
path.rLineTo(100,200);
canvas.drawPath(path,mDeafultPaint);

1.6 computeBounds 计算边界

void computeBounds (RectF bounds, boolean exact)
  • 这个方法主要作用是计算Path所占用的空间以及所在位置
    • bounds 测量结果会放入这个矩形
    • exact 是否精确测量,目前这一个参数作用已经废弃,一般写true即可。

这里写图片描述

1.7 填充模式FillType

//设置填充规则
setFillType 
//获取当前填充规则
getFillType 
//判断是否是反向(INVERSE)规则
isInverseFillType   
//切换填充规则(即原有规则与反向规则之间相互切换)
toggleInverseFillType   

我们在之前的文章中了解到,Paint有三种样式,“描边” “填充” 以及 “描边加填充”,我们这里所了解到就是在Paint设置为后两种样式时不同的填充模式对图形渲染效果的影响

我们要给一个图形内部填充颜色,首先需要分清哪一部分是外部,哪一部分是内部,机器不像我们人那么聪明,机器是如何判断内外呢?

模式简介
EVEN_ODD奇偶规则
INVERSE_EVEN_ODD反奇偶规则
WINDING非零环绕数规则
INVERSE_WINDING反非零环绕数规则

PS:此处所有的图形均为封闭图形,不包括图形不封闭这种情况

  • 奇偶规则: 奇数表示在图形内,偶数表示在图形外

    • 从任意位置p作一条射线, 若与该射线相交的图形边的数目为奇数,则p是图形内部点,否则是外部点。
  • 非零环绕数规则: 若环绕数为0表示在图形外,非零表示在图形内

    • 先使图形的边变为矢量。将环绕数初始化为零。再从任意位置p作一条射线。当从p点沿射线方向移动时,对在每个方向上穿过射线的边计数,每当图形的边从右到左穿过射线时,环绕数加1,从左到右时,环绕数减1。处理完图形的所有相关边之后,若环绕数为非零,则p为内部点,否则,p是外部点

这里写图片描述

  • 奇偶规则
    • P1: 从P1发出一条射线,发现图形与该射线相交边数为0,偶数,故P1点在图形外部。
    • P2: 从P2发出一条射线,发现图形与该射线相交边数为1,奇数,故P2点在图形内部。
    • P3: 从P3发出一条射线,发现图形与该射线相交边数为2,偶数,故P3点在图形外部。
  • 非零环绕数规则
    • 从P1点发出一条射线,沿射线防线移动,并没有与边相交点部分,环绕数为0,故P1在图形外边。
    • P2: 从P2点发出一条射线,沿射线方向移动,与图形点左侧边相交,该边从左到右穿过穿过射线,环绕数-1,最终环绕数为-1,故P2在图形内部。
    • P3: 从P3点发出一条射线,沿射线方向移动,在第一个交点处,底边从右到左穿过射线,环绕数+1,在第二个交点处,右侧边从左到右穿过射线,环绕数-1,最终环绕数为0,故P3在图形外部

通常,这两种方法的判断结果是相同的,但也存在两种方法判断结果不同的情况,如下面这种情况:
这里写图片描述

1.8 布尔运算(API19)

布尔操作与我们中学所学的集合操作非常像,只要知道集合操作中等交集,并集,差集等操作,那么理解布尔操作也是很容易的

//对 调用path 和 path 执行布尔运算,运算方式由第二个参数指定,运算结果存入到调用path中。
boolean op (Path path, Path.Op op)
// 对 path1 和 path2 执行布尔运算,运算方式由第三个参数指定,运算结果存入到 调用path 中。
boolean op (Path path1, Path path2, Path.Op op)

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

如太极中的阴阳鱼,如果用贝塞尔曲线制作的话,可能需要六段贝塞尔曲线才行,而在这里我们可以用四个Path通过布尔运算得到,而且会相对来说更容易理解一点

逻辑名称类比说明示意图
DIFFERENCE差集Path1中减去Path2后剩下的部分这里写图片描述
REVERSE_DIFFERENCE差集Path2中减去Path1后剩下的部分这里写图片描述
INTERSECT交集Path1与Path2相交的部分这里写图片描述
UNION并集包含全部Path1和Path2这里写图片描述
XOR异或包含Path1与Path2但不包括两者相交的部分这里写图片描述

我们在这里给出一个阴阳鱼效果:
这里写图片描述

canvas.translate(mViewWidth / 2, mViewHeight / 2);

Path path1 = new Path();
Path path2 = new Path();
Path path3 = new Path();
Path path4 = new Path();

path1.addCircle(0, 0, 200, Path.Direction.CW);
path2.addRect(0, -200, 200, 200, Path.Direction.CW);
path3.addCircle(0, -100, 100, Path.Direction.CW);
path4.addCircle(0, 100, 100, Path.Direction.CCW);


path1.op(path2, Path.Op.DIFFERENCE);
path1.op(path3, Path.Op.UNION);
path1.op(path4, Path.Op.DIFFERENCE);

canvas.drawPath(path1, mDeafultPaint);

1.9 重置路径

这个两个方法应该何时选择呢?

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

重置Path有两个方法,分别是reset和rewind,两者区别主要有一下两点:

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

二、贝塞尔曲线

类型作用
数据点确定曲线的起始和结束位置
控制点确定曲线的弯曲程度

贝塞尔曲线是用一系列点来控制曲线状态的:

这里写图片描述

  • 一阶曲线是没有控制点的,仅有两个数据点(A 和 B),最终效果一个线段

2.1 二阶曲线

这里写图片描述

  • 二阶曲线由两个数据点(A 和 C),一个控制点(B)来描述曲线状态
    • 连接AB BC,并在AB上取点D,BC上取点E,使其满足条件:AD/AB = BE/BC
    • 连接DE,取点F,使得:AD/AB = BE/BC = DF/DE。 此时F点为目标点
      这里写图片描述
//x1,y1控制点
//x2,y2终点
 quadTo(float x1, float y1, float x2, float y2)

2.2 三阶曲线

  • 三阶曲线由两个数据点(A 和 D),两个控制点(B 和 C)来描述曲线状态

这里写图片描述

//x1,y1控制点A
//x2,y2控制点B
//x3,y3终点
cubicTo(float x1, float y1, float x2, float y2,
                        float x3, float y3)

2.3 降阶与升阶

三阶曲线相比于二阶曲线可以制作更加复杂的形状,但是对于高阶的曲线,用低阶的曲线组合也可达到相同的效果,就是传说中的降阶。因此我们对贝塞尔曲线的封装方法一般最高只到三阶曲线

类型释义变化
降阶在保持曲线形状与方向不变的情况下,减少控制点数量,即降低曲线阶数方法变得简单,数据点变多,控制点可能减少,灵活性变弱
升阶在保持曲线形状与方向不变的情况下,增加控制点数量,即升高曲线阶数方法更加复杂,数据点不变,控制点增加,灵活性变强

三、PathMeasure路径测量

Path 可以由多条曲线构成, getLength , getSegment 或者是其它方法,都只会在其中第一条线段上运行

PathMeasure是一个用来测量Path的类,我们看下它的主要函数:

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

3.1 构建

它构造方法如下:

//创建一个空的PathMeasure
PathMeasure ()
//创建 PathMeasure 并关联一个指定的Path(Path需要已经创建完成)
PathMeasure(Path path, boolean forceClosed)

其中:

  • 无参构造函数:
    • 用这个构造函数可创建一个空的 PathMeasure,但是使用之前需要先调用 setPath 方法来与 Path 进行关联
    • 被关联的 Path 必须是已经创建好的,如果关联之后 Path 内容进行了更改,则需要使用 setPath 方法重新关联
  • 有参构造函数
    • 创建一个 PathMeasure 并关联一个 Path(其实和创建一个空的 PathMeasure 后调用 setPath 进行关联效果是一样的)
    • 第二个参数forceClosed是用来确保 Path 闭合,如果设置为 true, 则不论之前Path是否闭合,都会自动闭合该 Path(如果Path可以闭合的话)

需要注意的是,在构造函数中:

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

3.2 setPath、 isClosed 和 getLength

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

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

  • getLength 用于获取 Path 的总长度

3.3 getSegment获取路径片段

//沿路径方向获取指定长度的路径片段
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被截取路径的起始点是否使用如果 startWithMoveTo 为 true, 则被截取出来到Path片段保持原状;如果 startWithMoveTo 为 false,则会将截取出来的 Path 片段的起始点移动到 dst 的最后一个点,以保证 dst 的连续性
  • 截取一般都是前闭后开
  • 如果 startD、stopD 的数值不在取值范围 [0, getLength] 内,或者 startD == stopD 则返回值为 false,不会改变 dst 内容。
  • 如果在安卓4.4或者之前的版本,在默认开启硬件加速的情况下,更改 dst 的内容后可能绘制会出现问题,请关闭硬件加速或者给 dst 添加一个单个操作,例如: dst.rLineTo(0, 0)

我们看一个例子:

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 中添加一条线段

// 将 Path 与 PathMeasure 关联
PathMeasure measure = new PathMeasure(path, false);
// 截取一部分 并使用 moveTo 保持截取得到的 Path 第一个点的位置不变
measure.getSegment(200, 600, dst, true);
//measure.getSegment(200, 600, dst, false);

canvas.drawPath(dst, mDeafultPaint);// 绘制 Path

这里写图片描述

3.4 nextContour跳到下一线段

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

  • 曲线的顺序与 Path 中添加的顺序有关。
  • getLength 获取到到是当前一条曲线分长度,而不是整个 Path 的长度。
  • getLength 等方法是针对当前的曲线(其它方法请自行验证)
PathMeasure measure = new PathMeasure(path, false);// 将Path与PathMeasure关联
float len1 = measure.getLength();// 获得第一条路径的长度
measure.nextContour();// 跳转到下一条路径
float len2 = measure.getLength(); // 获得第二条路径的长度

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该点沿路径切线方向与X轴的交点的正切值结果:当前点在曲线上的方向,使用 Math.atan2(tan[1], tan[0]) 获取到正切角的弧度值。
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 得值计算出图片旋转的角度,tan 是 tangent 的缩写,即中学中常见的正切, 其中tan[0]是邻边边长,tan[1]是对边边长,而Math中 atan2 方法是根据正切是数值计算出该角度的大小,得到的单位是弧度(取值范围是 -pi 到 pi),所以上面又将弧度转为了角度。
  2. 通过 Matrix 来设置图片对旋转角度和位移,这里使用的方法与前面讲解过对 canvas操作 有些类似,对于 Matrix 会在后面专一进行讲解,敬请期待。
  3. 页面刷新,页面刷新此处是在 onDraw 里面调用了 invalidate 方法来保持界面不断刷新,但并不提倡这么做,正确对做法应该是使用 线程 或者 ValueAnimator 来控制界面的刷新,关于控制页面刷新这一部分会在后续的 动画部分 详细讲解,同样敬请期待

这里写图片描述

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(正切)

其实该方法就是把3.5中构建的切线矩阵,直接获取出来,我们看下对应的代码;

// 获取当前位置的坐标以及趋势的矩阵
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();      
  1. 对 matrix 的操作必须要在 getMatrix 之后进行,否则会被 getMatrix 重置而导致无效。
  2. 矩阵对旋转角度默认为图片的左上角,我们此处需要使用 preTranslate 调整为图片中心。
  3. pre(矩阵前乘) 与 post(矩阵后乘) 的区别,此处请等待后续的文章或者自行搜索

四、Path & SVG

我们知道,用Path可以创建出各种个样的图形,但如果图形过于复杂时,用代码写就不现实了,不仅麻烦,而且容易出错,所以在绘制复杂的图形时我们一般是将 SVG 图像转换为 Path。

你说什么是 SVG?

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

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

这里写图片描述

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

参考文献

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值