1. 程式人生 > >小清新載入等待控制元件

小清新載入等待控制元件

本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出

正文

背景

從錘子手機上看到的效果,但是找不到在那個應用哪裡看到的了,於是憑著記憶的效果實現了一下。

效果圖

demo1 圖1(效果圖1)

demo2 圖2(效果圖2)

使用方式

demo3 圖3(使用說明圖)

  • half_rect_width:半個方塊的寬度,單位dp
  • rect_divier_width:方塊之間間隔寬度,單位dp
  • start_empty_position:初始空出的位置
  • is_clockwise:是否順時針旋轉
  • line_count:一行的數量,最少為3
  • fix_round_cornor:固定的方框的圓角半徑
  • roll_round_cornor:旋轉的方框的圓角半徑,如果這兩個圓角半徑設定成不一樣的值就會得到上面圖1的效果,設定成一樣就是圖2.
  • roll_when_show_stop_when_hide:是否自動開始自定旋轉,如果設定為false,則需要手動呼叫startRoll()方法(下文會提到)才會開始運動,設定為true則設定View.Visibility就會自動開始旋轉。
  • square_color:方塊的顏色。使用十六進位制程式碼的形式(如:#333、#8e8e8e)

講解實現方法之前,首先要說明一下方格的排列方式是從左到右,從上到下,也就是如果line_count設定為3,那麼方格的序號如下圖:

這裡寫圖片描述 圖4(序號排列說明)

實現思路:

自定義控制元件最主要的就是如何去準備要展示給使用者看的東西,東西有了之後,我們在onDraw方法裡面按部就班的畫出來就可以了。接下來就帶大家來走一走我準備的整個過程。其實整個過程就像做菜,準備材料(準備資料),加調味料(處理初始資料),翻炒(編寫邏輯),這一切都是在鍋中完成的,這個鍋就是我們的onDraw方法,我們把所有的一些都準備好,然後扔進鍋(onDraw)裡面。

最終的繪製分為兩步:

  • 繪製固定的方塊
  • 繪製滾動的方塊

當運動的時候將固定的方框中的兩個方塊隱藏,然後讓滾動的方塊繼承其中一個的位置,然後通過屬性動畫改變其位置的值以及旋轉角度的值,最終呼叫invalidate()重繪讓其動起來。
這裡寫圖片描述

圖5(繪製原理圖示)

①(控制元件精髓就在此處)根據配置準備繪製的資料

處理自定義屬性:

private void initAttrs(Context context, AttributeSet attrs) {
    TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RollSquareView);
    //行列數
    mLineCount = typedArray.getInteger(R.styleable.RollSquareView_line_count, 3);
    //旋轉的方塊圓角
    mRollRoundCornor = typedArray.getFloat(R.styleable.RollSquareView_roll_round_cornor, 10);
    ...
    其他屬性省略,請大家看原始碼

    //開始的空格位置
    mStartEmptyPosition = typedArray.getInteger(R.styleable.RollSquareView_start_empty_position, 0);
    if (isInsideTheRect(mStartEmptyPosition, mLineCount)) {
        mStartEmptyPosition = 0;
    }
    //當動態滾動的時候實時更新的空格位置
    mCurrEmptyPosition = mStartEmptyPosition;
    typedArray.recycle();
}

當選擇空格位置不是外圍的方塊序號的時候,自動選擇0位置,判斷是否外圍一圈的演算法如下,純數學知識:

這裡寫圖片描述 圖6(綠色框出來的就是非外圍的方塊)

private boolean isInsideTheRect(int pos, int lineCount) {
    if (pos < lineCount) {//是否第一行
        return false;
    } else if (pos > (lineCount * lineCount - 1 - lineCount)) {//是否最後一行
        return false;
    } else if ((pos + 1) % lineCount == 0) {//是否右邊
        return false;
    } else if (pos % lineCount == 0) {//是否左邊
        return false;
    }
    //四邊都不在,那就是在內部了
    return true;
}

初始化方塊的方法:

private void initSquares(int startEmptyPosition) {
    //建立mLineCount * mLineCount個方塊
    mFixSquares = new FixSquare[mLineCount * mLineCount];
    for (int i = 0; i < mFixSquares.length; i++) {
        mFixSquares[i] = new FixSquare();
        mFixSquares[i].index = i;
        mFixSquares[i].isShow = startEmptyPosition == i ? false : true;
        mFixSquares[i].rectF = new RectF();
    }
    //外圈連結起來
    linkTheOuterSquare(mFixSquares, mIsClockwise);//下文講解
    //建立1個滾動方塊
    mRollSquare = new RollSquare();
    mRollSquare.rectF = new RectF();
    mRollSquare.isShow = false;
}

兩種方塊都使用內部類定義,程式碼如下:

private class FixSquare {
    RectF rectF;//需要繪製的方塊
    int index;//所在的序號
    boolean isShow;//是否需要繪製
    FixSquare next;//指向下一個需要滾動的位置,順時針和逆時針相反
}

private class RollSquare {
    RectF rectF;//需要繪製的方塊
    int index;//所在的序號
    boolean isShow;//是否需要繪製
    /**
     * 旋轉中心座標
     */
    float cx;//滾動的時候的旋轉中心x
    float cy;//滾動的時候的旋轉中心y
}

我們可以看到固定的方塊FixSquare中有一個next變數:

FixSquare next;//指向下一個需要滾動的位置,順時針和逆時針相反

因為我們需要將外圍的一圈方塊都連結起來,但是現在有一個問題就是外圍的方塊序號並不是按照0、1、2…排列的,因此我定義了一個next變數用於指定其下一個,這樣一個接一個的就把外圍連成一圈了。演算法如下,可能第一次看這個方法的小夥伴需要看一小會兒,因為需要適配行數3個以上的需求,因此都是動態變化的,因此都是一些數學公式,這裡篇幅有限不一一講解,大家可以順著註釋看看規律就很容易理解了,這個方法的主要目的就是為了讓每個FixSquare的“FixSquare next”都賦上值,最終將外圍都連成一圈,不要忘記考慮順逆時針isClockwise這個變數哦:

private void linkTheOuterSquare(FixSquare[] fixSquares, boolean isClockwise) {
    int lineCount = (int) Math.sqrt(mFixSquares.length);
    //連線第一行
    for (int i = 0; i < lineCount; i++) {
        if (i % lineCount == 0) {//位於最左邊
            fixSquares[i].next = isClockwise ? fixSquares[i + lineCount] : fixSquares[i + 1];
        } else if ((i + 1) % lineCount == 0) {//位於最右邊
            fixSquares[i].next = isClockwise ? fixSquares[i - 1] : fixSquares[i + lineCount];
        } else {//中間
            fixSquares[i].next = isClockwise ? fixSquares[i - 1] : fixSquares[i + 1];
        }
    }
    //連線最後一行
    for (int i = (lineCount - 1) * lineCount; i < lineCount * lineCount; i++) {
        if (i % lineCount == 0) {//位於最左邊
            fixSquares[i].next = isClockwise ? fixSquares[i + 1] : fixSquares[i - lineCount];
        } else if ((i + 1) % lineCount == 0) {//位於最右邊
            fixSquares[i].next = isClockwise ? fixSquares[i - lineCount] : fixSquares[i - 1];
        } else {//中間
            fixSquares[i].next = isClockwise ? fixSquares[i + 1] : fixSquares[i - 1];
        }
    }
    //連線左邊
    for (int i = 1 * lineCount; i <= (lineCount - 1) * lineCount; i += lineCount) {
        if (i == (lineCount - 1) * lineCount) {//如果是左下角的一個
            fixSquares[i].next = isClockwise ? fixSquares[i + 1] : fixSquares[i - lineCount];
            continue;
        }
        fixSquares[i].next = isClockwise ? fixSquares[i + lineCount] : fixSquares[i - lineCount];
    }
    //連線右邊
    for (int i = 2 * lineCount - 1; i <= lineCount * lineCount - 1; i += lineCount) {
        if (i == lineCount * lineCount - 1) {//如果是右下角的一個
            fixSquares[i].next = isClockwise ? fixSquares[i - lineCount] : fixSquares[i - 1];
            continue;
        }
        fixSquares[i].next = isClockwise ? fixSquares[i - lineCount] : fixSquares[i + lineCount];
    }
}

固定方塊的位置,分別使用fixFixSquarePosition和fixRollSquarePosition兩個方法來固定FixSquare和RollSquare:

private void fixFixSquarePosition(FixSquare[] fixSquares, int cx, int cy, float dividerWidth, float halfSquareWidth) {
    //確定第一個rect的位置
    float squareWidth = halfSquareWidth * 2;
    int lineCount = (int) Math.sqrt(fixSquares.length);
    float firstRectLeft = 0;
    float firstRectTop = 0;
    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;
    } else {//奇數
        int squareCountInAline = lineCount / 2;
        int diviCountInAline = squareCountInAline;
        float firstRectLeftTopFromCenter = squareCountInAline * squareWidth
                + diviCountInAline * dividerWidth
                + halfSquareWidth;
        firstRectLeft = cx - firstRectLeftTopFromCenter;
        firstRectTop = cy - firstRectLeftTopFromCenter;
    }
    for (int i = 0; i < lineCount; i++) {//行
        for (int j = 0; j < lineCount; j++) {//列
            if (i == 0) {
                if (j == 0) {
                    fixSquares[0].rectF.set(firstRectLeft, firstRectTop,
                            firstRectLeft + squareWidth, firstRectTop + squareWidth);
                } else {
                    int currIndex = i * lineCount + j;
                    fixSquares[currIndex].rectF.set(fixSquares[currIndex - 1].rectF);
                    fixSquares[currIndex].rectF.offset(dividerWidth + squareWidth, 0);
                }
            } else {
                int currIndex = i * lineCount + j;
                fixSquares[currIndex].rectF.set(fixSquares[currIndex - lineCount].rectF);
                fixSquares[currIndex].rectF.offset(0, dividerWidth + squareWidth);
            }
        }
    }
}

private void fixRollSquarePosition(FixSquare[] fixSquares,
                                   RollSquare rollSquare, int startEmptyPosition) {
    FixSquare fixSquare = fixSquares[startEmptyPosition];
    rollSquare.rectF.set(fixSquare.next.rectF);
}

對於方法fixFixSquarePosition:

  • 通過引數有控制元件的中點的x和y座標,cx和cy,加上行數,方塊的寬以及方塊間隔;
  • 通過以上引數很容易就可以通過計算得出第0個方塊的left和top值,分別是firstRectLeft和firstRectTop;
  • 因為行數可能是奇數也可能是偶數,所以分為奇數和偶數兩種計算方式;
  • 然後我把第一行的方塊都固定下來之後,剩下的方塊只需要往下平移即可固定下來了;
  • 第一個for迴圈表示行,第二個表示列,都是簡單的數學計數知識,不過多闡述。

對於方法fixRollSquarePosition:

  • 因為我們已經從初始化的操作中知道哪一個位置是空的,startEmptyPosition;
  • 而且已經把外圍的方塊連成了環(通過next關聯),上文的linkTheOuterSquare方法;
  • 因此可以很容易確定下來旋轉的方塊所要開始運動的初始位置。

②兩種運動,平移 和 90度旋轉

這裡主要講解一下思路,使用屬性動畫建立兩個動畫,一個是平移動畫,一個是旋轉動畫,如下圖,然後使用AnimatorSet將兩個連線起來,同時執行。

這裡寫圖片描述圖7(平移動畫)

這裡寫圖片描述圖8(旋轉動畫)

  • 由於篇幅有限,加之方法比較長,這裡不貼出,感興趣的朋友可以去原碼檢視:
  • createTranslateValueAnimator方法 和 createRollValueAnimator方法;
  • 其中值得關注的點是:需要考慮順逆時針,以及實時更新旋轉方塊的旋轉中心,因為平移過程中旋轉中心也會跟著改變的,因此需要改變RollSquare的cx和cy,具體的邏輯就在setRollSquareRotateCenter方法中,呼叫的時機當然就是在動畫運動的過程中啦(見onAnimationUpdate)。

③迴圈起來把

  • 通過呼叫startRoll方法,會建立一次動畫,當動畫結束的時候(onAnimationEnd),重新呼叫startRoll方法,以達到迴圈的目的。這裡相信大家都明白,就跟handler迴圈傳送訊息一樣。
  • 這裡有一點需要注意的就是如果動畫速度調的很快,那麼會導致ValueAnimator動畫物件頻繁重複的建立,可能會有記憶體抖動的風險;因此建議使用者不要將速度調的太塊,不過這個控制元件的後期的迭代我可能將這個動畫物件換成始終只有一個ValueAnimator的情況。

④停止條件

  • 在動畫結束準備重新呼叫startRoll方法之前做一個變數判斷,來控制是否需要迴圈呼叫,如下:
if (mAllowRoll) {
    startRoll();
}
  • 當我們呼叫stopRoll方法的時候,mAllowRoll會變為false,呼叫startRoll的時候,mAllowRoll會變為true;

⑤最後,畫出來

@Override
protected void onDraw(Canvas canvas) {
    for (int i = 0; i < mFixSquares.length; i++) {
        if (mFixSquares[i].isShow) {
            canvas.drawRoundRect(mFixSquares[i].rectF, mFixRoundCornor, mFixRoundCornor, mPaint);
        }
    }
    if (mRollSquare.isShow) {
        canvas.rotate(mIsClockwise ? mRotateDegree : -mRotateDegree, mRollSquare.cx, mRollSquare.cy);
        canvas.drawRoundRect(mRollSquare.rectF, mRollRoundCornor, mRollRoundCornor, mPaint);
    }
}

上文也有提到,最終的繪製分為兩步:

  1. 繪製固定的方塊
  2. 繪製滾動的方塊;

如果讀者還有不明朗的地方,歡迎檢視原始碼,並且給我提bug,一起為這個社群做出自己的微薄貢獻。