Android自定義加載動畫-顫抖吧!球球

Android自定義動畫系列六,今天來分享第六個自定義Loading動畫(ElasticBallBuilder),我給他起了一個逼格很高的名字,叫顫抖吧!球球,這個動畫讓我絞盡腦汁,算數算的暈乎乎,不過結果還是很滿意的,還是老規矩先介紹,效果圖在最后面。

實現效果圖在最后,GIF有點大,手機流量請三思。

??這里我想問大家一個問題,這個最終效果圖,是放在文章的 開頭 好呢?還是放在 結尾 好呢?
大家評論里面給個建議吧。謝謝了。

介紹

首先依舊是聲明,做這些動畫的初衷是為了學習和分享,所以希望大家可以指點錯誤,讓我更好的進步。(系列加載動畫的截止時間:我放棄的時候)。
上一個動畫鏈接:Android自定義加載動畫-PacMan

正文

參數變量初始化,用處我都在代碼里寫了注釋了,不懂得大家也可以問,有什么不對的,也希望大家幫忙指出,謝謝了。如下:

    //動畫間隔時間
    private static final long DURATION_TIME = 333;
    //最終階段
    private static final int                     FINAL_STATE   = 2;
    //小球共5個位置
    private static final int                     SUM_POINT_POS = 5;
    //貝塞爾曲線常量
    private static final float                   PROP_VALUE    = 0.551915024494f;
    //小球點集合
    private final        LinkedList<CirclePoint> mBallPoints   = new LinkedList<>();
    //背景圓集合
    private final        LinkedList<CirclePoint> mBGCircles    = new LinkedList<>();
    private Paint mPaint;
    private float mBallR;
    private Path  mPath;
    //當前動畫階段
    private int mCurrAnimatorState = 0;
    //每個小球的偏移量
    private float mCanvasTranslateOffset;
    //當前狀態是否翻轉
    private boolean mIsReverse    = false;
    //當前小球的位置
    private int     mCurrPointPos = 0;

首先初始化參數,mBallR 為小球半徑,mCanvasTranslateOffset 為小球之間的間距,mPath 路徑,其它如注釋:

    @Override
    protected void initParams(Context context)
    {
        mBallR = getAllSize() / SUM_POINT_POS;
        mCanvasTranslateOffset = getIntrinsicWidth() / SUM_POINT_POS;
        mPath = new Path();
        initPaint(5);
        initPoints();
        initBGPoints();
    }

    /**
     * 背景圓點初始化
     */
    private void initBGPoints()
    {
        float centerX = getViewCenterX();
        float centerY = getViewCenterY();
        CirclePoint p_0 = new CirclePoint(centerX - mCanvasTranslateOffset * 2, centerY);
        CirclePoint p_1 = new CirclePoint(centerX - mCanvasTranslateOffset, centerY);
        CirclePoint p_2 = new CirclePoint(centerX, centerY);
        CirclePoint p_3 = new CirclePoint(centerX + mCanvasTranslateOffset, centerY);
        CirclePoint p_4 = new CirclePoint(centerX + mCanvasTranslateOffset * 2, centerY);

        p_0.setEnabled(false);//默認第一個圓不顯示
        mBGCircles.add(p_0);
        mBGCircles.add(p_1);
        mBGCircles.add(p_2);
        mBGCircles.add(p_3);
        mBGCircles.add(p_4);
    }

這里很重要,很重要,重要。
這里初始化的是小球的各個點坐標,這里的小球是通過貝塞爾曲線繪制了,為了后面方便動畫操作,所以球的繪制就會相對的繁瑣了。

貝塞爾曲線畫球的原理,如下:

原理圖,來自其它地方

具體標注點,請對照我注釋中的P0~P11點,如下:


    /**
     *      p10    p9      p8
     *      ------  ------
     * p11                     p7
     * |                       |
     * |                       |
     * p0 |      (0,0)         | p6
     * |                       |
     * |                       |
     * p1                      p5
     *      ------  ------
     *      p2      p3      p4
     */
    private void initPoints()
    {
        float centerX = getViewCenterX();
        float centerY = getViewCenterY();
        CirclePoint p_0 = new CirclePoint(centerX - mBallR, centerY);
        mBallPoints.add(p_0);
        CirclePoint p_1 = new CirclePoint(centerX - mBallR, centerY + mBallR * PROP_VALUE);
        mBallPoints.add(p_1);
        CirclePoint p_2 = new CirclePoint(centerX - mBallR * PROP_VALUE, centerY + mBallR);
        mBallPoints.add(p_2);
        CirclePoint p_3 = new CirclePoint(centerX, centerY + mBallR);
        mBallPoints.add(p_3);
        CirclePoint p_4 = new CirclePoint(centerX + mBallR * PROP_VALUE, centerY + mBallR);
        mBallPoints.add(p_4);
        CirclePoint p_5 = new CirclePoint(centerX + mBallR, centerY + mBallR * PROP_VALUE);
        mBallPoints.add(p_5);
        CirclePoint p_6 = new CirclePoint(centerX + mBallR, centerY);
        mBallPoints.add(p_6);
        CirclePoint p_7 = new CirclePoint(centerX + mBallR, centerY - mBallR * PROP_VALUE);
        mBallPoints.add(p_7);
        CirclePoint p_8 = new CirclePoint(centerX + mBallR * PROP_VALUE, centerY - mBallR);
        mBallPoints.add(p_8);
        CirclePoint p_9 = new CirclePoint(centerX, centerY - mBallR);
        mBallPoints.add(p_9);
        CirclePoint p_10 = new CirclePoint(centerX - mBallR * PROP_VALUE, centerY - mBallR);
        mBallPoints.add(p_10);
        CirclePoint p_11 = new CirclePoint(centerX - mBallR, centerY - mBallR * PROP_VALUE);
        mBallPoints.add(p_11);
    }

    /**
     * 初始化畫筆
     */
    private void initPaint(float lineWidth)
    {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mPaint.setStrokeWidth(lineWidth);
        mPaint.setColor(Color.BLACK);
        mPaint.setDither(true);
        mPaint.setFilterBitmap(true);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setStrokeJoin(Paint.Join.ROUND);
    }

以下,開始了繪制工作了,drawBG() 繪制背景,在這里就不多介紹了;我們看下 drawBall() 方法,通過路徑貝塞爾曲線繪制并連接一開始初始化的12個點坐標形成路徑,最終繪制到畫布上。


    @Override
    protected void onDraw(Canvas canvas)
    {
        drawBG(canvas);
        drawBall(canvas);
    }

    /**
     * 繪制小球
     *
     * @param canvas
     */
    private void drawBall(Canvas canvas)
    {
        canvas.save();
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        float offsetX = mBGCircles.size() / 2 * mCanvasTranslateOffset;
        canvas.translate(-offsetX + mCanvasTranslateOffset * mCurrPointPos, 0);
        mPath.reset();
        mPath.moveTo(mBallPoints.get(0).getX(), mBallPoints.get(0).getY());
        mPath.cubicTo(mBallPoints.get(1).getX(), mBallPoints.get(1).getY(), mBallPoints.get(2).getX(), mBallPoints.get(2).getY(), mBallPoints.get(3).getX(), mBallPoints.get(3).getY());
        mPath.cubicTo(mBallPoints.get(4).getX(), mBallPoints.get(4).getY(), mBallPoints.get(5).getX(), mBallPoints.get(5).getY(), mBallPoints.get(6).getX(), mBallPoints.get(6).getY());
        mPath.cubicTo(mBallPoints.get(7).getX(), mBallPoints.get(7).getY(), mBallPoints.get(8).getX(), mBallPoints.get(8).getY(), mBallPoints.get(9).getX(), mBallPoints.get(9).getY());
        mPath.cubicTo(mBallPoints.get(10).getX(), mBallPoints.get(10).getY(), mBallPoints.get(11).getX(), mBallPoints.get(11).getY(), mBallPoints.get(0).getX(), mBallPoints.get(0).getY());
        canvas.drawPath(mPath, mPaint);
        canvas.restore();
    }

    /**
     * 繪制背景圓
     *
     * @param canvas
     */
    private void drawBG(Canvas canvas)
    {
        canvas.save();
        mPaint.setStyle(Paint.Style.STROKE);
        for (CirclePoint point : mBGCircles)
        {
            point.draw(canvas, mBallR, mPaint);
        }
        canvas.restore();
    }

這里是不同階段對應的不同偏移量賦值,前三個階段是針對順序移動的操作,后三個階段是對應的逆序時所需要的賦值操作,所有位移操作都是對小球的12點進行X軸方向的處理。


    @Override
    protected void computeUpdateValue(ValueAnimator animation, @FloatRange(from = 0.0, to = 1.0) float animatedValue)
    {
        float offset = mCanvasTranslateOffset;
        int currState = mIsReverse ? mCurrAnimatorState + 3 : mCurrAnimatorState;
        switch (currState)
        {
            case 0:
                animation.setDuration(DURATION_TIME);
                animation.setInterpolator(new AccelerateInterpolator());
                mBallPoints.get(5).setOffsetX(animatedValue * offset);
                mBallPoints.get(6).setOffsetX(animatedValue * offset);
                mBallPoints.get(7).setOffsetX(animatedValue * offset);
                break;
            case 1:
                animation.setDuration(DURATION_TIME + 111);
                animation.setInterpolator(new DecelerateInterpolator());
                mBallPoints.get(2).setOffsetX(animatedValue * offset);
                mBallPoints.get(3).setOffsetX(animatedValue * offset);
                mBallPoints.get(4).setOffsetX(animatedValue * offset);
                mBallPoints.get(8).setOffsetX(animatedValue * offset);
                mBallPoints.get(9).setOffsetX(animatedValue * offset);
                mBallPoints.get(10).setOffsetX(animatedValue * offset);
                break;
            case 2:
                animation.setDuration(DURATION_TIME + 333);
                animation.setInterpolator(new BounceInterpolator());
                mBallPoints.get(0).setOffsetX(animatedValue * offset);
                mBallPoints.get(1).setOffsetX(animatedValue * offset);
                mBallPoints.get(11).setOffsetX(animatedValue * offset);
                break;
            case 3:
                animation.setDuration(DURATION_TIME);
                animation.setInterpolator(new AccelerateInterpolator());
                mBallPoints.get(0).setOffsetX((1 - animatedValue) * offset);
                mBallPoints.get(1).setOffsetX((1 - animatedValue) * offset);
                mBallPoints.get(11).setOffsetX((1 - animatedValue) * offset);
                break;
            case 4:
                animation.setDuration(DURATION_TIME + 111);
                animation.setInterpolator(new DecelerateInterpolator());
                mBallPoints.get(2).setOffsetX((1 - animatedValue) * offset);
                mBallPoints.get(3).setOffsetX((1 - animatedValue) * offset);
                mBallPoints.get(4).setOffsetX((1 - animatedValue) * offset);
                mBallPoints.get(8).setOffsetX((1 - animatedValue) * offset);
                mBallPoints.get(9).setOffsetX((1 - animatedValue) * offset);
                mBallPoints.get(10).setOffsetX((1 - animatedValue) * offset);
                break;
            case 5:
                animation.setDuration(DURATION_TIME + 333);
                animation.setInterpolator(new BounceInterpolator());
                mBallPoints.get(5).setOffsetX((1 - animatedValue) * offset);
                mBallPoints.get(6).setOffsetX((1 - animatedValue) * offset);
                mBallPoints.get(7).setOffsetX((1 - animatedValue) * offset);
                break;
        }
    }

在下面的方法中,對小球動畫的各個階段進行分布,各個點的偏移量進行重置,以及順序倒序移動的切換邏輯進行判斷,并且對背景圓也做了關聯處理。


    @Override
    public void onAnimationRepeat(Animator animation)
    {
        if (++mCurrAnimatorState > FINAL_STATE)
        {//還原到第一階段
            mCurrAnimatorState = 0;

            /* 小球位置改變 */
            if (mIsReverse)
            {//倒序
                mCurrPointPos--;
            }
            else
            {//順序
                mCurrPointPos++;
            }

            /* 重置并翻轉動畫過程 */
            if (mCurrPointPos >= SUM_POINT_POS - 1)
            {//倒序
                mIsReverse = true;
                mCurrPointPos = SUM_POINT_POS - 2;//I Don't Know
                for (int i = 0; i < mBGCircles.size(); i++)
                {
                    CirclePoint point = mBGCircles.get(i);
                    if (i == mBGCircles.size() - 1)
                    {
                        point.setEnabled(true);
                    }
                    else
                    {
                        point.setEnabled(false);
                    }

                }
            }
            else if (mCurrPointPos < 0)
            {//順序
                mIsReverse = false;
                mCurrPointPos = 0;
                for (int i = 0; i < mBGCircles.size(); i++)
                {
                    CirclePoint point = mBGCircles.get(i);
                    if (i == 0)
                    {
                        point.setEnabled(false);
                    }
                    else
                    {
                        point.setEnabled(true);
                    }

                }
            }

            //每個階段恢復狀態,以及對背景圓的控制
            if (mIsReverse)
            {//倒序
                //恢復狀態
                for (CirclePoint point : mBallPoints)
                {
                    point.setOffsetX(mCanvasTranslateOffset);
                }
                mBGCircles.get(mCurrPointPos + 1).setEnabled(true);
            }
            else
            {//順序
                //恢復狀態
                for (CirclePoint point : mBallPoints)
                {
                    point.setOffsetX(0);
                }
                mBGCircles.get(mCurrPointPos).setEnabled(false);
            }
        }
    }

圓點的內部類,封裝了此動畫中所涉及到的點和球的參數信息(小球的點與背景球都是復用此類的)

    /**
     * 圓點內部類
     */
    private class CirclePoint
    {
        private final float mX;
        private final float mY;
        private float   mOffsetX = 0;
        private float   mOffsetY = 0;
        private boolean mEnabled = true;

        CirclePoint(float x, float y)
        {
            mX = x;
            mY = y;
        }

        float getX()
        {
            return mX + mOffsetX;
        }

        float getY()
        {
            return mY + mOffsetY;
        }

        void setOffsetX(float offsetX)
        {
            mOffsetX = offsetX;
        }

        void setOffsetY(float offsetY)
        {
            mOffsetY = offsetY;
        }

        public void setEnabled(boolean enabled)
        {
            mEnabled = enabled;
        }

        void draw(Canvas canvas, float r, Paint paint)
        {
            if (this.mEnabled)
            {
                canvas.drawCircle(this.getX(), this.getY(), r, paint);
            }
        }
    }

總結

小伙伴們,介紹就到這里了,要是想看更多細節,可以前往文章最下面的Github鏈接,如果大家覺得ok的話,希望能給個喜歡,最渴望的是在Github上給個star。謝謝了。

如果大家有什么更好的方案,或者想要實現的加載效果,可以給我留言或者私信我,我會想辦法實現出來給大家。謝謝支持。

演示

動畫效果演示

Github:zyao89/ZCustomView

作者:Zyao89;轉載請保留此行,謝謝;

個人博客:https://zyao89.cn

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,076評論 25 708
  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,200評論 4 61
  • 【Android 動畫】 動畫分類補間動畫(Tween動畫)幀動畫(Frame 動畫)屬性動畫(Property ...
    Rtia閱讀 6,213評論 1 38
  • 安裝iterm2 說明:mac系統上替代終端的命令行工具 安裝方法:直接從官網下載安裝即可,下載地址 安裝brew...
    欒呱呱閱讀 2,599評論 1 6
  • 剛做完手頭的工作空出點時間,閑來無聊就刷刷朋友圈,老媽又在朋友圈霸屏了,但我并不反感,反而很喜歡這樣的老媽。 我母...
    夏筱白閱讀 983評論 3 17