PathMeasure:路径动画飞机转圈的加载动画

PathMeasure这个东西还是挺神奇的,我们看到的许多酷炫的动画大多要依靠他,他就像一个计算器,你给他一个path,他还你路径总长、指定长度的终点坐标,路径上某一点的tan、sin、cos值等等。这次我们来看看怎么用它做一个飞机转圈的加载动画,效果如下图:

2.gif

先了解PathMeasure的一些方法:

一、初始化

他的初始化有两种,第一种直接new空的构造方法,得到实例后利用setPath传入路径,如:

PathMeasure p=new PathMeasure ();
p.setPath(path,true);

第二种,直接在构造时候传入path,

PathMeasure p=new PathMeasure (path,true);

我们看到true这个参数多次出现,他代表的是PathMeasure 是否闭合的参数,如果为true,那么不管path有没有闭合,PathMeasure 都会闭合,但是只会影响PathMeasure 对path的计算,而不会改变path本身。

二、getLength

顾名思义就是获取path在计算后的长度。下面我们利用getLength看看上面说的true是怎么影响计算的。我们先定义一个自定义view,如下:

public class MyView extends View {

    private Paint paint;

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

    public MyView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        paint = new Paint();
        paint.setColor(Color.BLACK);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(5);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(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 pathMeasure1=new PathMeasure(path,false);
        PathMeasure pathMeasure2=new PathMeasure(path,true);
        Log.d("yanjin","pathMeasure1的length="+pathMeasure1.getLength()+"--pathMeasure2的length="+pathMeasure2.getLength());

        canvas.drawPath(path,paint);
    }
}
length.png

展示效果如上图,我们打印的getLength在设置为true和false的时候,会有不同的数值,一个为300,一个为400,多出来的100,大家应该也知道在哪来的吧,哈哈哈哈。

三、nextContour

我们都知道,一个path就相当于一个集合,他可以不断地add很多不连续的路径PathMeasure只对连续的路径有效果,那么假如path里面有A/B/C三个不连续的线段,怎么计算他们的值呢?这里就用到了nextContour函数,简单的说他就是跳到下一个线段的作用。比如如下代码:

public class MyView2 extends View {
    private Paint paint;

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

    public MyView2(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public MyView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        paint = new Paint();
        paint.setColor(Color.BLACK);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(5);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        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.CW);
        canvas.drawPath(path,paint);

        PathMeasure pathMeasure=new PathMeasure(path,false);//已经闭合了,我们可以传false。
        do {
            float length = pathMeasure.getLength();
            Log.d("yanjin","len="+length);
        }while (pathMeasure.nextContour());

    }
}

输出的值为:

2019-02-22 15:48:33.787 7889-7889/com.easy.customeasytablayout.customviews D/yanjin: len=400.0
2019-02-22 15:48:33.787 7889-7889/com.easy.customeasytablayout.customviews D/yanjin: len=800.0
2019-02-22 15:48:33.787 7889-7889/com.easy.customeasytablayout.customviews D/yanjin: len=960.0

我们可以得出以下结论:
1、nextContour函数得到的path循序与我们path.add时顺序一样。
2、getLength针对的是当前线段,不是整个path。

四、getSegment函数

他的定义如下:

public boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)

getSegment是用来截取一段path的,通过startD与stopD设置起始点。然后将截取的path存到dst中,startWithMoveTo表示是否使用moveTo,将路径的新起点移动到结果path的起点,一般为true。
进过上面的介绍,我们可以先写一个常见的加载动画了,动画效果如下:


1.gif

这个很常见吧,下面来讲讲他的代码。
先写自定义CirclePathAnimView代码

public class CirclePathAnimView extends View {

    private float mAnimatorValue;
    private PathMeasure mPathMeasure;
    private Path mDevPath;
    private Paint mPaint;
    private ValueAnimator mValueAnimator;

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

    public CirclePathAnimView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CirclePathAnimView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setLayerType(LAYER_TYPE_SOFTWARE, null);//关闭硬件加速
        //初始化画笔
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(getResources().getDimension(R.dimen.dp_3));
        mPaint.setColor(getResources().getColor(R.color.colorPrimary));

        //画真正显示的path
        mDevPath = new Path();


        //开始动画,当然当前动画你可以单独写成一个方法
        mValueAnimator = ValueAnimator.ofFloat(0, 1);
        mValueAnimator.setInterpolator(new LinearInterpolator());
        mValueAnimator.setDuration(2000);
        mValueAnimator.setRepeatCount(-1);
        mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mAnimatorValue = (float) valueAnimator.getAnimatedValue();
                invalidate();
            }
        });
        mValueAnimator.start();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        int radius = 0;
        if (width >= height) {
            radius = height / 2 - height / 8;
        } else {
            radius = width / 2 - width / 8;
        }
        //绘制path
        //先画圆的path,但是这个圆只是用来计算
        Path circlePath = new Path();
        circlePath.addCircle(width / 2, height / 2, radius, Path.Direction.CW);


        //计算圆的path的长度
        mPathMeasure = new PathMeasure(circlePath, true);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        float length = mPathMeasure.getLength();
        float stop = length * mAnimatorValue;
        //在0到0.5以前,起点不变,0.5到1,起点开始向终点靠拢。
        float start = (float) (stop - ((0.5 - Math.abs(mAnimatorValue - 0.5)) * length));

        mDevPath.reset();
        mPathMeasure.getSegment(start, stop, mDevPath, true);
        canvas.drawPath(mDevPath, mPaint);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mValueAnimator.cancel();
        mValueAnimator = null;
    }
}

我们可以先看构造方法,我们先设置mPaint ,然后开启动画,其实这个动画可以另写一个方法,手动掉一下,这里为了方便就写在这了,可以看到动画的更新监听里面我们获取动画值之后,调用invalidate刷新界面,这样会重走onDraw方法,这里讲onDraw之前,先看看onMeasure。
onMeasure里面我们主要拿到控件自己的宽高,设置了一个圆形Path--》circlePath ,但是这个circlePath 并没有被画出来,他只是用来被截取的,mPathMeasure 存入这个circlePath 。
然后动画中每调用invalidate进入onDraw的时候,拿动画值mAnimatorValue*path总长得到当前终点,起点的话,我们采取在0到0.5以前,起点不变,0.5到1,起点开始向终点靠拢的算法获得起点。这样我们就能调用截取方法了

mPathMeasure.getSegment(start, stop, mDevPath, true);

截取后原本为空的mDevPath就有数据了,我们就可以把它画下来了。为了让他看起来更有意思,我们在Activity中对他整个空间进行旋转,

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main29);
        CirclePathAnimView circlePathAnimView = findViewById(R.id.view);

        ObjectAnimator objectAnimator=ObjectAnimator.ofFloat(circlePathAnimView,"rotation",0,360);
        objectAnimator.setRepeatCount(-1);
        objectAnimator.setInterpolator(new LinearInterpolator());
        objectAnimator.setDuration(2500);
        objectAnimator.start();
    }

就能看到上面的效果了。
说了半天,答应的飞机呢?这样,我先上代码。还是那个自定义View,我只是改了一点点代码。

public class CirclePathAnimView extends View {

    private float mAnimatorValue;
    private PathMeasure mPathMeasure;
    private Path mDevPath;
    private Paint mPaint;
    private ValueAnimator mValueAnimator;
    private Bitmap airplayBitmap;

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

    public CirclePathAnimView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CirclePathAnimView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setLayerType(LAYER_TYPE_SOFTWARE, null);//关闭硬件加速
        //初始化画笔
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(getResources().getDimension(R.dimen.dp_2));
        mPaint.setColor(getResources().getColor(R.color.colorPrimary));

        //飞机图片
        airplayBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.airplay);

        //画真正显示的path
        mDevPath = new Path();


        //开始动画,当然当前动画你可以单独写成一个方法
        mValueAnimator = ValueAnimator.ofFloat(0, 1);
        mValueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        mValueAnimator.setDuration(3000);
        mValueAnimator.setRepeatCount(-1);
        mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mAnimatorValue = (float) valueAnimator.getAnimatedValue();
                invalidate();
            }
        });
        mValueAnimator.start();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        int radius = 0;
        if (width >= height) {
            radius = height / 2 - height / 8;
        } else {
            radius = width / 2 - width / 8;
        }
        //绘制path
        //先画圆的path,但是这个圆只是用来计算
        Path circlePath = new Path();
        circlePath.addCircle(width / 2, height / 2, radius, Path.Direction.CW);


        //计算圆的path的长度
        mPathMeasure = new PathMeasure(circlePath, true);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        float length = mPathMeasure.getLength();
        float stop = length * mAnimatorValue;
        //在0到0.5以前,起点不变,0.5到1,起点开始向终点靠拢。
        float start = (float) (stop - ((0.5 - Math.abs(mAnimatorValue - 0.5)) * length));

        mDevPath.reset();
        mPathMeasure.getSegment(start, stop, mDevPath, true);
        canvas.drawPath(mDevPath, mPaint);

        Matrix matrix=new Matrix();
        mPathMeasure.getMatrix(stop,matrix,PathMeasure.POSITION_MATRIX_FLAG|PathMeasure.TANGENT_MATRIX_FLAG);
        matrix.preTranslate(-airplayBitmap.getWidth()/2,-airplayBitmap.getHeight()/2);

        canvas.drawBitmap(airplayBitmap,matrix,mPaint);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mValueAnimator.cancel();
        mValueAnimator = null;
    }
}

在构造方法中,我们先获取图片


airplay.png
airplayBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.airplay);

在onDraw中,我们把飞机画上去。

        Matrix matrix=new Matrix();
        mPathMeasure.getMatrix(stop,matrix,PathMeasure.POSITION_MATRIX_FLAG|PathMeasure.TANGENT_MATRIX_FLAG);
        matrix.preTranslate(-airplayBitmap.getWidth()/2,-airplayBitmap.getHeight()/2);
        canvas.drawBitmap(airplayBitmap,matrix,mPaint);

就这么简单。
看是这么简单,但是里面有个getMatrix函数我们必须要讲一讲。

五、getMatrix函数

getMatrix函数可以获得某一长度终点的坐标以及该坐标的正切值的矩阵。

public boolean getMatrix(float distance, Matrix matrix, int flags)

distance指的是path长度,
matrix指的是容器,计算后会把结果存进来。
flags指的是要存入哪些内容,POSITION_MATRIX_FLAG是位置信息,TANGENT_MATRIX_FLAG是切边信息。


微信图片_20190222165911.png

图片中箭头代表飞机,飞机没飞一点,就要调整角度,他的方向基本要与切线一样,那么根据图中所示,角a+角b=90度,角a=角c,所以飞机头要掉角c这么多度数,而getMatrix就是能获取这些正切值。结合画图也有传Matrix的方式,刚刚好。

有时间再更新个支付宝支付成功的动画,嘻嘻。
对了,不喜勿喷哦!,我的心脏很弱小的。

你可能感兴趣的:(PathMeasure:路径动画飞机转圈的加载动画)