多種姿勢花式實現薄荷Loading動畫(上)

weixin_34120274發表於2017-08-23

本文屬於Android技術論述文章,閱讀完大致需要五分鐘

原創文章,轉載請註明出處。

沒時間的小夥伴可以直接跳過文章,點選專案地址,如果喜歡的話,順手給個star那是極好的【嬌羞……】

好吧。先說一下為什麼要做這個專案。
事情發生在前幾天的一個夜晚,我開啟薄荷,並點選播放。隨著視訊裡妹子的動作,我的身體也有規律地擺動了起來,不一會兒就開始喘起了粗氣,汗流浹背……

1484184-c74a44020eecb159.png

隨著妹子節奏的加快,我也情不自禁加快了頻率,眼看著就要【不可描述的累癱】的時候。我看到了這個畫面。我摔!深蹲還差10個就到600個呢!:
1484184-645e2813751d4f84.gif

這~~,就是我要的Loading圖!

先上本次最終實現的效果圖吧,顏色當然選擇今年最流行的原諒色:

1484184-6c27e4509ef581c2.gif

思路分析

  • 1、整個圖形的形狀如何繪製
  • 2、如何讓線條動起來

整個圖形的形狀分析

  • 好了,首先我們來分析一下這個圖案,如果是靜態的,那麼如何繪製?
    很簡單,拆分。我們將圖形拆開分解,然後再看。分析細節和步驟,這是要點。
    我這裡將這個圖分成了三份。
    • 第一個,也就是葉柄。也就是下面那一條小小的豎線。原Loading圖中不甚明顯,但還是有的。葉柄沒什麼說的,直線就可以了。
    • 第二個,葉子的左輪廓邊緣和右輪廓邊緣。這是一段下肥上窄的弧線,橢圓擷取感覺不妥,我這裡採用的是貝塞爾二階曲線。有關Android貝塞爾相關的知識大家可以看看這篇文章
    • 第三個,也就是葉片的脈絡,線和線交叉連線,沒什麼可說的。
  • 那麼重點其實就是葉子左右輪廓的繪製了,我畫了一張草圖。大家可以看看:
    1484184-138b92a7504ce6a5.png

其中黑色的框作為View的邊界。A點是左輪廓曲線的起點,B點事貝塞爾曲線的控制點,我把它定義到了View的左邊框那裡。C點事整個貝塞爾曲線的終點,D點則是實際上曲線的最高點。
右輪廓則和左輪廓是映象存在。
圖有點潦草,不過應該還看得懂。

  • 好了,靜態圖形拆解完畢。接著看,如何讓圖動起來。

如何讓線條動起來

整個專案中,如何讓線條真正的動起來才是要點。剛開始在這裡的思路,是想使用canvas.drawCircle繪製在一張Bitmap上,以點匯面。後面實現起來發現,這種方式特別不靠譜。
為什麼不靠譜呢?因為點連線成線,每次移動的速率和距離都得計算,很麻煩。很容易出現斷點的情況。
最後,我採用的是讓canvas去繪製一段Path路徑,然後Path路徑不停的重新整理改變。這樣做的好處,是Path更加直觀易於控制。而且還不用多繪製一張Bitmap
整個專案中,自定義的View,LeafAnimView做的工作很少,只是在onDraw方法內,調起了繪製而已。具體的繪製都交給LeafAtom了。物件導向嘛。

  • 具體的思路,是我把總時間按比例分成四部分。生成四個屬性動畫,在屬性動畫的監聽裡作Path的x和y的變化。在繪製的時候,只需要將這四個動畫依次播放,即可得到每個時間段的具體運動值。而且還是均勻變化的。

  • LeafAnimView內部作為動畫引擎的是一個ValueAnimator,使用它來觸發View的onDraw。同時也使用它來控制整個動畫的時間。

 mValueAnimator = ValueAnimator.ofFloat(0, 1);
        mValueAnimator.setDuration(5000);
        mValueAnimator.setRepeatCount(ValueAnimator.INFINITE);
        mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                invalidate();
            }
        });
  • LeafAtom類內部接受到這個總時長,然後將運動總時間分割,根據比例計算出繪製葉柄、左右輪廓、脈絡的動畫時間。
-------在LeafAnimView類內部---------
 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (null == mLeafAtom) {
         //傳入總時長
            mLeafAtom = new LeafAtom(getWidth(), getHeight(), mValueAnimator.getDuration());
        }
        if (!mValueAnimator.isStarted()) {
            mValueAnimator.start();
        }
        //開始繪製
        mLeafAtom.drawGraph(canvas, mPaint);
    }
    
-------在LeafAtome類---------------
public static final float PETIOLE_RATIO = 0.1f;//葉柄所佔比例
public LeafAtom(int width, int height, long duration) {

        mWidth = width;
        mHeight = height;

        mPetioleTime = (long) (duration * PETIOLE_RATIO);//繪製葉柄的時間
        mArcTime = (long) (duration * (1 - PETIOLE_RATIO) * 0.4f);//左右輪廓弧線的時間
        mLastLineTime = duration - mPetioleTime - mArcTime * 2;//最後一段葉脈的時間

        mBezierBottom = new PointF(mWidth * 0.5f, mHeight * (1 - PETIOLE_RATIO));//左側輪廓底部點
        mBezierControl = new PointF(0, mHeight * (1 - 3 * PETIOLE_RATIO));//左側輪廓控制點
        mBezierTop = new PointF(mWidth * 0.5f, 0);//左側輪廓頂部結束點

        mVeinBottomY = mHeight * (1 - PETIOLE_RATIO) - 10;//右側輪廓底部點Y軸座標,稍稍低一點
        mOneNodeY = mVeinBottomY * 4 / 5;//第一個節點的Y軸座標
        mTwoNodeY = mVeinBottomY * 2 / 5;//第二個節點Y軸座標
        initEngine();
        setOrginalStatus();
    }
  • LeafAtom的建構函式中,得到每一個階段動畫的時間,然後生成四個屬性動畫,在這個屬性動畫的監聽裡去做Path的x和y座標的值變化。
 /**
     * 初始化path引擎
     */
    private void initEngine() {
         //葉柄動畫,Y軸變化由底部運動到葉柄高度的地方
        mPetioleAnim = ValueAnimator.ofFloat(mHeight, mHeight * (1 - PETIOLE_RATIO)).setDuration(mPetioleTime);
        //左右輪廓貝塞爾曲線,只需要只奧時間變化是從0~1的。起點、控制點、結束點都知道了
        mArcAnim = ValueAnimator.ofFloat(0, 1.0f).setDuration(mArcTime);
        //繪製葉脈的動畫
        mLastAnim = ValueAnimator.ofFloat(mVeinBottomY, 0).setDuration(mLastLineTime);

        mPetioleAnim.setInterpolator(new LinearInterpolator());
        mArcAnim.setInterpolator(new LinearInterpolator());
        mLastAnim.setInterpolator(new LinearInterpolator());
        mArcRightAnim = mArcAnim.clone();

        mPetioleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mY = (float) animation.getAnimatedValue();
            }
        });
        mArcAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                computeArcPointF(animation, true);
            }
        });
        mArcRightAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                computeArcPointF(animation, false);
            }
        });
        mLastAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mY = (float) animation.getAnimatedValue();
                float tan = (float) Math.tan(Math.toRadians(30));
                if (mY <= mOneNodeY && mY > mTwoNodeY) {
                    mOneLpath.moveTo(mX, mOneNodeY);
                    mOneRpath.moveTo(mX, mOneNodeY);
                    //這裡的引數x和y代表相對當前位置偏移量,y軸不加偏移量會空一截出來,這裡的15是經驗值
                    mMainPath.addPath(mOneLpath, 0, EXPRIENCE_OFFSET);
                    mMainPath.addPath(mOneRpath, 0, EXPRIENCE_OFFSET);
                    //第一個節點和第二個節點之間
                    float gapY = mOneNodeY - mY;
                    mOneLpath.rLineTo(-gapY * tan, -gapY);
                    mOneRpath.lineTo(mX + gapY * tan, mY);
                } else if (mY <= mTwoNodeY) {
                    mTwoLpath.moveTo(mX, mTwoNodeY);
                    mTwoRpath.moveTo(mX, mTwoNodeY);

                    //第二個節點,為避免線超出葉子,取此時差值的一半作計算
                    float gapY = (mTwoNodeY - mY) * 0.5f;
                    mMainPath.addPath(mTwoLpath, 0, EXPRIENCE_OFFSET);
                    mMainPath.addPath(mTwoRpath, 0, EXPRIENCE_OFFSET);

                    mTwoLpath.rLineTo(-gapY * tan, -gapY);
                    mTwoRpath.rLineTo(gapY * tan, -gapY);
                }
            }
        });

        mEngine = new AnimatorSet();
        mEngine.playSequentially(mPetioleAnim, mArcAnim, mArcRightAnim, mLastAnim);
        mEngine.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                setOrginalStatus();
            }
        });
    }
  • 計算貝塞爾曲線運動過程中的方法。貝塞爾曲線是有一個函式的,我們知道起點、控制點、終點的話,就可以根據時間計算出此時此刻的x和y的座標。而這個時間變化是從0~1變化的。謹記。
private void computeArcPointF(ValueAnimator animation, boolean isLeft) {
        float ratio = (float) animation.getAnimatedValue();
        //ratio從0~1變化,左右輪廓三個點不一樣
        PointF bezierStart = isLeft ? mBezierBottom : mBezierTop;
        PointF bezierControl = isLeft ? mBezierControl : new PointF(mWidth, mHeight * (1 - 3 * PETIOLE_RATIO));
        PointF bezierEnd = isLeft ? mBezierTop : new PointF(mWidth * 0.5f, mVeinBottomY);
        PointF pointF = calculateCurPoint(ratio, bezierStart, bezierControl, bezierEnd);
        mX = pointF.x;
        mY = pointF.y;
    }
    private PointF calculateCurPoint(float t, PointF p0, PointF p1, PointF p2) {
        PointF point = new PointF();
        float temp = 1 - t;
        point.x = temp * temp * p0.x + 2 * t * temp * p1.x + t * t * p2.x;
        point.y = temp * temp * p0.y + 2 * t * temp * p1.y + t * t * p2.y;
        return point;
    }
  • 葉脈的繪製,在節點一和節點二,分別加上兩個向左和向右伸展開的Path路徑即可。

    • 需要說明的是,lineTorLineTo的區別,lineTo的引數代表的就是目標引數,而rLineTo的引數代表的是,目標引數和起點引數的差值。
  • 最後在drawGraph函式中,啟動這個動畫集合:

public void drawGraph(Canvas canvas, Paint paint) {
        if (mEngine.isStarted()) {
            canvas.drawPath(mMainPath, paint);
            mMainPath.lineTo(mX, mY);
        } else {
            mEngine.start();
        }
    }

以上,就是本次專案的主要思路了。相關注釋程式碼裡都寫的很清楚了,專案地址在這裡。仿薄荷Loading動畫,大家走過路過千萬別忘了給個Star啊。

下次還是這個動畫,我會嘗試一種新的方式來實現這個動畫~~

1484184-c3bbf1bd3801735d.jpg

相關文章