一款炫酷Loading動畫--載入成功

簡單介紹

昨天在簡書上看到一篇文章。介紹了一個載入動畫的實現過程
一款Loading動畫的實現思路(一)
僅僅惋惜原動畫是IOS上製做的。而看了一下。做者的實現思路比較複雜,因而趁着空暇寫了一個Android版本號。這篇文章將給你們介紹一下實現過程。html


首先讓咱們來看一下動畫效果
這裏寫圖片描寫敘述


java

動畫結構分析

從上面的gif圖中可以看到,這個載入動畫有成功,失敗兩種狀態,由於Gif速度比較快,咱們再來分別看一張慢圖android

一、成功狀態載入動畫

這裏寫圖片描寫敘述
成功動畫的狀態轉移描寫敘述例如如下:git

一、載入過程。畫藍色圓環,當進度爲100%時,圓環完畢
二、從右側拋出藍色小方塊。小方塊沿着曲線到達圓環正上方
三、藍色小方塊下落。下落過程當中,逐漸變長。當方塊與圓圈接觸時,進入圓環的部分變粗。同一時候圓環逐漸被擠壓,變成橢圓形
四、方塊底端到達圓環中心後,發出三個分叉向圓周延伸,同一時候橢圓被撐大。逐漸恢復回圓形
五、圓環變綠色畫出綠色勾√github

整個過程可以說是比較複雜的,甚至對照原動畫。事實上另外一些細節我沒有去實現。只是接下來我爲你們逐個分解每個過程是怎麼實現的。而且並不難理解canvas

每個小過程組合起來,就是一款炫酷動畫,但願你們都有信心去了解它。markdown




本身定義View,依據進度繪製圓形

首先咱們來實現第一個過程。圓環的繪製。
在動畫效果中。圓環的完整程度。是依據實際的進度來衡量的,當載入完畢。整個圓就畫好了。
因此咱們本身定義一個View控件。在其提供了一個setProgress()方法來給使用者設置進度ide

public class SuperLoadingProgress extends View {
    /** * 當前進度 */
    private int progress = 0;
    /** * 最大進度 */
    private static final int maxProgress = 100;

    ....
    public void setProgress(int progress) {
        this.progress = Math.min(progress,maxProgress);
        postInvalidate();
        if (progress==0){
            status = 0;
        }
    }
    ...
   }

有了這個進度之後,咱們就調用postInvalidate()去讓控件重繪,事實上就是觸發了其ondraw()方法。而後咱們就再ondraw()方法裏面。繪製圓弧
對於圓弧的繪製。相信你們都不會陌生(陌生也沒有關係。由於很是easy),僅僅要調用一個canvas.drawArc()方法就可以了。
但是我要細緻觀察這裏的圓形效果。在單獨來看三張圖post

圓弧起始狀態

這裏寫圖片描寫敘述

圓弧運動狀態

這裏寫圖片描寫敘述

圓弧終於狀態

這裏寫圖片描寫敘述

可以看到,首先圓弧有必定的起始角度。咱們知道。在Android座標系中,0度事實上是指水平向右開始的
也就是起點的起始角度。事實上是-90度終點的起始角度,事實上-150度動畫

而整個過程當中。
起點:-90度,逆時針旋轉270度。最後回到0度位置
終點:-150度。與起點相差60度。最後相差360度,與起點重合

因此當progress=1。也就是動畫完畢時。起點會減去270度,那麼相應每個progress
起點的位置應該是

-90-270*progress

當progress=1,終點和起點相差360度。而一開始就相差60度,因此整個過程就是多相差了300度,那麼相應每個progress。終點和起點應該相差

-(60+precent*300)

依據上面的結論。咱們獲得圓弧的詳細繪製方式例如如下:

/** * 起始角度 */
    private static final float startAngle = -90;
    @Override
    protected void onDraw(Canvas canvas) {
        ...
        float precent = 1.0f*progress/maxProgress;//當前完畢百分比
        //mRectF是表明整個view的範圍
        canvas.drawArc(mRectF, startAngle-270*precent, -(60 + precent*300), false, circlePaint);
    }



圓環完畢,拋出小方塊

在圓環繪製完畢之後,會拋出一個小方塊。小方塊沿曲線運動到圓環正上方,實際整個曲線,是一段圓弧
咱們來看下圖

方塊運動狀態

這裏寫圖片描寫敘述

運動狀態分析圖

這裏寫圖片描寫敘述
從圖中可以看出,方塊運動的終點,距離圓心爲2R
若是運動軌跡是某個圓的一段弧,那麼依據勾股定理有例如如下方程

(X+R)^2 + (2R)^2 = (X+2R)^2

解得X=R/2(事實上也很是easy解,就是勾三股四玄五)
若是咱們但願方塊在500ms內從起點運動到終點。那麼咱們就需要提供一個計時器,告訴咱們現在運動了多少毫秒。而後依據這個時間,計算出方塊當前位置
另外,由於方塊自己有必定的長度。所以方塊也有本身的起始端和末端

但是二者的運動軌跡是同樣的,僅僅是前後不一樣

//拋出動畫
        endAngle = (float) Math.atan(4f/3);
        mRotateAnimation = ValueAnimator.ofFloat(0f, endAngle*0.9f );
        mRotateAnimation.setDuration(500);
        mRotateAnimation.setInterpolator(new AccelerateDecelerateInterpolator());
        mRotateAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                curSweepAngle = (float) animation.getAnimatedValue();//運動了多少角度
                invalidate();
            }
        });

每次得到新角度。咱們就去又一次繪製方塊的位置:

/** * 拋出小方塊 * @param canvas */
    private void drawSmallRectFly(Canvas canvas){
        canvas.save();
        canvas.translate(radius / 2 + strokeWidth, 2 * radius + strokeWidth);//將座標移動到大圓圓心
        float bigRadius = 5*radius/2;//大圓半徑
        //方塊起始端座標
        float x1 = (float) (bigRadius*Math.cos(curSweepAngle));
        float y1 = -(float) (bigRadius*Math.sin(curSweepAngle));
        //方塊末端座標
        float x2 = (float) (bigRadius*Math.cos(curSweepAngle+0.05*endAngle+0.1*endAngle*(1-curSweepAngle/0.9*endAngle)));//
        float y2 = -(float) (bigRadius*Math.sin(curSweepAngle+0.05*endAngle+0.1*endAngle*(1-curSweepAngle/0.9*endAngle)));
        canvas.drawLine(x1, y1, x2, y2, smallRectPaint);//小方塊。事實上是一條直線
        canvas.restore();        
        canvas.drawArc(mRectF, 0, 360, false, circlePaint);//藍色圓環
    }



拋出完畢。方塊下落

可以說下落過程,是整個動畫中最複雜的過程了。包含方塊下落。圓環擠壓,方塊變粗三個過程,整個過程,從方塊下落開始,到方塊底部究竟圓心
這裏寫圖片描寫敘述
首先是方塊的下落,這個easy理解,方塊會逐漸變長。由於在一樣時間內,起始端和末端運動的距離不同
咱們拿末端做爲樣例,這裏要使用到一個知識。就是P**ath路徑類**
這是Android提供的一個類。表明咱們制定的一段路徑實例。對於方塊末端來講,其運動的路徑就是從頂部,到圓心

Path downPath1 = new Path();//起始端路徑
    downPath1.moveTo(2*radius+strokeWidth,strokeWidth);
    downPath1.lineTo(2 * radius+strokeWidth, radius+strokeWidth);
    Path downPath2 = new Path();//末端路徑
    downPath2.moveTo(2 * radius+strokeWidth, strokeWidth);
    downPath2.lineTo(2 * radius+strokeWidth, 2 * radius+strokeWidth);

那麼問題來了,有了運動路徑之後,咱們但願有動畫。起始就是但願,咱們給定一個動畫時間,咱們可以得到在這段時間的某個點上,起始端/末端運動到路徑的哪一個位置
那麼有了路徑之後,咱們能不能得到路徑上的隨意一個位置呢?答案是使用PathMeasure類


可能有不少朋友對這個類不熟悉,可以參考一些文章。或者看看官方API介紹
看PathMeasure大展身手

咱們首先來看,怎麼初始化一個PathMeasure,很是easy,傳入一個Path對象就能夠,false表示不閉合這個路徑

downPathMeasure1 = new PathMeasure(downPath1,false);
    downPathMeasure2 = new PathMeasure(downPath2,false);

由於動畫有必定時間。咱們又需要一個計時器

//下落動畫 
    mDownAnimation = ValueAnimator.ofFloat(0f, 1f );
    mDownAnimation.setDuration(500);
    mDownAnimation.setInterpolator(new AccelerateInterpolator());
    mDownAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            downPrecent = (float) animation.getAnimatedValue();
            invalidate();
        }
    });

接下來是使用PathMeasure得到下落過程中,起始端和末端的座標

//下落方塊的起始端座標
    float pos1[] = new float[2];
    float tan1[] = new float[2];
    downPathMeasure1.getPosTan(downPrecent * downPathMeasure1.getLength(), pos1, tan1);
    //下落方塊的末端座標
    float pos2[] = new float[2];
    float tan2[] = new float[2];
    downPathMeasure2.getPosTan(downPrecent * downPathMeasure2.getLength(), pos2, tan2);

getPosTan()方法,第一個參數是指想要得到的路徑長度。好比你設置的Path長度爲100
那麼你傳入60,就會得到長度爲60時的終點座標(文字真的表達很差/(ㄒoㄒ)/~~,你們可以去看API)

依據起始端和末端的座標*。咱們繪制一條直線。就是小方塊啦!




方塊下落,進入圓內部分變粗。圓被擠壓變形

接下來要處理一個更加複雜的問題,就是進入圓環中的方塊部分,要變粗
爲了解決問題。咱們就需要分辨方塊哪部分在圓內,哪部分在圓外,這個推斷起來自己就很是麻煩。何況,圓環還會被壓縮!也就是園內圓外,沒有一個固定的分界點。

怎麼區分圓內圓外呢?我決定本身推斷太麻煩了,後來想到一個辦法,推斷交集!
咱們知道,Android提供了API。讓咱們可以推斷兩個Rect是否相交,也可以得到它們的相交部分(也就是重合部分),還可以得到非重合部分。


若是我把方塊當作是一個矩形。圓環當作一個矩形,那麼問題就簡單了,我就可以調用API計算出進入圓內的部分,和在圓外的部分了。
例如如下圖:
這裏寫圖片描寫敘述
咱們知道,事實上圓/橢圓。都是依靠一個矩形肯定的。在這個動畫中,咱們但願圓被擠壓成橢圓,終於縮放比例爲0.8,大概是這種
這裏寫圖片描寫敘述
利用前面提到的計時器,咱們可以依據當前時間。知道圓被擠壓的比例。實現擠壓效果

//橢圓形區域
    Rect mRect = new Rect(Math.round(mRectF.left),Math.round(mRectF.top+mRectF.height()*0.1f*downPrecent),
                Math.round(mRectF.right),Math.round(mRectF.bottom-mRectF.height()*0.1f*downPrecent));

這樣,咱們就有了表明橢圓的矩形。由於在一步中,咱們知道了小方塊的起始端和末端座標。咱們可以使這個兩個座標,分別向左右偏移必定距離,從而得到4個座標。來建立矩形。
最後,咱們直接利用兩個矩形,取交集和非交集,詳細實現例如如下:

//非交集
        Region region1 = new Region(Math.round(pos1[0])-strokeWidth/4,Math.round(pos1[1]),Math.round(pos2[0]+strokeWidth/4),Math.round(pos2[1]));
        region1.op(mRect, Region.Op.DIFFERENCE);
        drawRegion(canvas, region1, downRectPaint);

        //交集
        Region region2 = new Region(Math.round(pos1[0])-strokeWidth/2,Math.round(pos1[1]),Math.round(pos2[0]+strokeWidth/2),Math.round(pos2[1]));
        boolean isINTERSECT = region2.op(mRect, Region.Op.INTERSECT);
        drawRegion(canvas, region2, downRectPaint);

Region是Android提供的,用於處理區域運算問題的一個類,使用這個類,咱們可以很是方便進行Rect交集補集等運算,不瞭解的朋友,查看API

最後繪製這兩個區域,而且加上一個推斷。就是這個兩個矩形是否有相交,若是沒有,那麼圓環就不用被擠壓。直接繪製圓環就能夠。

//橢圓形區域
    if(isINTERSECT) {//若是有交集
        float extrusionPrecent = (pos2[1]-radius)/radius;
        RectF rectF = new RectF(mRectF.left, mRectF.top + mRectF.height() * 0.1f * extrusionPrecent, mRectF.right, mRectF.bottom - mRectF.height() * 0.1f * extrusionPrecent);//繪製橢圓
        canvas.drawArc(rectF, 0, 360, false, circlePaint);
    }else{
        canvas.drawArc(mRectF, 0, 360, false, circlePaint);//繪製圓
    }



下落完畢。繪製三叉

對於三叉的繪製,就沒有什麼特別的了,事實上三叉就是三條Path路徑,咱們用類似前面的作法,利用一個計時器,三個Path,相應三個PathMeasure,就可以動態繪製出路徑了。

/** * 繪製分叉 * @param canvas */
    private void drawFork(Canvas canvas) {
        float pos1[] = new float[2];
        float tan1[] = new float[2];
        forkPathMeasure1.getPosTan(forkPrecent * forkPathMeasure1.getLength(), pos1, tan1);
        float pos2[] = new float[2];
        float tan2[] = new float[2];
        forkPathMeasure2.getPosTan(forkPrecent * forkPathMeasure2.getLength(), pos2, tan2);
        float pos3[] = new float[2];
        float tan3[] = new float[2];
        forkPathMeasure3.getPosTan(forkPrecent * forkPathMeasure3.getLength(), pos3, tan3);

        canvas.drawLine(2 * radius+strokeWidth, radius+strokeWidth, 2 * radius+strokeWidth, 2 * radius+strokeWidth, downRectPaint);
        canvas.drawLine(2 * radius+strokeWidth, 2 * radius+strokeWidth, pos1[0], pos1[1], downRectPaint);
        canvas.drawLine(2 * radius+strokeWidth, 2 * radius+strokeWidth, pos2[0], pos2[1], downRectPaint);
        canvas.drawLine(2 * radius+strokeWidth, 2 * radius+strokeWidth, pos3[0], pos3[1], downRectPaint);
        //橢圓形區域
        RectF rectF = new RectF(mRectF.left, mRectF.top + mRectF.height() * 0.1f * (1-forkPrecent), 
                mRectF.right, mRectF.bottom - mRectF.height() * 0.1f * (1-forkPrecent));
        canvas.drawArc(rectF, 0, 360, false, circlePaint);
    }

最後,還要記得將橢圓還原成圓。事實上就是壓縮的逆過程
效果例如如下:
這裏寫圖片描寫敘述



繪製綠色勾√

綠色勾的繪製事實上也和上面的作法類似,需要一個計時器,一個Path,相應的PathMeasure就能夠
勾的路徑例如如下:

//初始化打鉤路徑
        Path tickPath = new Path();
        tickPath.moveTo(1.5f * radius+strokeWidth, 2 * radius+strokeWidth);
        tickPath.lineTo(1.5f * radius + 0.3f * radius+strokeWidth, 2 * radius + 0.3f * radius+strokeWidth);
        tickPath.lineTo(2*radius+0.5f * radius+strokeWidth,2*radius-0.3f * radius+strokeWidth);
        tickPathMeasure = new PathMeasure(tickPath,false);

最後將路徑動態繪製出現,到這裏你們都很是熟悉這個作法了。但是這裏我使用了另一個方法,這種方法可以依據進度。直接返回當前路徑成一個Path對象

/** * 繪製打鉤 * @param canvas */
    private void drawTick(Canvas canvas) {
        Path path = new Path();
        /* * On KITKAT and earlier releases, the resulting path may not display on a hardware-accelerated Canvas. * A simple workaround is to add a single operation to this path, such as dst.rLineTo(0, 0). */
        tickPathMeasure.getSegment(0, tickPrecent * tickPathMeasure.getLength(), path, true);//該方法,可以得到整個路徑的一部分
        path.rLineTo(0, 0);//解決Android自己的一個bug
        canvas.drawPath(path, tickPaint);//繪製出這一部分
        canvas.drawArc(mRectF, 0, 360, false, tickPaint);
    }

因而咱們在必定時間內。逐漸得到勾這個路徑的一部分。知道得到整個勾,並將其繪製出來!
終於效果例如如下:
這裏寫圖片描寫敘述


寫在最後

本篇文章。首先介紹成功載入的動畫實現過程下一篇文章將會接着介紹載入失敗過程的實現。
經過這篇文章,咱們應該熟悉了Path,PathMeasure,Region等一系列API,利用這些API。咱們可以方便得繪製出路徑效果。
每個步驟組合起來,就是一個好看的,複雜的動效。對於API不熟悉的朋友,建議用到的時候去查官方文檔,或者看看其它朋友的一些介紹基礎的文章。


最後,提供源代碼下載地址github地址,歡迎你們下載和star

相關文章
相關標籤/搜索