1. 程式人生 > >Android自定義view-高仿小米視訊載入動畫效果

Android自定義view-高仿小米視訊載入動畫效果

*本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出

1、概述

        前幾日出差,每晚回到酒店的時候,睡前打發時間就是拿起自己的小米手機擼劇,酒店的wifi網路實在太差,眼睜睜的看著小米視訊的載入動畫一直拼命的loading中,正好最近一直在看自定義view的東西,何不乘此擼一個山寨的小米視訊動畫練練手,廢話不多說了,先上效果圖。

2、原理分析

2.1 總體結構分析


        如上圖所示,動畫主要由四個三角形構成,對四個三角形進行標號,其中中間的三角形為1號,外圍順時針方向依次為2、3、4號。該圖形的巧妙之處在於四個三角形整體又組合為一個新的大三角形。根據上文的預覽動畫可以看出,四個三角形是依次出現和消失的,四個三角形出現的順序正好是按照編號1-2-3-4出現的,消失的順序是按照4-2-3-1(不是4-3-2-1)。

2.2 三角形的出現形式

        顧名思義,只要知道三角形的三個頂點,即可以繪製出對應的三角形來。動畫中三角形的繪製有個漸變過程,三角形的出現不是一蹴而就的,這裡以上文中的1號三角形為例進行說明,廢話不多說,直接上圖:
圖 三角形顯示載入過程         如上圖,三角形在載入的過程中,會以其中一個頂點開始,向其他兩個頂點進行延伸擴充套件。我們這裡把延伸的起點稱為start,延伸中的兩個頂點分別是current1和current2,延伸的終點稱為end1和end2,演變的過程很簡單,即start點保持不變,其餘兩個頂點分別從start點向end點延伸。

3、程式碼實現

        說了這麼多,可以著手開始擼程式碼了。既然我們的動畫都是以三角形為單元的,所以我們可以定義一個三角形類TriangleView,這個類至少包含如下幾個屬性:
  • 三角形的三個頂點座標,即上文分析中提到的start、end1、end2座標;
  • 三角形的背景色;
  • 三角形目前載入過程中current1和current2的座標位置;
public class TriangleView {

    // 起始點座標
    public int startX;
    public int startY;
    //終點座標
    public int endX1;
    public int endY1;
    public int endX2;
    public int endY2;
    //當前延伸中的座標位置
    public int currentX1;
    public int currentY1;

    public int currentX2;
    public int currentY2;

    //背景色
    public String color;

}
         程式碼很簡單,僅僅是我們上述所描述的屬性,起點座標、終點座標、延伸中的當前座標和背景色。         下面自定義我們的View,我這裡起名為MyVideoView,程式碼如下:
public class MyVideoView extends View{

    //控制元件中心點座標
    private int cvX,cvY;
    //三角形邊長
    private int edge = 200;
    //畫筆
    private Paint myPaint;
    //繪製三角形的路徑
    private Path mPath;
    //存放三角形的陣列,一共有4個三角形
    private TriangleView[] triangles = new TriangleView[4];
    //繪製狀態,用來標記當前應該繪製哪個三角形
    private STATUS currentStatus = STATUS.MID_LOADING;
    //繪製動畫
    private ValueAnimator valueAnimator;
    //列舉變數,存放繪製狀態
    private enum STATUS {
        MID_LOADING,
        FIRST_LOADING,
        SECOND_LOADING,
        THIRD_LOADING,
        LOADING_COMPLETE,
        THIRD_DISMISS,
        FIRST_DISMISS,
        SECOND_DISMISS,
        MID_DISMISS
    }


    public MyVideoView(Context context) {
        super(context);
        init();
    }

    private void init() {
        //初始畫筆和路徑
        myPaint = new Paint();
        myPaint.setStyle(Paint.Style.FILL);
        myPaint.setAntiAlias(true);
        mPath = new Path();
    }

    public MyVideoView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public MyVideoView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //初始化控制元件中心點和四個三角形的位置
        cvX = getMeasuredWidth()/2;
        cvY = getMeasuredHeight()/2;
        initTriangle();
    }
        如上程式碼,註釋已經很清楚了,相信你們都可以看明白,需要說明的是,因為我們的動畫是由四個三角形構成,所以我們用TriangleView陣列來進行存放,存放的順序按照上文中的1-2-3-4號三角形存放,即三角形的出現順序。另外,為了讓程式在特定的時間點知道應當繪製哪一個三角形,設定一個列舉變數來存放當前應當繪製的狀態。列舉變數的含義這裡簡單說下,XXXX_LOADING表示是第幾個三角形正在展現,例如MID_LOADING表示中間的三角形開始繪製,XXXX_DISMISS表示第幾個三角形正在消失,LOADING_COMPLETE表示四個三角形全部展現完畢,需要在這個狀態下停留一段時間。         在onMeasure的時候,除了獲取控制元件的中心點座標以為,還需要對四個三角形的座標進行初始化,首先需要確定的就是1號三角形,即中間三角形的位置,為了進一步說明座標的計算,示意圖如下所示:
        如上圖是中間三角形的示意圖,三個頂點我們記為A、B、C,根據最終三角形的構造來看,中間三角形的中心點E為控制元件的中心點,即上述程式碼中的CvX和CvY,我們需要通過中心點CvX和CvY,即E點,求出A、B、C點的座標,三角形是一個等邊三角形,同時邊長Edge是已知的,所以要求出線段AD、DB、ED、CE的長度就可以計算出A、B、C點三點的座標。由於AD、DB正好是二分之一的邊長,所以也是已知的,E是中心點,所以ED和CE的長度一樣,所以關鍵只要求出CD的值就可以了,而三角形BCD又是一個直角三角形,CD是直角邊,根據勾股定理,可以求出CD的值,CD的長度為BC的平方-DB的平方再開方即可(這裡吐槽一下,初中的幾何知識較多尷尬)         廢話又太多了,上程式碼:
private void initTriangle() {
        //計算中間三角形的座標位置,startx表示要開始延伸的起點,endx1和endx2表示延伸的兩個終點,currentX1、currentX2表示的是正在延伸的點的位置
        currentStatus = STATUS.MID_LOADING;
        TriangleView triangleView = new TriangleView();
        //offset就是CD的長度,利用勾股定理
        int offset = (int) Math.sqrt(Math.pow(edge,2) - Math.pow(edge/2,2));
        triangleView.startX = cvX + offset/2;
        triangleView.startY = cvY + edge/2;
        triangleView.endX1 = cvX + offset/2;
        triangleView.endY1 = cvY - edge/2;
        triangleView.endX2 = cvX - offset/2;
        triangleView.endY2 = cvY;
        //current為延伸中的實時座標,預設在起始點位置
        triangleView.currentX1 = triangleView.startX;
        triangleView.currentY1 = triangleView.startY;
        triangleView.currentX2 = triangleView.startX;
        triangleView.currentY2 = triangleView.startY;
        triangleView.color = "#be8cd5";
        triangles[0] = triangleView;
        //計算第一個三角形的座標位置
        TriangleView firstTriangle = new TriangleView();
        firstTriangle.startX = triangleView.endX2;
        firstTriangle.startY = triangleView.endY2;
        firstTriangle.endX1 = triangleView.endX1;
        firstTriangle.endY1 = triangleView.endY1;
        firstTriangle.endX2 = firstTriangle.startX;
        firstTriangle.endY2 = firstTriangle.startY - edge;
        firstTriangle.color = "#fcb131";
        triangles[1] = firstTriangle;
        //計算第二個三角形的座標位置
        TriangleView secondTriangle = new TriangleView();
        secondTriangle.startX = triangleView.endX1;
        secondTriangle.startY = triangleView.endY1;
        secondTriangle.endX1 = secondTriangle.startX;
        secondTriangle.endY1 = secondTriangle.startY + edge;
        secondTriangle.endX2 = secondTriangle.startX + offset;
        secondTriangle.endY2 = secondTriangle.startY + edge/2;
        secondTriangle.color = "#67c6ca";
        triangles[2] = secondTriangle;
        //計算第三個三角形的座標位置
        TriangleView thirdTriangle = new TriangleView();
        thirdTriangle.startX = triangleView.startX;
        thirdTriangle.startY = triangleView.startY;
        thirdTriangle.endX1 = triangleView.endX2;
        thirdTriangle.endY1 = triangleView.endY2;
        thirdTriangle.endX2 = triangleView.endX2;
        thirdTriangle.endY2 = thirdTriangle.endY1 + edge;
        thirdTriangle.color = "#eb7583";
        triangles[3]  = thirdTriangle;
    }
        相信上面說了那麼多,這塊程式碼應當很容易理解,在紙上把四個三角形的相對位置繪製出來,座標位置便一目瞭然。          三角形的具體繪製放到onDraw中,程式碼如下:
protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        for (int i = 0; i < triangles.length;i++){
            mPath.reset();
            //移動到當前三角形的起始點位置上
            mPath.moveTo(triangles[i].startX,triangles[i].startY);
            //連線目前的current1
            mPath.lineTo(triangles[i].currentX1,triangles[i].currentY1);
            //連線目前的current2
            mPath.lineTo(triangles[i].currentX2,triangles[i].currentY2);
            //三角形線段閉合
            mPath.close();
            //設定三角形顏色
            myPaint.setColor(Color.parseColor(triangles[i].color));
            //繪製三角形
            canvas.drawPath(mPath,myPaint);
            //當只繪製中間三角形時,其他三角形不需要進行繪製
            if (currentStatus == STATUS.MID_LOADING){
                break;
            }
        }

    }
        onDraw的方法是對三角形的實際繪製,程式碼量沒有幾行,原理也很簡單,只是將三角形陣列triangles中的三角形物件取出來分別進行繪製。這裡有個簡單的處理,即如果當前的繪製狀態是MID_LOADING的時候,即最開始繪製中間三角形的時候,其他三角形沒有必要繪製了,通過break跳出迴圈。        動畫的製作最為關鍵的就是插值,在繪圖的過程中不停的改變繪製的變數來達到動畫形變的效果,好了繼續貼上動畫插值的程式碼:
public void startTranglesAnimation() {
        //初始化三角形位置
        initTriangle();
        //如果有動畫已經在執行了,取消當前執行的動畫。
        if (valueAnimator != null && valueAnimator.isRunning()){
            valueAnimator.cancel();
        }
        //動畫插值從0變成1
        valueAnimator = ValueAnimator.ofFloat(0,1);
        //每次動畫的執行時長為300毫秒
        valueAnimator.setDuration(300);
        //無限次執行
        valueAnimator.setRepeatCount(-1);
        //每次執行的方案都是從頭開始
        valueAnimator.setRepeatMode(ValueAnimator.RESTART);
        //監聽每次動畫的迴圈情況,沒迴圈一次進入下一個階段
        valueAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {

            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {
                //當上一個動畫狀態執行完之後進入下一個階段。
                if (currentStatus == STATUS.MID_LOADING){
                    currentStatus = STATUS.FIRST_LOADING;
                }else if (currentStatus == STATUS.FIRST_LOADING){
                    currentStatus = STATUS.SECOND_LOADING;
                }else if (currentStatus == STATUS.SECOND_LOADING){
                    currentStatus = STATUS.THIRD_LOADING;
                }else if (currentStatus == STATUS.THIRD_LOADING){
                    currentStatus = STATUS.LOADING_COMPLETE;
                    reverseTriangleStart();
                }else if (currentStatus == STATUS.LOADING_COMPLETE){
                    currentStatus = STATUS.THIRD_DISMISS;
                }else if (currentStatus == STATUS.THIRD_DISMISS){
                    currentStatus = STATUS.FIRST_DISMISS;
                }else if (currentStatus == STATUS.FIRST_DISMISS){
                    currentStatus = STATUS.SECOND_DISMISS;
                }else if (currentStatus == STATUS.SECOND_DISMISS){
                    currentStatus = STATUS.MID_DISMISS;
                }else if (currentStatus == STATUS.MID_DISMISS){
                    Log.e("wangjinfeng","onAnimationRepeat");
                    currentStatus = STATUS.MID_LOADING;
                    reverseTriangleStart();
                }
            }
        });
        //監聽動畫執行過程
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                //或者目前的插值(0-1)
                float fraction = animation.getAnimatedFraction();
                //如果目前的動畫是消失狀態,則插值正好是反過來的,是1-0,所以需要用1-fraction
                if (currentStatus == STATUS.FIRST_DISMISS || currentStatus == STATUS.SECOND_DISMISS || currentStatus == STATUS.THIRD_DISMISS || currentStatus == STATUS.MID_DISMISS){
                    fraction = 1 - fraction;
                }
                //根據目前執行的狀態,取出對應的需要處理的三角形
                TriangleView triangleView = triangles[0];
                if (currentStatus == STATUS.MID_LOADING || currentStatus == STATUS.MID_DISMISS){
                    triangleView = triangles[0];
                }else if (currentStatus == STATUS.FIRST_LOADING || currentStatus == STATUS.FIRST_DISMISS){
                    triangleView = triangles[1];
                }else if (currentStatus == STATUS.SECOND_LOADING || currentStatus == STATUS.SECOND_DISMISS){
                    triangleView = triangles[2];
                }else if (currentStatus == STATUS.THIRD_LOADING || currentStatus == STATUS.THIRD_DISMISS){
                    triangleView = triangles[3];
                }else if (currentStatus == STATUS.LOADING_COMPLETE){
                    //如果是LOADING_COMPLETE狀態的話,此次動畫效果保持不變
                    invalidate();
                    return;
                }
                //這裡是三角形變化的過程,計算目前current的座標應當處在什麼位置上
                //當fration為0的時候,current的座標為start位置,當fratcion為1的時候,current的座標是end位置
                triangleView.currentX1 = (int) (triangleView.startX + fraction * (triangleView.endX1 - triangleView.startX));
                triangleView.currentY1 = (int) (triangleView.startY + fraction * (triangleView.endY1 - triangleView.startY));
                triangleView.currentX2 = (int) (triangleView.startX + fraction * (triangleView.endX2 - triangleView.startX));
                triangleView.currentY2 = (int) (triangleView.startY + fraction * (triangleView.endY2 - triangleView.startY));
                invalidate();
            }
        });

        valueAnimator.start();
    }
        上述程式碼中需要對動畫的迴圈onAnimationRepeat進行監聽,每次迴圈會更改一次動畫狀態,例如每次迴圈會繪製一個三角形或者消失一個三角形,然後對onAnimationUpdate進行監聽,這裡需要注意的是,如果目前的動畫效果是要顯示三角形,則提取到的fraction應當為從0到1的漸變過程,如果動畫效果是消失一個三角形,則fraction應當是從1到0的漸變過程,所以程式碼中有一句1-fraction的操作。current的值初始是在start位置上,隨著fraction的演進,current的值需要在start的基礎上增加對應的演進過程,演進的變化量就是fraction乘以start與end節點之間的距離。         細心的同學或許已經發現在上述程式碼的第56行呼叫了reverseTriangleStart方法,這個方法有什麼用呢,如果大家再回頭本文的開頭部分觀察動畫的演示效果的話,細心一點會發現,三角形出現的時候起始點的位置和三角形消失的時候起始點的位置是不同的,即原來的起始點在三角形消失的時候是作為end點進行的,而原來的其中一個end點變成了start點,這個動畫的巧妙之處就在於看似有規律的三角形繪製順序,其實並不是按照既定的規則來的,而且三角形消失的順序與三角形出現的順序是不一致的。所以我們的列舉變數中在THIRD_DISMISS之後是FIRST_DISMISS,並不是SECONDE_DISMISS。好了下面繼續貼上程式碼:
private void reverseTriangleStart(){
        for (int i = 0; i < triangles.length; i++){
            int startX = triangles[i].startX;
            int startY = triangles[i].startY;
            triangles[i].startX = triangles[i].endX1;
            triangles[i].startY = triangles[i].endY1;
            triangles[i].endX1 = startX;
            triangles[i].endY1 = startY;
            triangles[i].currentX1 = triangles[i].endX1;
            triangles[i].currentY1 = triangles[i].endY1;
        }
    }
          程式碼很簡單,僅僅是交換每個三角形start和end1的值,讓之前的end1變成了新的start頂點,這裡需要注意的是,current的值應當與end1的值保持一致。否則在onDraw中,當處於LOADING_COMPLETE狀態下,由於start與end1的值進行了交換,current的值還是按照之前的來會出現問題,大家可以手動除錯下就知道了。

4、結語

         本工程主要傳遞該動畫的製作思路,還有些地方並不完善,例如沒有把屬性自定義抽取出來,onMeasure沒有針對自適應進行重構等,第一篇博文歡迎各位拍磚。