欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

PathMeasure工具类简单使用

程序员文章站 2022-07-11 15:36:17
...

前言

路径动画是一种非常有用的动画实现方式,使用SVG可以很容易地实现路径动画效果,不过SVG要求的版本比较高很多应用目前还无法完全支持。Android系统中提供的PathMeasure工具类能够计算路径的长度,根据提供的路径区间获取路径的某一部分,还可以获取路径中的任意一点的位置和切线角度,下面就通过简单示例学习这个强大的工具。

实现效果

PathMeasure工具类简单使用

PathMeasure接口

方法名 意义
PathMeasure() 创建一个空的PathMeasure
PathMeasure(Path path, boolean forceClosed) 创建 PathMeasure 并关联一个指定的Path,需要注意这个Path一定要初始化过,否则只会测量空的Path对象,forceCloase是否将指定的Path闭合,只影响PathMeasure计算不会对原始Path产生任何影响
setPath(Path path, boolean forceClosed) 关联一个Path,forceCloase是否将指定的Path闭合,只影响PathMeasure计算不会对原始Path产生任何影响
isClosed() 路径Path是否闭合
getLength() 获取Path的长度,返回值是一个float类型的数字
nextContour() 如果Path里包含多个不相连的路径,跳转到下一个轮廓,可以通过这个方法遍历Path中所有独立路径
getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) 截取片段
getPosTan(float distance, float[] pos, float[] tan) 获取指定长度的位置坐标及该点切线值
getMatrix(float distance, Matrix matrix, int flags) 获取指定长度的位置坐标及该点Matrix

单路径动画

CheckBox是一种常见的用户控件,每次选中的时候都会在最前面的空格内展示“✔“图形,而且这个图形是自己绘制出来的,现在就使用PathMeasure来简单的模拟实现这个对号的绘制动画效果。

public class PathView extends AppCompatImageView {
    private Path mRight;
    private Path mTmpPath;
    private Paint mPaint;
    private PathMeasure mPathMeasure;
    private ValueAnimator mPathAnimator;

    public PathView(Context context) {
        this(context, null);
    }

    public PathView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PathView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mPaint = new Paint();
        mPaint.setColor(getResources().getColor(R.color.colorAccent));
        mPaint.setStrokeWidth(5);
        mPaint.setStyle(Paint.Style.STROKE);
        mRight = new Path();
        mTmpPath = new Path();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mRight.moveTo(0, h / 2);
        mRight.lineTo(w / 4, h);
        mRight.lineTo(w, 0);

        mPathMeasure = new PathMeasure(mRight, false);
        float length = mPathMeasure.getLength();
        mPathAnimator = ValueAnimator.ofFloat(0f, length);
        mPathAnimator.setDuration(1000);
        mPathAnimator.setInterpolator(new LinearInterpolator());
        mPathAnimator.setRepeatCount(ValueAnimator.INFINITE);
        mPathAnimator.setRepeatMode(ValueAnimator.RESTART);
        mPathAnimator.addUpdateListener(animation -> {
            float current = (float) animation.getAnimatedValue();
            mTmpPath.reset();
            mPathMeasure.getSegment(0f, current, mTmpPath, true);
            invalidate();
        });
        mPathAnimator.start();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPaint.setStrokeWidth(5);
        // 绘制外部边框
        canvas.drawRect(4, 4, getMeasuredWidth() - 4, getMeasuredHeight() - 4, mPaint);
        mPaint.setStrokeWidth(10);
        // 绘制内部的“✔”
        canvas.drawPath(mTmpPath, mPaint);
    }
}

在onSizeChanged方法中得到对号的长度,并且创建从0到对号长度的属性动画,在动画执行过程中通过getSegment获取从0到current的路径片段,最后刷新控件绘制截取到的对号片段,这样就实现了对号绘制效果。

多个路径绘制

除了方框类型的CheckBox还有圆形的CheckBox控件,这时候不但内部的对号需要绘制,外部的圆形路径也需要绘制,在一个Path内部有两个不相互连接的路径直接使用PathMeasure操作的是第一个路径,这里就是圆形路径,为了能够继续绘制第二个对号路径需要调用nextContour跳转到下一条路径。

public class CirclePathView extends AppCompatImageView {
    private Path mPath;
    private Paint mPaint;
    private PathMeasure mPathMeasure;
    private ValueAnimator mPathAnimator;
    private ValueAnimator mRightAnimator;
    private Path mTmpPath;
    private Path mRightPath;

    public CirclePathView(Context context) {
        this(context, null);
    }

    public CirclePathView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CirclePathView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mPaint = new Paint();
        mPaint.setColor(getResources().getColor(R.color.colorAccent));
        mPaint.setStrokeWidth(10);
        mPaint.setStyle(Paint.Style.STROKE);
        mPath = new Path();
        mTmpPath = new Path();
        mRightPath = new Path();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        float padding = CommonUtils.dp2px(4);
        float rightPadding = padding + CommonUtils.dp2px(2);

        // 添加圆形路径
        mPath.addCircle(w / 2, h / 2, Math.min(w / 2, h / 2) - padding, Path.Direction.CCW);
        // 添加对号路径
        mPath.moveTo(rightPadding, h / 2);
        mPath.lineTo(w / 3, h - rightPadding);
        mPath.lineTo(w - rightPadding, rightPadding);

        mPathMeasure = new PathMeasure(mPath, false);
        float length = mPathMeasure.getLength();
        mPathAnimator = ValueAnimator.ofFloat(0f, length);
        mPathAnimator.setDuration(1000);
        mPathAnimator.setInterpolator(new LinearInterpolator());
        mPathAnimator.addUpdateListener(animation -> {
            float current = (float) animation.getAnimatedValue();
            mTmpPath.reset();

            // 获取圆形的路径片段,重绘
            mPathMeasure.getSegment(0f, current, mTmpPath, true);
            invalidate();
        });
        mPathAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                // 圆形绘制完成之后,跳转到下一个路径,也就是对号路径
                mPathMeasure.nextContour();
                float length = mPathMeasure.getLength();
                mRightAnimator = ValueAnimator.ofFloat(0f, length);
                mRightAnimator.setDuration(1000);
                mRightAnimator.setInterpolator(new LinearInterpolator());
                mRightAnimator.addUpdateListener(anim -> {
                    float current = (float) anim.getAnimatedValue();
                    mRightPath.reset();
                    // 获取对号路径
                    mPathMeasure.getSegment(0f, current, mRightPath, true);
                    invalidate();
                });
                mRightAnimator.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        // 需要将对号路径清空
                        mRightPath.reset();
                        // 重新将mPath关联到PathMeasure,否则会使用之前的路径,
                        // 因为已经绘制了所有的路径,就会展示空白
                        mPathMeasure.setPath(mPath, false);
                        // 重新开始绘制
                        mPathAnimator.start();
                    }
                });
                mRightAnimator.start();
            }
        });
        mPathAnimator.start();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 绘制片段
        canvas.drawPath(mTmpPath, mPaint);
        canvas.drawPath(mRightPath, mPaint);
    }
}

上面的多路径绘制就是使用nextContour跳转到下一条路径,继续执行单条路径的绘制,不过在重新开始绘制需要将PathMeasure和Path重新绑定,否则PathMeasure会保持之前的nextContour状态,无法重新绘制。

延路径运动

PathMeasure的getPosTan方法可以获取任意长度处的位置和切线角度值,可以在属性动画中得到当前长度的位置和角度,再调整图片对象的位置和角度,这样就好像图片对象正沿着路径做运动。

public class FlightPathView extends AppCompatImageView {
    private Path mPath;
    private Path mTmpPath;
    private Paint mPaint;
    private PathMeasure mPathMeasure;
    private ValueAnimator mPathAnimator;
    private Bitmap mFlight;
    private float[] mPos;
    private float[] mTan;
    private Matrix mMatrix;

    public FlightPathView(Context context) {
        this(context, null);
    }

    public FlightPathView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FlightPathView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mPaint = new Paint();
        mPaint.setColor(getResources().getColor(R.color.colorAccent));
        mPaint.setStrokeWidth(10);
        mPaint.setStyle(Paint.Style.STROKE);
        mPath = new Path();
        mTmpPath = new Path();
        mFlight = BitmapFactory.decodeResource(getResources(), R.drawable.flight_ic);
        mPos = new float[2];
        mTan = new float[2];
        mMatrix = new Matrix();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        // 绘制圆形路径
        mPath.addCircle(w / 2, h / 2, Math.min(w / 2, h / 2) - CommonUtils.dp2px(20), Path.Direction.CCW);

        mPathMeasure = new PathMeasure(mPath, false);
        float length = mPathMeasure.getLength();
        mPathAnimator = ValueAnimator.ofFloat(0f, length);
        mPathAnimator.setDuration(2000);
        mPathAnimator.setInterpolator(new LinearInterpolator());
        mPathAnimator.setRepeatCount(ValueAnimator.INFINITE);
        mPathAnimator.setRepeatMode(ValueAnimator.RESTART);
        mPathAnimator.addUpdateListener(animation -> {
            float current = (float) animation.getAnimatedValue();
            mTmpPath.reset();
            // 获取当前长度的片段
            mPathMeasure.getSegment(0f, current, mTmpPath, true);
            // 获取当前长度的位置和角度
            mPathMeasure.getPosTan(current, mPos, mTan);
            mMatrix.reset(); // 重置Matrix

            float degrees = (float) (Math.atan2(mTan[1], mTan[0]) * 180.0 / Math.PI); // 计算图片旋转角度
            mMatrix.postRotate((degrees + 45f), mFlight.getWidth() / 2, mFlight.getHeight() / 2);   // 旋转图片
            mMatrix.postTranslate(mPos[0] - mFlight.getWidth() / 2, mPos[1] - mFlight.getHeight() / 2); // 移动图片

            invalidate();
        });
        mPathAnimator.start();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(mTmpPath, mPaint);
        canvas.drawBitmap(mFlight, mMatrix, mPaint);
    }
}

在onSizeChanged方法中首先通过getPosTan方法获取当前长度的位置和角度,位置是相对于控件坐标系的可以直接使用,mTan则需要通过反函数计算出弧度角再转换成角度值,由于飞机图片是45度旋转了所以后面要加上45度做调整。