PathMeasure顾名思义,它是一个path(路径)的测量工具。我们可用通过它的一些API得到当前路径上某个点的坐标,还有夹角等信息,这些信息在自定义控件中都很适用。PathMeasure都会和一个或者多个Path结合使用。先看常用的API。
构造方法
//构造方法1
public PathMeasure() {
mPath = null;
native_instance = native_create(0, false);
}
//构造方法2
public PathMeasure(Path path, boolean forceClosed) {
mPath = path;
native_instance = native_create(path != null ? path.readOnlyNI() : 0,
forceClosed);
}
两个构造方法的区别就是,构造方法1中没有传递任何参数,而构造方法2中传递了我们要测量的path和一个boolean类型的变量值,其实最终都是调用底层jni的native_create方法,只不过是构造方法1构造出来的PathMeasure是没有关联Path对象的,我们要向使用PathMeasure进行测量,必须关联对应的Path。可以调用setPath方法。
public void setPath(Path path, boolean forceClosed) {
mPath = path;
native_setPath(native_instance,
path != null ? path.readOnlyNI() : 0,
forceClosed);
}
调用setPath方法,给PathMeasure关联一个Path,看到构造方法2中和setPath中都有一个boolean类型forceClosed的值,这个值啥意思呢,先看一段代码,这里以setPath的方法中的forceClosed方法进行讲解(和构造方法中一个意思):
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
path.lineTo(300,400);
//path.lineTo(200,400)
path.rLineTo(200,0);
pathMeasure = new PathMeasure();
pathMeasure.setPath(path,false);
Log.e("PathMeasureView","forceClosed = false and length="+pathMeasure.getLength());
pathMeasure.setPath(path,true);
Log.e("PathMeasureView","forceClosed = true and length="+pathMeasure.getLength());
//########代码块1 开始#######
// path.close(); //使path成为封闭的曲线
// pathMeasure.setPath(path,false);
// Log.e("PathMeasureView","forceClosed = true and path close of the length="+pathMeasure.getLength());
//########代码块1 结束#######
canvas.drawPath(path,paint);
}
我们可以看到下面一段打印的log
04-18 07:54:00.376 1566-1566/com.pathmeasure.demo E/PathMeasureView: forceClosed = false and length=700.0
04-18 07:54:00.376 1566-1566/com.pathmeasure.demo E/PathMeasureView: forceClosed = true and length=1340.3125
显示的效果图好打印结果如上所述,我们利用数学当中的勾股定理可以得到斜边OA = 500 ,线段AB = 200,那么当forceClose = false的时候,pathMeasure测量的结果位700 = OA+AB = 500+200,这个可以很好的理解。当我们将forceClose = true的时候,length = 1304.3125,可是原来path在界面上的显示效果还是一样。这里直接下结论:
forceClose = true 的时候,"相当于"将当前的Path设置为闭合状态 ,此时只会改变PathMeasure的测量结果,不会改变原来的path,也就是上面的forceClose = true的时候 length = 1340.3125 = OA +AB + OB 的长度,这里多了OB的长度。如果我们放开上面的"代码块1" 调用path.close()使path闭合(连接起点和终点),然后调用setPath(path,false方法,最终得到的测量结果和 forceClose = true的测量结果一致。
forClose = false,不测量闭合曲线的长度(起点到终点的连线线段长度不算)。
setPath方法
上文说道setPath方法中的forceClose参数的含义。已经很清晰了,调用PathMeasure的setPath方法将传入的Path与PathMeasure关联,需要注意的是:如果当前关联的Path改变的时候,想正确测量当前的Path的长度,必须再次调用PathMeasure的setPath方法,将改变后的path再次与PathMeasure关联。
getPostan方法
getPostan这个方法在自定义控件的时候,很实用,可以计算出距离Path起点distance距离的某一点的位置和方向:
/**
* Pins distance to 0 <= distance <= getLength(), and then computes the
* corresponding position and tangent. Returns false if there is no path,
* or a zero-length path was specified, in which case position and tangent
* are unchanged.
*
* @param distance The distance along the current contour to sample
* @param pos If not null, returns the sampled position (x==[0], y==[1])
* @param tan If not null, returns the sampled tangent (x==[0], y==[1])
* @return false if there was no path associated with this measure object
*/
public boolean getPosTan(float distance, float pos[], float tan[]) {
if (pos != null && pos.length < 2 ||
tan != null && tan.length < 2) {
throw new ArrayIndexOutOfBoundsException();
}
return native_getPosTan(native_instance, distance, pos, tan);
}
distance ,距离当前path起点的距离。它的范围是0 - pathMeasure.getLength() 的范围。
pos[ ] 是一个长度为2的数组,pos[0] 是当前点的X坐标,pos[1]是当前点的Y坐标。
tan[ ]是一个长度为2的数组,这个数组其实我们可以"理解为"表示是一个角度(单位圆上表示的角度)信息,就是该点在Path上的切线与X轴(正方向)的角度关系,该角度用弧度表示,大小在 -π -- π 之间我,我们在绘制的时候需要转换成对应的角度。 得到的角度信息为 degree = Math.atan2(tan[1],tan[0])*180°/π ,我们只需要记得这表示的是一个角度的信息,如果我们得到的角度位90°,那么自然tan(degree) 不存在,也就是我们可以理解为1/0 这个不存在 ,无穷大的值。 我的记忆就是tan[0]表示单位圆下该点的Y左边,tan[1]表示单位圆下该点的X坐标(我不知道为啥API里面写的是相反的,我感觉这样让我更好理解,不喜勿喷)
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawLine(0,getHeight()/2,getWidth(),getHeight()/2,paint);
canvas.drawLine(getWidth()/2,0,getWidth()/2,getHeight(),paint);
canvas.translate(getWidth()/2,getHeight()/2);
path.addCircle(0,0,200,Path.Direction.CW);
canvas.drawPath(path,paint);
pathMeasure.setPath(path,false);
pathMeasure.getPosTan(0,pos,tan);
Log.e("PathMeasureView","pos[0]="+pos[0]+" pos[1]="+pos[1]);
Log.e("PathMeasureView","tan[0]="+tan[0]+" tan[1]="+pos[1]);
double degree = Math.atan2(tan[1], tan[0]) * 180 / Math.PI;
Log.e("PathMeasureView","degree="+degree);
}
上面代码的效果如上图,打印的log如下:
04-18 22:04:07.002 2415-2415/com.pathmeasure.demo E/PathMeasureView: pos[0]=200.0 pos[1]=0.0
04-18 22:04:07.002 2415-2415/com.pathmeasure.demo E/PathMeasureView: tan[0]=0.0 tan[1]=1.0
04-18 22:04:07.002 2415-2415/com.pathmeasure.demo E/PathMeasureView: degree=90.0
上面图片和 日志借用一张图来说明问题:
我们上面的代码中设置的direction 为Path.Direction.CW(顺时针) 也就是我们上面的 (二分之一根号3 )/(1/2) = (根号3)....(PS数学公式难打,太麻烦,不好用)那么等于根号3的tan的值,我们可以用上面数学公式计算为60°,那么它与X周正向相交,得到的角度就为+60°,前面对称 得到的角度为150°,但是不与X轴正向相交那么就是负值(我是这么理解的,我觉得这样理解比较好,也符合上面Math.atan2计算出来的值的理解),如果方向换成Path.Direction.CCW(逆时针)那么与X轴负轴相交即为正角度,与X轴正轴相交为负角度。
看下面代码的打印结果
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawLine(0,getHeight()/2,getWidth(),getHeight()/2,paint);
canvas.drawLine(getWidth()/2,0,getWidth()/2,getHeight(),paint);
canvas.translate(getWidth()/2,getHeight()/2);
path.addCircle(0,0,200,Path.Direction.CCW);
canvas.drawPath(path,paint);
pathMeasure.setPath(path,false);
pathMeasure.getPosTan(1*pathMeasure.getLength()/3,pos,tan);
Log.e("PathMeasureView","pos[0]="+pos[0]+" pos[1]="+pos[1]);
Log.e("PathMeasureView","tan[0]="+tan[0]+" tan[1]="+tan[1]);
double degree = Math.atan2(tan[1], tan[0]) * 180 / Math.PI;
Log.e("PathMeasureView","degree="+degree);
}
04-18 23:26:08.849 3187-3187/? E/PathMeasureView: pos[0]=-141.41779 pos[1]=141.42493
04-18 23:26:08.849 3187-3187/? E/PathMeasureView: tan[0]=-0.7071247 tan[1]=-0.707089
04-18 23:26:08.849 3187-3187/? E/PathMeasureView: degree=-135.0014464869287
上面无论是顺时针还是逆时针,都是从单位圆与X轴正方向的交点作为起始点的。所以距离起点位置(顺时针绘制)的pathMesure.getLength()/3 长度的点,就是上面150°切线与X轴的夹角,由于与X轴负方向相交所以是负值。逆时针(Path.Direction.CCW)可以自行验证,也都可以这么理解。
接下来看一个讲解PathMeasure的博客或者文章基本都会讲到的实例(我也不例外):
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
path.reset(); //重绘的时候需要重置path
canvas.drawLine(0,getHeight()/2,getWidth(),getHeight()/2,paint);
canvas.drawLine(getWidth()/2,0,getWidth()/2,getHeight(),paint);
canvas.translate(getWidth()/2,getHeight()/2);
path.addCircle(0,0,200,Path.Direction.CW);
canvas.drawPath(path,paint);
pathMeasure.setPath(path,false);
percent+=0.005;
if(percent>=1){
percent = 0;
}
pathMeasure.getPosTan(0*pathMeasure.getLength(),pos,tan);
float degree = (float) (Math.atan2(tan[1], tan[0]) * 180 / Math.PI);
mMatrix.reset();
//将要绘制的图像的角度调整,始终沿着该点的切线方法
mMatrix.postRotate(degree,mBitmap.getWidth()/2,mBitmap.getHeight()/2);
//将即将要绘制的bitmap的中心点坐标 移动到与圆周的pos[]代表的点重合
mMatrix.postTranslate(pos[0]-mBitmap.getWidth()/2,pos[1]-mBitmap.getHeight()/2);
canvas.drawBitmap(mBitmap,mMatrix,paint); //绘制当前的bitmap对象
invalidate(); //重绘 让小箭头旋转起来 根据distance
}
这里说一说为啥要进行先旋转 后平移了,因为如果是先旋转的话,当前我们bitmap处在这个圆的中心位置上,旋转之后只会调整一个角度,然后再平移这样位置不会改变。看下面2张对比图。
图1 图2
上面图1的效果位只做了平移postTranslate的方法,我们看到效果是正确的,图2为在 图1postTranslate的基础之上,再在后面执行postRotation的操作,此时如果在旋转90度的话,就会出现图2的效果。(图1 图2 都是在距离path起点距离为0的点举例说明的)。
getMatrix方法
先看源码中的getMatrix方法
public boolean getMatrix(float distance, Matrix matrix, int flags) {
return native_getMatrix(native_instance, distance, matrix.native_instance, flags);
}
方法中有三个参数:
distance,表示距离path的起点distance长度 取值范围 0- pathMeasure.getLength()
matrix,将上面距离distance的点的位置,角度信息记录在需要传递的matrix中
flags,一个标志位,它的取值有2个,定义为PathMature里面的两个常量:
public static final int POSITION_MATRIX_FLAG = 0x01; // must match flags in SkPathMeasure.h
public static final int TANGENT_MATRIX_FLAG = 0x02;
如果只想将位置信息记录到第二个参数matrix中就只需要传递 flags = POSITION_MATRIX_FLAG ,如果只希望记录角度信息,传入的flags = POSITION_MATRIX_FLAG ,如果两者都想记录则flags = POSITION_MATRIX_FLAG | TANGENT_MATRIX_FLAG,那么上面的代码 同样的效果我们可以以下面的代码实现:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
path.reset(); //重绘的时候需要重置path
canvas.drawLine(0,getHeight()/2,getWidth(),getHeight()/2,paint);
canvas.drawLine(getWidth()/2,0,getWidth()/2,getHeight(),paint);
canvas.translate(getWidth()/2,getHeight()/2);
path.addCircle(0,0,200,Path.Direction.CW);
canvas.drawPath(path,paint);
pathMeasure.setPath(path,false);
percent+=0.005;
if(percent>=1){
percent = 0;
}
mMatrix.reset();
pathMeasure.getMatrix(percent*pathMeasure.getLength(),mMatrix,PathMeasure.POSITION_MATRIX_FLAG|PathMeasure.TANGENT_MATRIX_FLAG);
mMatrix.preTranslate(-mBitmap.getWidth()/2,-mBitmap.getHeight()/2);
canvas.drawBitmap(mBitmap,mMatrix,paint);
invalidate();
}
getSegment方法
PathMeasure的getSegment方法是获取path路径中的某一点保存到传入的Path变量中
public boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) {
}
float startD ,距离Path起点距离为startD 长度的点 为截取的path的起点,取值范围位0 - pathMeasure.getLength
float stopD,距离Path起点距离为stopD长度的点为要截取的path的终点,取值范围位0 - pathMeasure.getLength
Path dst , 将上面截取的两点之间的路径存放到dst中,注意这个变量时可以叠加的,也就是说,假如当前的dst已经存在某一条路径,此时如果调用getSegment截取的片段会以add的方式添加到dst中,截取到的path不会覆盖前面的在dst中已经存在的path。
boolean startWithMoveTo, 是否移动path的起点位置,稍后解释。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
path.reset();
canvas.drawLine(0,getHeight()/2,getWidth(),getHeight()/2,paint);
canvas.drawLine(getWidth()/2,0,getWidth()/2,getHeight(),paint);
canvas.translate(getWidth()/2,getHeight()/2);
Path dst = new Path();
dst.lineTo(-200,-200);
path.addRect(-100,-100,100,100,Path.Direction.CW);
pathMeasure.setPath(path,true);
//此时传递的startWithMoveTo = false
pathMeasure.getSegment(200,400,dst,false);
canvas.drawPath(path,paint);
canvas.drawPath(dst,mBlackPaint);
}
图1 startWithMoveTo = fasle 图2 startWithMoveTo = true
在代码中我们首先给Path dst 添加了一条(0,0) 到 (-200,-200)的直线,此时dst的路径的终点为(-200,-200)也即为 接下来在dst中 添加其他图形的起点,当startWithMoveTo = false意思就是不将当前 截取的path的起点移动到 我们需要截取path的起点位置,让两个点重合,这个时候就需要连接(-200,-200)到从PathMeasure中截取的path的起点连接一条线(在图中用黑色画笔 画的就是最终的dst)。当startWithMoveTo = true的时候,就是将当前绘制的起点移动到截取path的起点位置,也就产生了图2的效果。
同时也印证了上面的 截取到的path不会覆盖之前dst中已经存在的path(从原点位置到(-200,-200))。
nextContour方法
nextContour是跳转到到下一条path路径,看下面实例:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
path.reset();
canvas.drawLine(0,getHeight()/2,getWidth(),getHeight()/2,paint);
canvas.drawLine(getWidth()/2,0,getWidth()/2,getHeight(),paint);
canvas.translate(getWidth()/2,getHeight()/2);
path.addRect(-100,-100,100,100,Path.Direction.CW);
path.addCircle(0,0,200,Path.Direction.CW);
pathMeasure.setPath(path,true);
Log.e("PathMeasureView","length="+pathMeasure.getLength());
if(pathMeasure.nextContour()){
Log.e("PathMeasureView","nextContour length="+pathMeasure.getLength());
}
canvas.drawPath(path,paint);
}
查看打印日志
04-20 15:10:28.181 3638-3638/? E/PathMeasureView: length=800.0
04-20 15:10:28.181 3638-3638/? E/PathMeasureView: nextContour length=1256.1327
上面代码中给path添加了2条路径一条是矩形,一条是圆,当我们调用nextContour的时候,会从矩形路径跳转到圆路径,所以代码中的日志开始打印pathMeasure的length = 800,调用nextContour之后跳转到 圆所在的这条路径上,此时打印的长度就是圆的周长1256.1372(精度的原因)。
PathEffects
PathEffect一般是需要配合Paint来使用,会在Canvas的变换操作之前影响绘画的效果。它有几个子类:
CornerPathEffect 这个类的作用就是将Path的各个连接线段之间的夹角用一种更平滑的方式连接,类似于圆弧与切线的效果。
一般的,通过CornerPathEffect(float radius)指定一个具体的圆弧半径来实例化一个CornerPathEffect。
DashPathEffect 这个类的作用就是将Path的线段虚线化。构造函数为DashPathEffect(float[] intervals, float phase),其中intervals为虚线的ON和OFF数组,该数组的length必须大于等于2,phase为绘制时的偏移量
DiscretePathEffect 这个类的作用是离散Path的线段,使得在原来路径的基础上发生打散效果。一般的,通过构造DiscretePathEffect(float segmentLength,float deviation)来构造一个实例,其中,segmentLength指离散片段的最大的段长,deviation指定随机的最大偏离量。
PathDashPathEffect 这个类的作用是使用Path图形来填充当前的路径,其构造函数为PathDashPathEffect (Path shape, float advance, float phase,PathDashPathEffect.Stylestyle)。shape则是指填充图形,advance指每个图形间的间距,phase为绘制时的偏移量,style为该类自由的枚举值,有三种情况:Style.ROTATE、Style.MORPH和Style.TRANSLATE。其中ROTATE的情况下,线段连接处的图形转换以旋转到与下一段移动方向相一致的角度进行转转,MORPH时图形会以发生拉伸或压缩等变形的情况与下一段相连接,TRANSLATE时,图形会以位置平移的方式与下一段相连接。
ComposePathEffect 组合效果,这个类需要两个PathEffect参数来构造一个实例,ComposePathEffect (PathEffect outerpe,PathEffect innerpe),表现时,会首先将innerpe表现出来,然后再在innerpe的基础上去增加outerpe的效果
SumPathEffect 叠加效果,这个类也需要两个PathEffect作为参数SumPathEffect(PathEffect first,PathEffect second),但与ComposePathEffect不同的是,在表现时,会分别对两个参数的效果各自独立进行表现,然后将两个效果简单的重叠在一起显示出来
关于参数phase
在存在phase参数的两个类里,如果phase参数的值不停发生改变,那么所绘制的图形也会随着偏移量而不断的发生变动,这个时候,看起来这条线就像动起来了一样
文章demo地址:PathMeasureDemo
参考文章: