前言
Path扮演着路径的角色,在绘制View起着非常重要的位置,而PathMeasure是对Path进行测量,通过使用PathMeasure可以更加方便的使用Path工具。网上都好多关于这方面的文章,在这里只是做个笔录,不好不要见怪。嘿嘿
Part 1、谈谈Path的使用
首先先分析方法
public class Path { /** * 空构造方法 */ public Path() { mNativePath = init1(); } /** * 重置Path */ public void reset() { isSimplePath = true; mLastDirection = null; if (rects != null) rects.setEmpty(); // We promised not to change this, so preserve it around the native // call, which does now reset fill type. final FillType fillType = getFillType(); native_reset(mNativePath); setFillType(fillType); } /** * 和reset一样,只不过这个会将FillType也清楚掉,但reset不会 */ public void rewind() { isSimplePath = true; mLastDirection = null; if (rects != null) rects.setEmpty(); native_rewind(mNativePath); } /** * Path和Path之间的运算方法 */ public boolean op(Path path, Op op) { return op(this, path, op); } /** * 得到填充的类型 */ public FillType getFillType() { return sFillTypeArray[native_getFillType(mNativePath)]; } /** * 设置Path的填充类型 */ public void setFillType(FillType ft) { native_setFillType(mNativePath, ft.nativeInt); } /** * 判断是否反向填充 */ public boolean isInverseFillType() { final int ft = native_getFillType(mNativePath); return (ft & FillType.INVERSE_WINDING.nativeInt) != 0; } /** * 计算Path所占用的空间以及位置,将信息存入bounds中,exact:是否精确测量 */ @SuppressWarnings({"UnusedDeclaration"}) public void computeBounds(RectF bounds, boolean exact) { native_computeBounds(mNativePath, bounds); } /** * 自动改变,取反 */ public void toggleInverseFillType() { int ft = native_getFillType(mNativePath); ft ^= FillType.INVERSE_WINDING.nativeInt; native_setFillType(mNativePath, ft); } /** * Path是否为空 */ public boolean isEmpty() { return native_isEmpty(mNativePath); } /** * 将画笔移动的坐标位置 */ public void moveTo(float x, float y) { native_moveTo(mNativePath, x, y); } /** * 和上面一样,只不过上面是绝对位置,这个是相对位置(相对于上一个点) */ public void rMoveTo(float dx, float dy) { native_rMoveTo(mNativePath, dx, dy); } /** * 在lineTo之前要先moveTo否则则将默认为从原点开始划线 */ public void lineTo(float x, float y) { isSimplePath = false; native_lineTo(mNativePath, x, y); } /** * 于lineTo相对,这个是相对位置 */ public void rLineTo(float dx, float dy) { isSimplePath = false; native_rLineTo(mNativePath, dx, dy); } /** * 二阶贝塞尔曲线 */ public void quadTo(float x1, float y1, float x2, float y2) { isSimplePath = false; native_quadTo(mNativePath, x1, y1, x2, y2); } /** * */ public void rQuadTo(float dx1, float dy1, float dx2, float dy2) { isSimplePath = false; native_rQuadTo(mNativePath, dx1, dy1, dx2, dy2); } /** * 三阶贝塞尔曲线 */ public void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) { isSimplePath = false; native_cubicTo(mNativePath, x1, y1, x2, y2, x3, y3); } /** * */ public void rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3) { isSimplePath = false; native_rCubicTo(mNativePath, x1, y1, x2, y2, x3, y3); } /** * 画弧线 */ public void arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo) { arcTo(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, forceMoveTo); } /** * 当调用close则将结束点和起始点连线 */ public void close() { isSimplePath = false; native_close(mNativePath); } /** * 绘制的方向 */ public enum Direction { CW (0), // 顺时针方向 CCW (1); //逆时针 Direction(int ni) { nativeInt = ni; } final int nativeInt; } /** * 提供了大量的add图形的方法(将更多的图片添加到Path路径便于设置方向、填充方式) */ public void addXXX(XXX) { } /** * 将Path进行偏移,偏移之后的结果存入dst中 */ public void offset(float dx, float dy, @Nullable Path dst) { if (dst != null) { dst.set(this); } else { dst = this; } dst.offset(dx, dy); } /** * 将Path进行偏移,偏移之后的结果写入path中 */ public void offset(float dx, float dy) { if (isSimplePath && rects == null) { // nothing to offset return; } if (isSimplePath && dx == Math.rint(dx) && dy == Math.rint(dy)) { rects.translate((int) dx, (int) dy); } else { isSimplePath = false; } native_offset(mNativePath, dx, dy); } }
其中里面有一个Path的运算和Path填充比较常用,下面来介绍下
(1)Path运算:
/** * Path和Path之间的运算 */ public enum Op { /** * path1中减去Path2剩下的部分 */ DIFFERENCE, /** * path1和path2相交的部分 */ INTERSECT, /** * 包含path1和path2部分 */ UNION, /** *包含path和path2但不包含相交的部分 */ XOR, /** * Path2减去Path1剩下的部分 */ REVERSE_DIFFERENCE }
为了更好的理解,下面来附上一张图
根据Path的运算我们可以实现一个八卦图的效果
效果~
实现起来非常简单,只需要利用上面的运算规则即可,这里就不多说,直接上代码。
canvas.translate(getWidth() / 2, getHeight() / 2); canvas.save(); for (int i = 0; i < 2; i++) { path1.addCircle(0, 0, 200, Path.Direction.CW); path2.addRect(-200, -200, 0, 200, Path.Direction.CW); path1.op(path2, Path.Op.INTERSECT);//去相交的区域 path2.reset(); path2.addCircle(0, -100, 100, Path.Direction.CCW); path1.op(path2, Path.Op.UNION);//去全部的区域 path2.reset(); path2.addCircle(0, 100, 100, Path.Direction.CW); path1.op(path2, Path.Op.DIFFERENCE);//取Path1减去path2的区域 canvas.drawPath(path1, paint); canvas.rotate(180, 0, 0); } canvas.restore(); paint.setShader(new RadialGradient(0, -100, 25, Color.WHITE, Color.BLACK, Shader.TileMode.MIRROR)); canvas.drawCircle(0, -100, 25, paint); paint.setShader(new RadialGradient(0, 100, 25, Color.BLACK, Color.WHITE, Shader.TileMode.MIRROR)); canvas.drawCircle(0, 100, 25, paint);
(2)Path的填充:
/** * Enum for the ways a path may be filled. */ public enum FillType { // these must match the values in SkPath.h /** * 非零环绕数规则 */ WINDING (0), /** *奇偶规则 */ EVEN_ODD (1), /** * 反非零环绕数规则 */ INVERSE_WINDING (2), /** * 反奇偶规则 */ INVERSE_EVEN_ODD(3); FillType(int ni) { nativeInt = ni; } final int nativeInt; }
关于上面的解释在网上有一种比较可靠
网址:http://blog.csdn.net/u013831257/article/details/51477575
奇偶规则:从任意位置p作一条射线, 若与该射线相交的图形边的数目为奇数,则p是图形内部点,否则是外部点。
非零环绕数规则:首先使图形的边变为矢量。将环绕数初始化为零。再从任意位置p作一条射线。当从p点沿射线方向移动时,对在每个方向上穿过射线的边计数,每当图形的边从右到左穿过射线时,环绕数加1,从左到右时,环绕数减1。处理完图形的所有相关边之后,若环绕数为非零,则p为内部点,否则,p是外部点。
接下来我们先了解一下两种判断方法是如何工作的。
奇偶规则(Even-Odd Rule)
这一个比较简单,也容易理解,直接用一个简单示例来说明。
在上图中有一个四边形,我们选取了三个点来判断这些点是否在图形内部。
P1: 从P1发出一条射线,发现图形与该射线相交边数为0,偶数,故P1点在图形外部。
P2: 从P2发出一条射线,发现图形与该射线相交边数为1,奇数,故P2点在图形内部。
P3: 从P3发出一条射线,发现图形与该射线相交边数为2,偶数,故P3点在图形外部。
非零环绕数规则(Non-Zero Winding Number Rule)
P1: 从P1点发出一条射线,沿射线防线移动,并没有与边相交点部分,环绕数为0,故P1在图形外边。
P2: 从P2点发出一条射线,沿射线方向移动,与图形点左侧边相交,该边从左到右穿过穿过射线,环绕数-1,最终环绕数为-1,故P2在图形内部。
P3: 从P3点发出一条射线,沿射线方向移动,在第一个交点处,底边从右到左穿过射线,环绕数+1,在第二个交点处,右侧边从左到右穿过射线,环绕数-1,最终环绕数为0,故P3在图形外部。
通常,这两种方法的判断结果是相同的,但也存在两种方法判断结果不同的情况,如下面这种情况:
注意图形线段的方向,就不详细解释了,用上面的方法进行判断即可。
通过上面的介绍进行验证
path.op(path1, ops[i - 1]); canvas.drawPath(path, paint);
效果~
对于FillType=EVENT_ODD的时候,CCW和CW效果是一样的,但对于WINDING就需要考虑的绘制的方向
leftCenterX = startX + smallWidth * (i % 2); leftCenterY = startY + smallHeight * (i / 2); path.setFillType(Path.FillType.WINDING); path.addCircle(centerX, centerY, raduis, Path.Direction.CCW); path.addCircle(centerX, centerY, centerRadios - 50, Path.Direction.CCW); canvas.drawPath(path, paint);
根据上面的规则来做一个环嵌套环的效果
Part 2、PathMeasure的使用
首先先分析方法
public class PathMeasure { private Path mPath; /** * 创建一个空的PathMeasure */ public PathMeasure() { mPath = null; native_instance = native_create(0, false); } /** * 用这个构造函数可创建一个空的PathMeasure,但是使用之前需要先调用setPath方法来与Path进行关联。 * 被关联的Path必须是已经创建的好的,如果关联之后Path的内容进行了更改则需要使用setPath方法重新进行关联 */ public PathMeasure(Path path, boolean forceClosed) { // The native implementation does not copy the path, prevent it from being GC‘d mPath = path; native_instance = native_create(path != null ? path.readOnlyNI() : 0, forceClosed); } /** * 用这个构造函数是创建一个PathMeasure并关联一个Path,其实和创建一个空的PathMeasure后调用setPath进行关联效果是一样的 * 同样被关联的Path也必须已经是创建好的,如果关联的Path内容进行了更改,则需要是用setPath方法重新关联。 * 第二个参数是用来确保Path闭合,如果设置为true,则不论之前是否闭合,都会自动闭合该Path(如果Path可以闭合的话) * 这里需要注意: * 1、不论forceClosed设置为何种状态都不会影响原有的状态,即Path与PathMeasure关联之后,之前的Path不会有任何的改变 * 2、forceClosed的设置状态可能会影响测量结果,如果Path未闭合但在与PathMeasure关联的时候设置了true,则测量的结果 * 可能会比Path实际的长度稍长一点,获取到是该Path闭合的状态 */ public void setPath(Path path, boolean forceClosed) { mPath = path; native_setPath(native_instance, path != null ? path.readOnlyNI() : 0, forceClosed); } /** * 获取Path的总长度 */ public float getLength() { return native_getLength(native_instance); } /** * 用于得到路径上某一长度位置以及该位置的正切值 * 返回值:判断是否获取成功 true表示成功,数据会存入pos和tan中 * 参数 * distance : 距离Path起点的长度 取值范围0<=distance<=getLength * pos : 该点的坐标值 * tan : 该点的正切值 */ 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); } public static final int POSITION_MATRIX_FLAG = 0x01; // must match flags in SkPathMeasure.h public static final int TANGENT_MATRIX_FLAG = 0x02; // must match flags in SkPathMeasure.h /** * 用于得到路径上某一长度的位置以及该位置的正切值矩阵 * 返回值:判断获取是否成功 * 参数 * 1、distance :距离起点的长度 * 2、matrix : 根据flags封装好的matrix,会根据flags的位置而存入不同的内容 * 3、flags : 规定哪些内容会存入到matrix中,可选择POSITION_MATRIX_FLAG(位置) ANGENT_MATRIX_FLAG(正切) */ public boolean getMatrix(float distance, Matrix matrix, int flags) { return native_getMatrix(native_instance, distance, matrix.native_instance, flags); } /** * 获取Path的一个片段 * 返回值:判断截取是否成功,true表示截取成功,结果存入dst中,false表示截取失败,不会存在dst中 * 参数 * startD:开始截取位置距离Path起点的长度,取值范围 0 <=startD<stopD<=path总长度 stopD:结束截取位置距离Path起点的长度,取值范围 0<=startD<stopD<=path总长度 * dst : 截取的Path将会添加到dst中,注意是添加不是替换 * startWithMove: 起始点是否使用moveTo,用于保证截取的Path第一个点位置不变 * true:保证截取片段不会发生变形 false : 保证截取片段的Path连续性 * 注意: * 1、如果startD、stopD的数值不在取值范围【0,getLength】内,或者startD==stopD则返回false,不会改变dst的内容 * 2、如果在Android4.4或者之前的版本,在默认开启硬件加速的情况下,更改了dst的内容后可能会出现问题,请在关闭 * 硬件加速或者给dst添加一个单个操作,例如dst.rLineTo(0,0) * 3、可以用一下的规则来判断startWithMoveTo的取值 */ public boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) { // Skia used to enforce this as part of it‘s API, but has since relaxed that restriction // so to maintain consistency in our API we enforce the preconditions here. float length = getLength(); if (startD < 0) { startD = 0; } if (stopD > length) { stopD = length; } if (startD >= stopD) { return false; } return native_getSegment(native_instance, startD, stopD, dst.mutateNI(), startWithMoveTo); } /** * 用来判断Path是否闭合,但是如果你在关联Path的时候设置了forceClosed在true的话,这个方法的返回值则一定为true */ public boolean isClosed() { return native_isClosed(native_instance); } /** * Path是可以由多条曲线构成的,但不论是getLength,getSegment或者是其它的方法,都只会在其中的第一条线段上运行, * 而这个nextContour就是用于跳转到下一条曲线的方法,如果跳转成功则返回true,如果跳转失败则返回false */ public boolean nextContour() { return native_nextContour(native_instance); } }
理论都介绍完了,来实现一个如下效果
效果~
思路:不断的去对矩形进行截取片段在绘制
Path dst1 = new Path(); pathMeasure3.getSegment(changeD, pathMeasure3.getLength(), dst1, true); canvas.drawPath(dst1, paint); Path dst2 = new Path(); pathMeasure3.getSegment(0, 100 - pathMeasure3.getLength() + changeD, dst2, true); canvas.drawPath(dst2, paint);
几行代码就搞定了一个动画效果,是不是很简单,为了更好的去了解getPostTan和getMatrix方法,给出如下效果
效果~
实现代码
PathMeasure pathMeasure = new PathMeasure(path, true); float[] pos = new float[2]; float[] tan = new float[2]; pathMeasure.getPosTan(distance, pos, tan); Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher); Matrix matrix = new Matrix(); //计算方位角 float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI); matrix.postRotate(degrees, 50, 50); matrix.postTranslate(pos[0] - 50, pos[1] - 50); canvas.drawBitmap(bitmap, matrix, null);
tips:
1、Math.atan2() : 与之比较的是Math.atan(),Math.atan的范围是-pi/2~pi/2之间,Math.atan2()是-pi~pi之间,得到的是弧度需要进一步进行转化为角度值
2、你也可以使用getMatrix方法来使用现成的矩阵,只不过这个矩阵是以左上角为原点