PathMeasure

PathMeasure

大部分内容来自《Android自定义控件开发入门与实战》一书

PathMeasure可以计算指定path的一些信息,比如路径总长、指定长度所对应的坐标点等
创建方式:

 public PathMeasure()
 public PathMeasure(Path path, boolean forceClosed)
  • forceClosed - forceClosed参数对绑定path不会产生任何影响,如果一个折线没有闭合,当forceClosedtrue时,PathMeasure计算的path是闭合的,但path本身绘制出来的是不会闭合的。forceClosed参数只对PathMeasure的测量结果有影响。如果一个折线,本身没有闭合,当forceClosedtrue时,PathMeasure的计算就会包含最后一段闭合的路径,与原来的path不同

一些方法
1.getLength()

Return the total length of the current contour, or 0 if no path is associated with this measure object.
获取计算的路径的长度

如下的例子,当设置forceClosed为不同的值时,可以看出区别

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
        
        canvas.translate(50, 50);

        Path path = new Path();
        path.moveTo(0,0);
        path.lineTo(0, 100);
        path.lineTo(100, 100);
        path.lineTo(100, 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());

        Paint paint = new Paint();
        paint.setColor(Color.BLACK);
        paint.setStrokeWidth(8);
        paint.setStyle(Paint.Style.STROKE);

        canvas.drawPath(path, paint);

    }

Log输出如下:

2021-07-14 14:15:05.530 23640-23640/com.example.pathmeasuredemo E/PaintView: forceClosed = false--->300.0
2021-07-14 14:15:05.530 23640-23640/com.example.pathmeasuredemo E/PaintView: forceClosed = true--->400.0

getLength
2.isClosed()

public boolean isClosed()

用于判断测量path时是否计算闭合。如果关联path的时候设置forceClosedtrue,则这个函数的返回值一定为true

3.nextContour()

public boolean nextContour()

path可以由多条曲线组成,但不论getLength()getSegment()还是其他函数,都只会针对其中第一条线段进行计算。
nextContour()就是用于跳转到下一条曲线的函数。如果跳转成功,则返回true,否则返回false

Paint paint = new Paint();
paint.setColor(Color.RED);
paint.setStrokeWidth(8f);
paint.setStyle(Paint.Style.STROKE);
canvas.translate(150, 150);

Path path = new Path();
path.addRect(-50, -50, 50, 50, Path.Direction.CW);
path.addRect(-100, -100, 100, 100, Path.Direction.CW);
path.addRect(-120, -120, 120, 120, Path.Direction.CCW);
canvas.drawPath(path, paint);

PathMeasure measure = new PathMeasure(path, false);
do {
    float len = measure.getLength();
    Log.e(TAG, "len = " + len);
} while (measure.nextContour());

nextContour

Log输出如下:

2021-07-14 16:54:59.315 28689-28689/com.example.pathmeasuredemo E/PaintView: len = 400.0
2021-07-14 16:54:59.315 28689-28689/com.example.pathmeasuredemo E/PaintView: len = 800.0
2021-07-14 16:54:59.315 28689-28689/com.example.pathmeasuredemo E/PaintView: len = 960.0

可见getLength()获取的是当前的曲线,而不是整个Path

4.getSegment()

public boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) 
  • startD - 开始截取位置距离Path起始点的长度
  • stopD - 结束截取位置距离Path起始点的长度
  • dst - 截取的Path将会被添加到dst中
  • startWithMoveTo - 起始点是否使用moveTo将路径的新起始点移到结果Path的起始点。通常设置为true

getSegment()可以截取整个Path中的某个片段

如下的例子,有个矩形
getSegment
截取其中的0-150这段路径,截取的路径线段将添加到dst路径中,并将dst路径绘制出来

public class GetSegmentView extends View {
    private Paint mPaint;

    public GetSegmentView(Context context) {
        super(context);
        setLayerType(LAYER_TYPE_SOFTWARE, null);
    }

    public GetSegmentView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        //禁用硬件加速功能
        setLayerType(LAYER_TYPE_SOFTWARE, null);

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(4);
        mPaint.setColor(Color.BLACK);

    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);

        canvas.translate(100, 100);

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

        Path dst = new Path();
        PathMeasure measure = new PathMeasure(path, false);
        measure.getSegment(0, 150, dst, true);
        canvas.drawPath(dst, mPaint);

    }
}

getSegment

这里是顺时针绘制的,原因是原来路径是Path.Direction.CW顺时针方向

修改为Path.Direction.CCW后的效果
CCW
如果dst路径不为空,即原来就有存在的路径,是什么效果呢?
如下dst原来就有一段从(0,0)(10,100)的线段

        canvas.translate(100, 100);

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

        Path dst = new Path();
        dst.lineTo(10, 100);
        PathMeasure measure = new PathMeasure(path, false);
        measure.getSegment(0, 150, dst, true);
        canvas.drawPath(dst, mPaint);

Path

可见,getSegment会将截取的Path片段添加到路径dst中,而不是替换dst中的内容

如果startWithMoveTofalse,会是什么样呢?

measure.getSegment(0, 150, dst, false);

path

如果startWithMoveTofalse,则会将截取出来的Path片段的起始点移动到dst的最后一个点,以保证dst路径的连续性

例子,使用PathMeasure实现路劲加载动画,如下的自定义view,GetSegmentView

public class GetSegmentView extends View {
    private Paint mPaint;
    private Path mDstPath;
    private Path mCirclePath;
    private PathMeasure mPathMeasure;
    private Float mCurAnimValue = 0.0f;

    public GetSegmentView(Context context) {
        super(context);
        setLayerType(LAYER_TYPE_SOFTWARE, null);
    }

    public GetSegmentView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        //禁用硬件加速功能
        setLayerType(LAYER_TYPE_SOFTWARE, null);

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(4);
        mPaint.setColor(Color.BLACK);

        mCirclePath = new Path();
        mCirclePath.addCircle(100, 100, 50, Path.Direction.CW);

        mPathMeasure =  new PathMeasure(mCirclePath, true);
        mDstPath = new Path();

        ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mCurAnimValue = (Float) animation.getAnimatedValue();
                invalidate();
            }
        });
        animator.setDuration(2000);
        animator.start();

    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);

        canvas.drawColor(Color.WHITE);

        float stop = mPathMeasure.getLength() * mCurAnimValue;

        mDstPath.reset(); //清空之前生成的路径
        mPathMeasure.getSegment(0, stop, mDstPath, true);
        canvas.drawPath(mDstPath, mPaint);

    }
}

路径动画
修改下getSegment方法中的startDstopD,可以实现另一种效果

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);

        canvas.drawColor(Color.WHITE);

        float length = mPathMeasure.getLength();
        float stop = length * mCurAnimValue;
        /**
         * 在进度小于0.5时,start=0;而在进度大于0.5时,start=2*mCurAnimValue - 1;
         */
        float start = (float) (stop - ((0.5 - Math.abs(mCurAnimValue - 0.5)) * length));

        mDstPath.reset(); //清空之前生成的路径
        mPathMeasure.getSegment(start, stop, mDstPath, true);
        canvas.drawPath(mDstPath, mPaint);

    }

路径动画
5.getPosTan()

public boolean getPosTan(float distance, float pos[], float tan[])

用于得到路径上某一长度的位置以及该位置的正切值

  • distance - 距离path起始点的长度,取值范围为[0, getLength]
  • pos[] - 该点的坐标值。pos[0]表示x坐标,pos[1]表示y坐标
  • tan[] - 该点的正切值。代表单位圆的坐标点(x,y),通过y/x可以得到对应点的正切值
    单位圆
    在上面圆圈加载路径动画的基础上,实现一个带箭头的加载动画,如下的GetPosTanView
public class GetPosTanView extends View {
    private Paint mPaint;
    private Path mDstPath;
    private Path mCirclePath;
    private PathMeasure mPathMeasure;
    private Float mCurAnimValue = 0.0f;
    private Bitmap mArrowBitmap;
    private float[] pos = new float[2];
    private float[] tan = new float[2];

    public GetPosTanView(Context context) {
        super(context);
        setLayerType(LAYER_TYPE_SOFTWARE, null);
    }

    public GetPosTanView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        //禁用硬件加速功能
        setLayerType(LAYER_TYPE_SOFTWARE, null);

        //箭头图片
        mArrowBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.arraw);

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(4);
        mPaint.setColor(Color.BLACK);

        mCirclePath = new Path();
        mCirclePath.addCircle(100, 100, 50, Path.Direction.CW);

        mPathMeasure =  new PathMeasure(mCirclePath, true);
        mDstPath = new Path();

        ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mCurAnimValue = (Float) animation.getAnimatedValue();
                invalidate();
            }
        });
        animator.setDuration(2000);
        animator.start();

    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);

        canvas.drawColor(Color.WHITE);

        float length = mPathMeasure.getLength();
        float stop = length * mCurAnimValue;
        /**
         * 在进度小于0.5时,start=0;而在进度大于0.5时,start=2*mCurAnimValue - 1;
         */
        float start = (float) (stop - ((0.5 - Math.abs(mCurAnimValue - 0.5)) * length));

        mDstPath.reset(); //清空之前生成的路径
        mPathMeasure.getSegment(start, stop, mDstPath, true);
        canvas.drawPath(mDstPath, mPaint);

        //旋转箭头图片,并绘制
        mPathMeasure.getPosTan(stop, pos, tan);
        //取得弧度值
        float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180 / Math.PI);
        Matrix matrix = new Matrix();
        //围绕中心点旋转指定的角度
        matrix.postRotate(degrees, mArrowBitmap.getWidth() / 2, mArrowBitmap.getHeight() / 2);
        // 将图片从(0,0)移动到当前路径的最前端
        matrix.postTranslate(pos[0],pos[1]);
        //matrix.postTranslate(pos[0] - mArrowBitmap.getWidth() / 2, pos[1] - mArrowBitmap.getHeight() / 2);
        canvas.drawBitmap(mArrowBitmap, matrix, mPaint);

    }
}

箭头动画
可以看到箭头虽然随着路径移动,但是有点偏差。原因在于,移动箭头图片时,是以图片左上角为起始点开始移动的,修改为如下的形式:

        // 将图片从(0,0)移动到当前路径的最前端
        //matrix.postTranslate(pos[0],pos[1]);
        matrix.postTranslate(pos[0] - mArrowBitmap.getWidth() / 2, pos[1] - mArrowBitmap.getHeight() / 2);

箭头动画

例子

1.支付成功的动画

public class AliPayView extends View {
    private Path mCirclePath, mDstPath;
    private Paint mPaint;
    private PathMeasure mPathMeasure;
    private Float mCurAnimValue;
    private int mCentX = 100;
    private int mCentY = 100;
    private int mRadius = 50;

    public AliPayView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setLayerType(LAYER_TYPE_SOFTWARE, null);

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(4);
        mPaint.setColor(Color.BLACK);

        mDstPath = new Path();
        mCirclePath = new Path();

        //圆圈
        mCirclePath.addCircle(mCentX, mCentY, mRadius, Path.Direction.CW);
        //对勾
        mCirclePath.moveTo(mCentX - mRadius / 2, mCentY);
        mCirclePath.lineTo(mCentX, mCentY + mRadius / 2);
        mCirclePath.lineTo(mCentX + mRadius / 2, mCentY - mRadius / 3);

        mPathMeasure = new PathMeasure(mCirclePath, false);

        /**
         * 这里有2条路径,在0-1之间画第一条路径,在1-2之间画第二条路径
         */
        ValueAnimator animator = ValueAnimator.ofFloat(0, 2);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            public void onAnimationUpdate(ValueAnimator animation) {
                mCurAnimValue = (Float) animation.getAnimatedValue();
                invalidate();
            }
        });
        animator.setDuration(4000);
        animator.start();
    }

    boolean mNext = false;

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.WHITE);

        if (mCurAnimValue < 1) {
            //画外圈的圆形
            float stop = mPathMeasure.getLength() * mCurAnimValue;
            mPathMeasure.getSegment(0, stop, mDstPath, true);
        } else {
            /**
             * 画对勾
             * 不要用mCurAnimValue == 1来判断圆是否已经画完,根据设置Interpolater的不同,中间值不一定输出1
             */
            if (!mNext) {
                mNext = true;
                mPathMeasure.getSegment(0, mPathMeasure.getLength(), mDstPath, true);
                mPathMeasure.nextContour();
            }
            float stop = mPathMeasure.getLength() * (mCurAnimValue - 1);
            mPathMeasure.getSegment(0, stop, mDstPath, true);
        }
        canvas.drawPath(mDstPath, mPaint);
    }
}

支付成功动画

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值