PathMeasure工具类简单使用

前言

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

实现效果

PathMeasure工具类简单使用_第1张图片

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度做调整。

你可能感兴趣的:(Android学习,PathMeasure,轨迹动画)