Android 自定義 View 之 LeavesLoading

Lin_YT發表於2019-02-01

1.前言

前天的瀏覽 GitHub 時發現一個模仿 Gif 的 Loading 特效的專案,感覺效果很不錯,也比較有創意,如下:

Android 自定義 View 之 LeavesLoading

GitHub 上好幾個做這個效果的專案,但是很少有完全實現的,有的還有 Bug,於是花了 2 天實現了一下。

效果如下:

Android 自定義 View 之 LeavesLoading

GitHub 專案在這裡 LeavesLoading

2. 分析

實現要求:

  • 葉子
    • 隨機產生
    • 飄動軌跡為正弦函式,並且隨機振幅
    • 飄動時伴隨自旋轉,更符合物理規律
    • 遇到進度條似乎是融入的
  • 風扇
    • 可旋轉
    • Loading == 100% 時顯示一個動畫
  • 細節
    • 風扇和葉子自適應 View 大小
    • 葉子在視覺上不能飄出 RountRect 邊界

3. 核心實現

3.1 隨機產生葉子

本質是事先產生一定數量葉子,這些葉子的漂動時的振幅、相位、旋轉方向等等都是隨機的,並且飄動是週期性地即葉子飄動到最左邊時,又重新回到最右邊。

Leaf 類:

    private class Leaf{
        float x,y;//座標
        AmplitudeType type;//葉子飄動振幅
        int rotateAngle;//旋轉角度
        RotateDir rotateDir;//旋轉方向
        long startTime;//起始時間
        int n;//初始相位
    }
複製程式碼

Leaf 生成方法:

    Leaf generateLeaf(){
        Leaf leaf = new Leaf();
        //隨機振幅
        int randomType = mRandom.nextInt(3);
        switch (randomType){
          case 0:
            //小振幅
            leaf.type = AmplitudeType.LITTLE;
            break;
          case 1:
            //中等振幅
            leaf.type = AmplitudeType.MIDDLE;
            break;
          default:
            //大振幅
            leaf.type = AmplitudeType.BIG;
            break;
        }
        //隨機旋轉方向
        int dir = mRandom.nextInt(2);
        switch (dir){
          case 0:
            //逆時針
            leaf.rotateDir = RotateDir.ANTICLOCKWISE;
            break;
          default:
            //順時針
            leaf.rotateDir = RotateDir.CLOCKWISE;
            break;
        }
        //隨機起始角度
        leaf.rotateAngle = mRandom.nextInt(360);
        leaf.n = mRandom.nextInt(20);
        mAddTime += mRandom.nextInt((int)mLeafFloatTime);
        leaf.startTime = System.currentTimeMillis() + mAddTime;
        return leaf;
    }
複製程式碼

3.2 葉子飄動軌跡為正弦函式

確定 Leaf 在某個時刻的座標 ( x , y ):

    /**
      * 獲取葉子的(x,y)位置
      * @param leaf 葉子
      * @param currentTime 當前時間
      */
    private void getLeafLocation(Leaf leaf,long currentTime){
        long intervalTime = currentTime - leaf.startTime;//飄動時長
        if (intervalTime <= 0){
          // 此 Leaf 還沒到飄動時間
          return;
        }else if (intervalTime > mLeafFloatTime){
          // Leaf 的飄動時間大於指定的飄動時間,即葉子飄動到了最左邊,應回到最右邊
          leaf.startTime = currentTime + new Random().nextInt((int)mLeafFloatTime);
        }
        // 計算移動因子
        float fraction = (float) intervalTime / mLeafFloatTime;
        leaf.x = (1-fraction)*mProgressLen;
        leaf.y = getLeafLocationY(leaf);

        if (leaf.x <= mYellowOvalHeight / 4){
          //葉子飄到最左邊,有可能會超出 RoundRect 邊界,所以提前特殊處理
          leaf.startTime = currentTime + new Random().nextInt((int)mLeafFloatTime);
          leaf.x = mProgressLen;
          leaf.y = getLeafLocationY(leaf);
        }
    }
複製程式碼

要想讓 Leaf 飄動軌跡為正弦函式,關鍵在於確定 Leaf 的 Y 軸座標:

   /**
     * 獲取葉子的Y軸座標
     * @param leaf 葉子
     * @return 經過計算的葉子Y軸座標
     */
    private float getLeafLocationY(Leaf leaf){
        float w = (float) (Math.PI * 2 / mProgressLen);//角頻率
        float A;//計算振幅值
        switch (leaf.type){
            case LITTLE:
                A = mLeafLen/3;
                break;
            case MIDDLE:
                A = mLeafLen*2/3;
                break;
            default:
                A = mLeafLen;
                break;
        }
        // (mHeight-mLeafLen)/2 是為了讓 Leaf 的Y軸起始位置居中
        return (float) (A * Math.sin(w * leaf.x + leaf.n)+(mHeight-mLeafLen)/2);
    }
複製程式碼

3.3 葉子飄動時自旋轉

這裡就涉及到了 Leaf 的繪製,其實 Gif 中的葉子和風扇都可以使用 Canves 直接繪製圖案,但是這樣就會有兩個問題:

  1. 難畫:想要畫出滿意圖形,並且還要旋轉、縮放、平移可要下一番功夫。
  2. 靈活性低:如果想換其他樣式又得重新設計繪製過程。

因此這裡採用 Canves.drawBitmap() 的方式繪製,直接使用已有的圖片作為葉子和風扇,同時利用 Canves.drawBitmap() 的一個過載的方法可以很方便的實現旋轉、縮放、平移:

void drawBitmap(Bitmap bitmap,  Matrix matrix, Paint paint) ;
複製程式碼

就是通過這裡的 Matrix 矩陣,它內部封裝了 postScale()postTranslatepostRotate() 等方法,可以幫助我們快速的對 Bitmap 進行旋轉、縮放、平移還有其他操作。使用時要記得配合 Canves 的 save()restore() 使用,否則達不到想要的效果。

對這方面不熟的朋友可以看看 HenCoder 的自定義 View 教學 1-4

繪製 Leaf 的方法:

  private void drawLeaves(Canvas canvas){
      long currentTime = System.currentTimeMillis();
      for (Leaf leaf : mLeafList) {
          if (currentTime > leaf.startTime && leaf.startTime != 0){
            // 獲取 leaf 當前的座標
            getLeafLocation(leaf,currentTime);
            canvas.save();
            Matrix matrix = new Matrix();
            // 縮放 自適應 View 的大小
            float scaleX = (float) mLeafLen / mLeafBitmapWidth;
            float scaleY = (float) mLeafLen / mLeafBitmapHeight;
            matrix.postScale(scaleX,scaleY);
            // 位移
            float transX = leaf.x;
            float transY = leaf.y;
            matrix.postTranslate(transX,transY);
            // 旋轉
            // 計算旋轉因子
            float rotateFraction = ((currentTime - leaf.startTime) % mLeafRotateTime)
              /(float)mLeafRotateTime;
            float rotate;
            switch (leaf.rotateDir){
              case CLOCKWISE:
                //順時針
                rotate = rotateFraction * 360 + leaf.rotateAngle;
                break;
              default:
                //逆時針
                rotate = -rotateFraction * 360 + leaf.rotateAngle;
                break;
            }
            // 旋轉中心選擇 Leaf 的中心座標
            matrix.postRotate(rotate,transX + mLeafLen / 2,transY + mLeafLen / 2);
            canvas.drawBitmap(mLeafBitmap,matrix,mBitmapPaint);
            canvas.restore();
          }
    }
複製程式碼

3.4 Loading == 100% 出現動畫

增加一個判斷欄位 isLoadingCompleted ,在 onDraw() 中選擇對應繪製策略。

isLoadingCompleted 在 setProgress() 中根據 progress 設定:

   /**
     * 設定進度(自動重新整理)
     * @param progress 0-100
     */
    public void setProgress(int progress){
        if (progress < 0){
            mProgress = 0;
        }else if (progress > 100){
            mProgress = 100;
        }else {
            mProgress = progress;
        }
        if (progress == 100){
            isLoadingCompleted = true;
        }else {
            isLoadingCompleted = false;
        }
        // 255 不透明
        mCompletedFanPaint.setAlpha(255);
        postInvalidate();
    }
複製程式碼

LeavesLoading.onDraw() 部分實現:

   @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        ......
        if (isLoadingCompleted){
            //繪製載入完成特效
            drawCompleted(canvas);
        }else {
            //繪製扇葉
            drawFan(canvas,mFanLen,mBitmapPaint);
        }
        //重新整理
        postInvalidate();
    }
複製程式碼

drawCompleted() 實現:

    private void drawCompleted(Canvas canvas) {
        // 每次繪製風扇透明度遞減10
        int alpha = mCompletedFanPaint.getAlpha() - 10;
        if (alpha <= 0){
            alpha = 0;
        }
        mCompletedFanPaint.setAlpha(alpha);
        // 文字透明度剛好與風扇相反
        mCompletedTextPaint.setAlpha(255-alpha);
        // 計算透明因子
        float fraction = alpha / 255f;
        // 葉片大小 和 文字大小 也是相反變化的
        float fanLen = fraction * mFanLen;
        float textSize = (1 - fraction) * mCompletedTextSize;
        mCompletedTextPaint.setTextSize(textSize);
         //測量文字佔用空間
        Rect bounds = new Rect();
        mCompletedTextPaint.getTextBounds(
                LOADING_COMPLETED,
                0,
                LOADING_COMPLETED.length(),
                bounds);
      	// 與 drawLeaf() 相似,不再贅述
        drawFan(canvas, (int) fanLen, mCompletedFanPaint);
        //畫文字
        canvas.drawText(
                LOADING_COMPLETED,
                0,
                LOADING_COMPLETED.length(),
                mFanCx-bounds.width()/2f,
                mFanCy+bounds.height()/2f,
                mCompletedTextPaint);
    }
複製程式碼

流程:計算風扇和文字透明度 -> 計算風扇和文字大小以及文字佔用空間 -> 繪製 ,註釋寫得比較清楚就不贅述了。

4. 結束

文章中如有出現任何錯誤,歡迎大家到評論區留言指正。

如果覺得 LeavesLoading 對您有任何幫助,希望可以在 GitHub 得到您的 Star !

Thanks:

相關文章