Path&PathMeasure完全解析

来源:互联网 发布:在手机淘宝怎么删评价 编辑:程序博客网 时间:2024/05/18 17:42

前言

        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方法来使用现成的矩阵,只不过这个矩阵是以左上角为原点





1 0