当前位置:首页 > 北京头条 > 北京经验

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

2020-05-27 22:07:56 来源:163健康
浏览量:

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.02019-02-22 15:48:33.787 7889-7889/com.easy.customeasytablayout.customviews D/yanjin: len=800.02019-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的方式,刚刚好。

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