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的方式,刚刚好。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,539评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,594评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,871评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,963评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,984评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,763评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,468评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,850评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,002评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,144评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,823评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,483评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,026评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,150评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,415评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,092评论 2 355

推荐阅读更多精彩内容