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

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

實(shí)現(xiàn)效果圖在最后,GIF有點(diǎn)大,手機(jī)流量請三思。

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

介紹

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

正文

參數(shù)變量初始化,用處我都在代碼里寫了注釋了,不懂得大家也可以問,有什么不對的,也希望大家?guī)兔χ赋?,謝謝了。如下:

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

首先初始化參數(shù),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();
    }

    /**
     * 背景圓點(diǎn)初始化
     */
    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);//默認(rèn)第一個(gè)圓不顯示
        mBGCircles.add(p_0);
        mBGCircles.add(p_1);
        mBGCircles.add(p_2);
        mBGCircles.add(p_3);
        mBGCircles.add(p_4);
    }

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

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

原理圖,來自其它地方

具體標(biāo)注點(diǎn),請對照我注釋中的P0~P11點(diǎn),如下:


    /**
     *      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個(gè)點(diǎn)坐標(biāo)形成路徑,最終繪制到畫布上。


    @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();
    }

這里是不同階段對應(yīng)的不同偏移量賦值,前三個(gè)階段是針對順序移動的操作,后三個(gè)階段是對應(yīng)的逆序時(shí)所需要的賦值操作,所有位移操作都是對小球的12點(diǎn)進(jìn)行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;
        }
    }

在下面的方法中,對小球動畫的各個(gè)階段進(jìn)行分布,各個(gè)點(diǎn)的偏移量進(jìn)行重置,以及順序倒序移動的切換邏輯進(jìn)行判斷,并且對背景圓也做了關(guān)聯(lián)處理。


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

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

            /* 重置并翻轉(zhuǎn)動畫過程 */
            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);
                    }

                }
            }

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

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

    /**
     * 圓點(diǎn)內(nèi)部類
     */
    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);
            }
        }
    }

總結(jié)

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

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

演示

動畫效果演示

Github:zyao89/ZCustomView

作者:Zyao89;轉(zhuǎn)載請保留此行,謝謝;

個(gè)人博客:https://zyao89.cn

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容