Android:你也可以自己寫一個可愛 & 小資風格的加載等待自定義View


前言

  • Android開發(fā)中,加載等待的需求 非常常見
  • 本文將手把手教你做 一款 可愛 & 小資風格的加載等待Android自定義View控件抚官,希望你們會喜歡哨查。
示意圖

已在Github開源:Kawaii_LoadingView正塌,歡迎 Star 笼痹!


目錄

示意圖

1. 簡介

一款 可愛 、清新 & 小資風格的 Android自定義View控件

已在Github開源:Kawaii_LoadingView,歡迎 Star 臂寝!

示意圖

2. 應用場景

App 長時間加載等待時,用于提示用戶進度 & 緩解用戶情緒


3. 特點

對比市面上的加載等待自定義控件摊灭,該控件Kawaii_LoadingView 的特點是:

3.1 樣式清新

  • 對比市面上 各種酷炫咆贬、眼花繚亂的加載等待自定義控件,該款 Kawaii_LoadingView清新 & 小資風格 簡直是一股清流
  • 同時帚呼,可根據(jù)您的App定位 & 主色進行顏色調整掏缎,使得控件更加符合App的形象皱蹦。具體如下:
示意圖
示意圖
示意圖
示意圖

3.2 使用簡單

僅需要3步驟 & 配置簡單。

具體請看文章:Android開源控件:一款你不可錯過的可愛 & 小資風格的加載等待自定義View

3.3 二次開發(fā)成本低

  • 本項目已在 Github上開源:Kawaii_LoadingView
  • 詳細的源碼分析文檔:具體請看本文的第6節(jié)

所以眷蜈,在其上做二次開發(fā) & 定制化成本非常低沪哺。


4. 具體使用

具體請看文章:Android開源控件:一款你不可錯過的可愛 & 小資風格的加載等待自定義View


5. 完整Demo地址

Carson_Ho的Github地址:Kawaii_LoadingView_TestDemo

最終示意圖.gif

6. 源碼分析

下面,我將手把手教你如何實現(xiàn)這款 可愛 & 小資風格的加載等待Android自定義View控件

6.1 準備說明

  • 方格排列說明
示意圖
  • 方塊類型說明
示意圖

6.2 動畫原理

  • 隱藏固定的2個方塊 & 移動方塊繼承其中1個的位置

注:只有外部方塊運動

  • 通過 屬性動畫 (平移 + 旋轉 = 組合動畫)改變移動方塊的位置 & 旋轉角度
  • 通過調用 invalidate() 重新繪制酌儒,從而實現(xiàn)動態(tài)的動畫效果
  • 具體原理圖如下:
示意圖

6.3 實現(xiàn)步驟

示意圖

下面我將詳細介紹每個步驟:

步驟1:初始化動畫屬性

  • 屬性說明:
示意圖
  • 具體屬性設置
示意圖
  • 添加屬性文件

attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="Kawaii_LoadingView">
        <attr name="half_BlockWidth" format="dimension" />
        <attr name="blockInterval" format="dimension" />
        <attr name="initPosition" format="integer" />
        <attr name="isClock_Wise" format="boolean" />
        <attr name="lineNumber" format="integer"  />
        <attr name="moveSpeed" format="integer"  />
        <attr name="blockColor" format="color"  />
        <attr name="moveBlock_Angle" format="float"  />
        <attr name="fixBlock_Angle" format="float"  />
        <attr name="move_Interpolator" format="reference"  />
    </declare-styleable>
</resources>
  • 具體源碼分析
    private void initAttrs(Context context, AttributeSet attrs) {

        // 控件資源名稱
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.Kawaii_LoadingView);

        // 一行的數(shù)量(最少3行)
        lineNumber = typedArray.getInteger(R.styleable.Kawaii_LoadingView_lineNumber, 3);
        if (lineNumber < 3) {
            lineNumber = 3;
        }

        // 半個方塊的寬度(dp)
        half_BlockWidth = typedArray.getDimension(R.styleable.Kawaii_LoadingView_half_BlockWidth, 30);
        // 方塊間隔寬度(dp)
        blockInterval = typedArray.getDimension(R.styleable.Kawaii_LoadingView_blockInterval, 10);

        // 移動方塊的圓角半徑
        moveBlock_Angle = typedArray.getFloat(R.styleable.Kawaii_LoadingView_moveBlock_Angle, 10);
        // 固定方塊的圓角半徑
        fixBlock_Angle = typedArray.getFloat(R.styleable.Kawaii_LoadingView_fixBlock_Angle, 30);
        // 通過設置兩個方塊的圓角半徑使得二者不同可以得到更好的動畫效果哦

        // 方塊顏色(使用十六進制代碼辜妓,如#333、#8e8e8e)
        int defaultColor = context.getResources().getColor(R.color.colorAccent); // 默認顏色
        blockColor = typedArray.getColor(R.styleable.Kawaii_LoadingView_blockColor, defaultColor);

        // 移動方塊的初始位置(即空白位置)
        initPosition = typedArray.getInteger(R.styleable.Kawaii_LoadingView_initPosition, 0);

        // 由于移動方塊只能是外部方塊忌怎,所以這里需要判斷方塊是否屬于外部方塊 -->關注1
        if (isInsideTheRect(initPosition, lineNumber)) {
            initPosition = 0;
        }
        // 動畫方向是否 = 順時針旋轉
        isClock_Wise = typedArray.getBoolean(R.styleable.Kawaii_LoadingView_isClock_Wise, true);

        // 移動方塊的移動速度
        // 注:不建議使用者將速度調得過快
        // 因為會導致ValueAnimator動畫對象頻繁重復的創(chuàng)建籍滴,存在內存抖動
        moveSpeed = typedArray.getInteger(R.styleable.Kawaii_LoadingView_moveSpeed, 250);

        // 設置移動方塊動畫的插值器
        int move_InterpolatorResId = typedArray.getResourceId(R.styleable.Kawaii_LoadingView_move_Interpolator,
                android.R.anim.linear_interpolator);
        move_Interpolator = AnimationUtils.loadInterpolator(context, move_InterpolatorResId);

        // 當方塊移動后,需要實時更新的空白方塊的位置
        mCurrEmptyPosition = initPosition;

        // 釋放資源
        typedArray.recycle();
    }

// 此步驟結束


    /**
     * 關注1:判斷方塊是否在內部
     */

    private boolean isInsideTheRect(int pos, int lineCount) {
        // 判斷方塊是否在第1行
        if (pos < lineCount) {
            return false;
            // 是否在最后1行
        } else if (pos > (lineCount * lineCount - 1 - lineCount)) {
            return false;
            // 是否在最后1行
        } else if ((pos + 1) % lineCount == 0) {
            return false;
            // 是否在第1行
        } else if (pos % lineCount == 0) {
            return false;
        }
        // 若不在4邊榴啸,則在內部
        return true;
    }
    // 回到原處

步驟2:初始化方塊對象 & 之間的關系

    private void init() {
        // 初始化畫筆
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(blockColor);

        // 初始化方塊對象 & 關系 ->>關注1
        initBlocks(initPosition);

    }

    /**
     * 關注1
     * 初始化方塊對象异逐、之間的關系
     * 參數(shù)說明:initPosition = 移動方塊的初始位置
     */
    private void initBlocks(int initPosition) {

        // 1. 創(chuàng)建總方塊的數(shù)量(固定方塊) = lineNumber * lineNumber
        // lineNumber = 方塊的行數(shù)
        // fixedBlock = 固定方塊 類 ->>關注2
        mfixedBlocks = new fixedBlock[lineNumber * lineNumber];

        // 2. 創(chuàng)建方塊
        for (int i = 0; i < mfixedBlocks.length; i++) {

            // 創(chuàng)建固定方塊 & 保存到數(shù)組中
            mfixedBlocks[i] = new fixedBlock();

            // 對固定方塊對象里的變量進行賦值
            mfixedBlocks[i].index = i;
            // 對方塊是否顯示進行判斷
            // 若該方塊的位置 = 移動方塊的初始位置,則隱藏插掂;否則顯示
            mfixedBlocks[i].isShow = initPosition == i ? false : true;
            mfixedBlocks[i].rectF = new RectF();
        }

        // 3. 創(chuàng)建移動的方塊(1個) ->>關注3
        mMoveBlock = new MoveBlock();
        mMoveBlock.rectF = new RectF();
        mMoveBlock.isShow = false;

        // 4. 關聯(lián)外部方塊的位置
        // 因為外部的方塊序號 ≠ 0灰瞻、1、2…排列辅甥,通過 next變量(指定其下一個)酝润,一個接一個連接 外部方塊 成圈
        // ->>關注4
        relate_OuterBlock(mfixedBlocks, isClock_Wise);

    }
    // 此步驟結束

    /**
     * 關注2:固定方塊 類(內部類)
     */
    private class fixedBlock {

        // 存儲方塊的坐標位置參數(shù)
        RectF rectF;

        // 方塊對應序號
        int index;

        // 標志位:判斷是否需要繪制
        boolean isShow;

        // 指向下一個需要移動的位置
        fixedBlock next;
        // 外部的方塊序號 ≠ 0、1璃弄、2…排列要销,通過 next變量(指定其下一個),一個接一個連接 外部方塊 成圈

    }
    // 請回到原處

    /**
     * 關注3
     *:移動方塊類(內部類)
     */
    private class MoveBlock {
        // 存儲方塊的坐標位置參數(shù)
        RectF rectF;

        // 方塊對應序號
        int index;

        // 標志位:判斷是否需要繪制
        boolean isShow;

        // 旋轉中心坐標
        // 移動時的旋轉中心(X夏块,Y)
        float cx;
        float cy;
    }
    // 請回到原處



    /**
     * 關注4:將外部方塊的位置關聯(lián)起來
     * 算法思想: 按照第1行疏咐、最后1行、第1列 & 最后1列的順序脐供,分別讓每個外部方塊的next屬性 == 下一個外部方塊的位置浑塞,最終對整個外部方塊的位置進行關聯(lián)
     *  注:需要考慮移動方向變量isClockwise( 順 Or 逆時針)
     */

    private void relate_OuterBlock(fixedBlock[] fixedBlocks, boolean isClockwise) {
        int lineCount = (int) Math.sqrt(fixedBlocks.length);

        // 情況1:關聯(lián)第1行
        for (int i = 0; i < lineCount; i++) {
            // 位于最左邊
            if (i % lineCount == 0) {
                fixedBlocks[i].next = isClockwise ? fixedBlocks[i + lineCount] : fixedBlocks[i + 1];
                // 位于最右邊
            } else if ((i + 1) % lineCount == 0) {
                fixedBlocks[i].next = isClockwise ? fixedBlocks[i - 1] : fixedBlocks[i + lineCount];
                // 中間
            } else {
                fixedBlocks[i].next = isClockwise ? fixedBlocks[i - 1] : fixedBlocks[i + 1];
            }
        }
        // 情況2:關聯(lián)最后1行
        for (int i = (lineCount - 1) * lineCount; i < lineCount * lineCount; i++) {
            // 位于最左邊
            if (i % lineCount == 0) {
                fixedBlocks[i].next = isClockwise ? fixedBlocks[i + 1] : fixedBlocks[i - lineCount];
                // 位于最右邊
            } else if ((i + 1) % lineCount == 0) {
                fixedBlocks[i].next = isClockwise ? fixedBlocks[i - lineCount] : fixedBlocks[i - 1];
                // 中間
            } else {
                fixedBlocks[i].next = isClockwise ? fixedBlocks[i + 1] : fixedBlocks[i - 1];
            }
        }

        // 情況3:關聯(lián)第1列
        for (int i = 1 * lineCount; i <= (lineCount - 1) * lineCount; i += lineCount) {
            // 若是第1列最后1個
            if (i == (lineCount - 1) * lineCount) {
                fixedBlocks[i].next = isClockwise ? fixedBlocks[i + 1] : fixedBlocks[i - lineCount];
                continue;
            }
            fixedBlocks[i].next = isClockwise ? fixedBlocks[i + lineCount] : fixedBlocks[i - lineCount];
        }

        // 情況4:關聯(lián)最后1列
        for (int i = 2 * lineCount - 1; i <= lineCount * lineCount - 1; i += lineCount) {
            // 若是最后1列最后1個
            if (i == lineCount * lineCount - 1) {
                fixedBlocks[i].next = isClockwise ? fixedBlocks[i - lineCount] : fixedBlocks[i - 1];
                continue;
            }
            fixedBlocks[i].next = isClockwise ? fixedBlocks[i - lineCount] : fixedBlocks[i + lineCount];
        }
    }
    // 請回到原處

步驟3:設置方塊初始位置

    // 該步驟寫在onSizeChanged()
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        // 調用時刻:onCreate之后onDraw之前調用;view的大小發(fā)生改變就會調用該方法
        // 使用場景:用于屏幕的大小改變時政己,需要根據(jù)屏幕寬高來決定的其他變量可以在這里進行初始化操作
        super.onSizeChanged(w, h, oldw, oldh);

        int measuredWidth = getMeasuredWidth();
        int measuredHeight = getMeasuredHeight();

        // 1. 設置移動方塊的旋轉中心坐標
        int cx = measuredWidth / 2;
        int cy = measuredHeight / 2;

        // 2. 設置固定方塊的位置 ->>關注1
        fixedBlockPosition(mfixedBlocks, cx, cy, blockInterval, half_BlockWidth);
        // 3. 設置移動方塊的位置 ->>關注2
        MoveBlockPosition(mfixedBlocks, mMoveBlock, initPosition, isClock_Wise);
    }

// 此步驟結束

    /**
     * 關注1:設置 固定方塊位置
     */
    private void fixedBlockPosition(fixedBlock[] fixedBlocks, int cx, int cy, float dividerWidth, float halfSquareWidth) {

        // 1. 確定第1個方塊的位置
        // 分為2種情況:行數(shù) = 偶 / 奇數(shù)時
        // 主要是是數(shù)學知識酌壕,此處不作過多描述
        float squareWidth = halfSquareWidth * 2;
        int lineCount = (int) Math.sqrt(fixedBlocks.length);
        float firstRectLeft = 0;
        float firstRectTop = 0;

        // 情況1:當行數(shù) = 偶數(shù)時
        if (lineCount % 2 == 0) {
            int squareCountInAline = lineCount / 2;
            int diviCountInAline = squareCountInAline - 1;
            float firstRectLeftTopFromCenter = squareCountInAline * squareWidth
                    + diviCountInAline * dividerWidth
                    + dividerWidth / 2;
            firstRectLeft = cx - firstRectLeftTopFromCenter;
            firstRectTop = cy - firstRectLeftTopFromCenter;

            // 情況2:當行數(shù) = 奇數(shù)時
        } else {
            int squareCountInAline = lineCount / 2;
            int diviCountInAline = squareCountInAline;
            float firstRectLeftTopFromCenter = squareCountInAline * squareWidth
                    + diviCountInAline * dividerWidth
                    + halfSquareWidth;
            firstRectLeft = cx - firstRectLeftTopFromCenter;
            firstRectTop = cy - firstRectLeftTopFromCenter;
            firstRectLeft = cx - firstRectLeftTopFromCenter;
            firstRectTop = cy - firstRectLeftTopFromCenter;
        }

        // 2. 確定剩下的方塊位置
        // 思想:把第一行方塊位置往下移動即可
        // 通過for循環(huán)確定:第一個for循環(huán) = 行,第二個 = 列
        for (int i = 0; i < lineCount; i++) {//行
            for (int j = 0; j < lineCount; j++) {//列
                if (i == 0) {
                    if (j == 0) {
                        fixedBlocks[0].rectF.set(firstRectLeft, firstRectTop,
                                firstRectLeft + squareWidth, firstRectTop + squareWidth);
                    } else {
                        int currIndex = i * lineCount + j;
                        fixedBlocks[currIndex].rectF.set(fixedBlocks[currIndex - 1].rectF);
                        fixedBlocks[currIndex].rectF.offset(dividerWidth + squareWidth, 0);
                    }
                } else {
                    int currIndex = i * lineCount + j;
                    fixedBlocks[currIndex].rectF.set(fixedBlocks[currIndex - lineCount].rectF);
                    fixedBlocks[currIndex].rectF.offset(0, dividerWidth + squareWidth);
                }
            }
        }
    }

// 回到原處

    /**
     * 關注2:設置移動方塊的位置
     */
    private void MoveBlockPosition(fixedBlock[] fixedBlocks,
                                   MoveBlock moveBlock, int initPosition, boolean isClockwise) {

        // 移動方塊位置 = 設置初始的空出位置 的下一個位置(next)
        // 下一個位置 通過 連接的外部方塊位置確定
        fixedBlock fixedBlock = fixedBlocks[initPosition];
        moveBlock.rectF.set(fixedBlock.next.rectF);
    }
// 回到原處

步驟4:繪制方塊

    // 此步驟寫到onDraw()中
    @Override
    protected void onDraw(Canvas canvas) {

        // 1. 繪制內部方塊(固定的)
        for (int i = 0; i < mfixedBlocks.length; i++) {
            // 根據(jù)標志位判斷是否需要繪制
            if (mfixedBlocks[i].isShow) {
                // 傳入方塊位置參數(shù)歇由、圓角 & 畫筆屬性
                canvas.drawRoundRect(mfixedBlocks[i].rectF, fixBlock_Angle, fixBlock_Angle, mPaint);
            }
        }
        // 2. 繪制移動的方塊
        if (mMoveBlock.isShow) {
            canvas.rotate(isClock_Wise ? mRotateDegree : -mRotateDegree, mMoveBlock.cx, mMoveBlock.cy);
            canvas.drawRoundRect(mMoveBlock.rectF, moveBlock_Angle, moveBlock_Angle, mPaint);
        }

    }

步驟5:設置動畫

實現(xiàn)該動畫的步驟包括:設置平移動畫卵牍、旋轉動畫 & 組合動畫。

1.設置平移動畫

    private ValueAnimator createTranslateValueAnimator(fixedBlock currEmptyfixedBlock,
                                                       fixedBlock moveBlock) {
        float startAnimValue = 0;
        float endAnimValue = 0;
        PropertyValuesHolder left = null;
        PropertyValuesHolder top = null;

        // 1. 設置移動速度
        ValueAnimator valueAnimator = new ValueAnimator().setDuration(moveSpeed);

        // 2. 設置移動方向
        // 情況分為:4種沦泌,分別是移動方塊向左糊昙、右移動 和 上、下移動
        // 注:需考慮 旋轉方向(isClock_Wise)谢谦,即順逆時針 ->>關注1
        if (isNextRollLeftOrRight(currEmptyfixedBlock, moveBlock)) {

            // 情況1:順時針且在第一行 / 逆時針且在最后一行時释牺,移動方塊向右移動
            if (isClock_Wise && currEmptyfixedBlock.index > moveBlock.index || !isClock_Wise && currEmptyfixedBlock.index > moveBlock.index) {

                startAnimValue = moveBlock.rectF.left;
                endAnimValue = moveBlock.rectF.left + blockInterval;

                // 情況2:順時針且在最后一行 / 逆時針且在第一行萝衩,移動方塊向左移動
            } else if (isClock_Wise && currEmptyfixedBlock.index < moveBlock.index
                    || !isClock_Wise && currEmptyfixedBlock.index < moveBlock.index) {

                startAnimValue = moveBlock.rectF.left;
                endAnimValue = moveBlock.rectF.left - blockInterval;
            }

            // 設置屬性值
            left = PropertyValuesHolder.ofFloat("left", startAnimValue, endAnimValue);
            valueAnimator.setValues(left);

        } else {
            // 情況3:順時針且在最左列 / 逆時針且在最右列,移動方塊向上移動
            if (isClock_Wise && currEmptyfixedBlock.index < moveBlock.index
                    || !isClock_Wise && currEmptyfixedBlock.index < moveBlock.index) {

                startAnimValue = moveBlock.rectF.top;
                endAnimValue = moveBlock.rectF.top - blockInterval;

                // 情況4:順時針且在最右列 / 逆時針且在最左列船侧,移動方塊向下移動
            } else if (isClock_Wise && currEmptyfixedBlock.index > moveBlock.index
                    || !isClock_Wise && currEmptyfixedBlock.index > moveBlock.index) {
                startAnimValue = moveBlock.rectF.top;
                endAnimValue = moveBlock.rectF.top + blockInterval;
            }

            // 設置屬性值
            top = PropertyValuesHolder.ofFloat("top", startAnimValue, endAnimValue);
            valueAnimator.setValues(top);
        }

        // 3. 通過監(jiān)聽器更新屬性值
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                Object left = animation.getAnimatedValue("left");
                Object top = animation.getAnimatedValue("top");
                if (left != null) {
                    mMoveBlock.rectF.offsetTo((Float) left, mMoveBlock.rectF.top);
                }
                if (top != null) {
                    mMoveBlock.rectF.offsetTo(mMoveBlock.rectF.left, (Float) top);
                }
                // 實時更新旋轉中心 ->>關注2
                setMoveBlockRotateCenter(mMoveBlock, isClock_Wise);

                // 更新繪制
                invalidate();
            }
        });
        return valueAnimator;
    }
// 此步驟分析完畢

/**
     * 關注1:判斷移動方向
     * 即上下 or 左右
     */
    private boolean isNextRollLeftOrRight(fixedBlock currEmptyfixedBlock, fixedBlock rollSquare) {
        if (currEmptyfixedBlock.rectF.left - rollSquare.rectF.left == 0) {
            return false;
        } else {
            return true;
        }
    }
// 回到原處

/**
     * 關注2:實時更新移動方塊的旋轉中心
     * 因為方塊在平移旋轉過程中,旋轉中心也會跟著改變厅各,因此需要改變MoveBlock的旋轉中心(cx,cy)
     */

    private void setMoveBlockRotateCenter(MoveBlock moveBlock, boolean isClockwise) {

        // 情況1:以移動方塊的左上角為旋轉中心
        if (moveBlock.index == 0) {
            moveBlock.cx = moveBlock.rectF.right;
            moveBlock.cy = moveBlock.rectF.bottom;

            // 情況2:以移動方塊的右下角為旋轉中心
        } else if (moveBlock.index == lineNumber * lineNumber - 1) {
            moveBlock.cx = moveBlock.rectF.left;
            moveBlock.cy = moveBlock.rectF.top;

            // 情況3:以移動方塊的左下角為旋轉中心
        } else if (moveBlock.index == lineNumber * (lineNumber - 1)) {
            moveBlock.cx = moveBlock.rectF.right;
            moveBlock.cy = moveBlock.rectF.top;

            // 情況4:以移動方塊的右上角為旋轉中心
        } else if (moveBlock.index == lineNumber - 1) {
            moveBlock.cx = moveBlock.rectF.left;
            moveBlock.cy = moveBlock.rectF.bottom;
        }

        //以下判斷與旋轉方向有關:即順 or 逆順時針

        // 情況1:左邊
        else if (moveBlock.index % lineNumber == 0) {
            moveBlock.cx = moveBlock.rectF.right;
            moveBlock.cy = isClockwise ? moveBlock.rectF.top : moveBlock.rectF.bottom;

            // 情況2:上邊
        } else if (moveBlock.index < lineNumber) {
            moveBlock.cx = isClockwise ? moveBlock.rectF.right : moveBlock.rectF.left;
            moveBlock.cy = moveBlock.rectF.bottom;

            // 情況3:右邊
        } else if ((moveBlock.index + 1) % lineNumber == 0) {
            moveBlock.cx = moveBlock.rectF.left;
            moveBlock.cy = isClockwise ? moveBlock.rectF.bottom : moveBlock.rectF.top;

            // 情況4:下邊
        } else if (moveBlock.index > (lineNumber - 1) * lineNumber) {
            moveBlock.cx = isClockwise ? moveBlock.rectF.left : moveBlock.rectF.right;
            moveBlock.cy = moveBlock.rectF.top;
        }
    }
    // 回到原處

   

2. 設置旋轉動畫

private ValueAnimator createMoveValueAnimator() {

        // 通過屬性動畫進行設置
        ValueAnimator moveAnim = ValueAnimator.ofFloat(0, 90).setDuration(moveSpeed);

        moveAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                Object animatedValue = animation.getAnimatedValue();

                // 賦值
                mRotateDegree = (float) animatedValue;

                // 更新視圖
                invalidate();
            }
        });
        return moveAnim;
    }
    // 此步驟完畢

3. 設置組合動畫

private void setAnimation() {

        // 1. 獲取固定方塊當前的空位置镜撩,即移動方塊當前位置
        fixedBlock currEmptyfixedBlock = mfixedBlocks[mCurrEmptyPosition];
        // 2. 獲取移動方塊的到達位置,即固定方塊當前空位置的下1個位置
        fixedBlock movedBlock = currEmptyfixedBlock.next;

        // 3. 設置動畫變化的插值器
        mAnimatorSet.setInterpolator(move_Interpolator);
        mAnimatorSet.playTogether(translateConrtroller, moveConrtroller);
        mAnimatorSet.addListener(new AnimatorListenerAdapter() {

            // 4. 動畫開始時進行一些設置
            @Override
            public void onAnimationStart(Animator animation) {

                // 每次動畫開始前都需要更新移動方塊的位置 ->>關注1
                updateMoveBlock();

                // 讓移動方塊的初始位置的下個位置也隱藏 = 兩個隱藏的方塊
                mfixedBlocks[mCurrEmptyPosition].next.isShow = false;

                // 通過標志位將移動的方塊顯示出來
                mMoveBlock.isShow = true;
            }

            // 5. 結束時進行一些設置
            @Override
            public void onAnimationEnd(Animator animation) {
                isMoving = false;
                mfixedBlocks[mCurrEmptyPosition].isShow = true;
                mCurrEmptyPosition = mfixedBlocks[mCurrEmptyPosition].next.index;

                // 將移動的方塊隱藏
                mMoveBlock.isShow = false;

                // 通過標志位判斷動畫是否要循環(huán)播放
                if (mAllowRoll) {
                    startMoving();
                }
            }
        });

// 此步驟分析完畢

/**
     * 關注1:更新移動方塊的位置
     */

    private void updateMoveBlock() {

        mMoveBlock.rectF.set(mfixedBlocks[mCurrEmptyPosition].next.rectF);
        mMoveBlock.index = mfixedBlocks[mCurrEmptyPosition].next.index;
        setMoveBlockRotateCenter(mMoveBlock, isClock_Wise);
    }
    // 回到原處


步驟6:啟動動畫

public void startMoving() {

        // 1. 根據(jù)標志位 & 視圖是否可見確定是否需要啟動動畫
        // 此處設置是為了方便手動 & 自動停止動畫
        if (isMoving || getVisibility() != View.VISIBLE ) {
            return;
        }

        // 2. 設置標記位:以便是否停止動畫
        isMoving = true;
        mAllowRoll = true;

        // 3. 啟動動畫
        mAnimatorSet.start();

    // 停止動畫
    public void stopMoving() {
        // 通過標記位來設置
        mAllowRoll = false;
    }

7. 貢獻代碼

  • 希望你們能和我一起完善這款清新 & 小資風格的自定義控件袁梗,具體請看:貢獻代碼說明
  • 關于該開源項目的意見 & 建議可在Issue上提出。歡迎 Star 憔古!

Github開源地址:Kawaii_LoadingView


8. 總結

  • 相信你一定會喜歡上 這款可愛遮怜、清新 & 小資風格的加載等待自定義控件

已在Github上開源:Kawaii_LoadingView,歡迎 Star 鸿市!

示意圖

a. 手把手教你實現(xiàn)一個簡單好用的搜索框(含歷史搜索記錄)
b. 你需要一款簡單實用的SuperEditText(一鍵刪除&自定義樣式))
c. Android 自定義View實戰(zhàn)系列 :時間軸

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末焰情,一起剝皮案震驚了整個濱河市陌凳,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌内舟,老刑警劉巖合敦,帶你破解...
    沈念sama閱讀 219,366評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異验游,居然都是意外死亡充岛,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評論 3 395
  • 文/潘曉璐 我一進店門耕蝉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來崔梗,“玉大人,你說我怎么就攤上這事垒在〕淳悖” “怎么了?”我有些...
    開封第一講書人閱讀 165,689評論 0 356
  • 文/不壞的土叔 我叫張陵爪膊,是天一觀的道長权悟。 經(jīng)常有香客問我,道長推盛,這世上最難降的妖魔是什么峦阁? 我笑而不...
    開封第一講書人閱讀 58,925評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮耘成,結果婚禮上榔昔,老公的妹妹穿的比我還像新娘驹闰。我一直安慰自己,他們只是感情好撒会,可當我...
    茶點故事閱讀 67,942評論 6 392
  • 文/花漫 我一把揭開白布嘹朗。 她就那樣靜靜地躺著,像睡著了一般诵肛。 火紅的嫁衣襯著肌膚如雪屹培。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,727評論 1 305
  • 那天怔檩,我揣著相機與錄音褪秀,去河邊找鬼。 笑死薛训,一個胖子當著我的面吹牛媒吗,可吹牛的內容都是我干的。 我是一名探鬼主播乙埃,決...
    沈念sama閱讀 40,447評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼闸英,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了介袜?” 一聲冷哼從身側響起自阱,我...
    開封第一講書人閱讀 39,349評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎米酬,沒想到半個月后沛豌,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,820評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡赃额,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,990評論 3 337
  • 正文 我和宋清朗相戀三年加派,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片跳芳。...
    茶點故事閱讀 40,127評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡芍锦,死狀恐怖,靈堂內的尸體忽然破棺而出飞盆,到底是詐尸還是另有隱情娄琉,我是刑警寧澤,帶...
    沈念sama閱讀 35,812評論 5 346
  • 正文 年R本政府宣布吓歇,位于F島的核電站孽水,受9級特大地震影響,放射性物質發(fā)生泄漏城看。R本人自食惡果不足惜女气,卻給世界環(huán)境...
    茶點故事閱讀 41,471評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望测柠。 院中可真熱鬧炼鞠,春花似錦缘滥、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至霎肯,卻和暖如春擎颖,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背姿现。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評論 1 272
  • 我被黑心中介騙來泰國打工肠仪, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留肖抱,地道東北人备典。 一個月前我還...
    沈念sama閱讀 48,388評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像意述,于是被迫代替她去往敵國和親提佣。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,066評論 2 355

推薦閱讀更多精彩內容