
前言
-
Android開發(fā)中,加載等待的需求 非常常見 - 本文將手把手教你做 一款 可愛 & 小資風(fēng)格的加載等待
Android自定義View控件,希望你們會喜歡。

示意圖
已在
Github開源:Kawaii_LoadingView,歡迎Star!
目錄

示意圖
1. 簡介
一款 可愛 、清新 & 小資風(fēng)格的 Android自定義View控件
已在
Github開源:Kawaii_LoadingView,歡迎Star!

示意圖
2. 應(yīng)用場景
App 長時間加載等待時,用于提示用戶進(jìn)度 & 緩解用戶情緒
3. 特點
對比市面上的加載等待自定義控件,該控件Kawaii_LoadingView 的特點是:
3.1 樣式清新
- 對比市面上 各種酷炫、眼花繚亂的加載等待自定義控件,該款
Kawaii_LoadingView的 清新 & 小資風(fēng)格 簡直是一股清流 - 同時,可根據(jù)您的
App定位 & 主色進(jìn)行顏色調(diào)整,使得控件更加符合App的形象。具體如下:

示意圖

示意圖

示意圖

示意圖
3.2 使用簡單
僅需要3步驟 & 配置簡單。
3.3 二次開發(fā)成本低
- 本項目已在
Github上開源:Kawaii_LoadingView - 詳細(xì)的源碼分析文檔:具體請看本文的第6節(jié)
所以,在其上做二次開發(fā) & 定制化成本非常低。
4. 具體使用
具體請看文章:Android開源控件:一款你不可錯過的可愛 & 小資風(fēng)格的加載等待自定義View
5. 完整Demo地址
Carson_Ho的Github地址:Kawaii_LoadingView_TestDemo

最終示意圖.gif
6. 源碼分析
下面,我將手把手教你如何實現(xiàn)這款 可愛 & 小資風(fēng)格的加載等待Android自定義View控件
6.1 準(zhǔn)備說明
- 方格排列說明

示意圖
- 方塊類型說明

示意圖
6.2 動畫原理
- 隱藏固定的2個方塊 & 移動方塊繼承其中1個的位置
注:只有外部方塊運(yùn)動
- 通過 屬性動畫 (平移 + 旋轉(zhuǎn) = 組合動畫)改變移動方塊的位置 & 旋轉(zhuǎn)角度
- 通過調(diào)用
invalidate()重新繪制,從而實現(xiàn)動態(tài)的動畫效果 - 具體原理圖如下:

示意圖
6.3 實現(xiàn)步驟

示意圖
下面我將詳細(xì)介紹每個步驟:
步驟1:初始化動畫屬性
- 屬性說明:

示意圖
- 具體屬性設(shè)置

示意圖
- 添加屬性文件
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);
// 通過設(shè)置兩個方塊的圓角半徑使得二者不同可以得到更好的動畫效果哦
// 方塊顏色(使用十六進(jìn)制代碼,如#333、#8e8e8e)
int defaultColor = context.getResources().getColor(R.color.colorAccent); // 默認(rèn)顏色
blockColor = typedArray.getColor(R.styleable.Kawaii_LoadingView_blockColor, defaultColor);
// 移動方塊的初始位置(即空白位置)
initPosition = typedArray.getInteger(R.styleable.Kawaii_LoadingView_initPosition, 0);
// 由于移動方塊只能是外部方塊,所以這里需要判斷方塊是否屬于外部方塊 -->關(guān)注1
if (isInsideTheRect(initPosition, lineNumber)) {
initPosition = 0;
}
// 動畫方向是否 = 順時針旋轉(zhuǎn)
isClock_Wise = typedArray.getBoolean(R.styleable.Kawaii_LoadingView_isClock_Wise, true);
// 移動方塊的移動速度
// 注:不建議使用者將速度調(diào)得過快
// 因為會導(dǎo)致ValueAnimator動畫對象頻繁重復(fù)的創(chuàng)建,存在內(nèi)存抖動
moveSpeed = typedArray.getInteger(R.styleable.Kawaii_LoadingView_moveSpeed, 250);
// 設(shè)置移動方塊動畫的插值器
int move_InterpolatorResId = typedArray.getResourceId(R.styleable.Kawaii_LoadingView_move_Interpolator,
android.R.anim.linear_interpolator);
move_Interpolator = AnimationUtils.loadInterpolator(context, move_InterpolatorResId);
// 當(dāng)方塊移動后,需要實時更新的空白方塊的位置
mCurrEmptyPosition = initPosition;
// 釋放資源
typedArray.recycle();
}
// 此步驟結(jié)束
/**
* 關(guān)注1:判斷方塊是否在內(nèi)部
*/
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邊,則在內(nèi)部
return true;
}
// 回到原處
步驟2:初始化方塊對象 & 之間的關(guān)系
private void init() {
// 初始化畫筆
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(blockColor);
// 初始化方塊對象 & 關(guān)系 ->>關(guān)注1
initBlocks(initPosition);
}
/**
* 關(guān)注1
* 初始化方塊對象、之間的關(guān)系
* 參數(shù)說明:initPosition = 移動方塊的初始位置
*/
private void initBlocks(int initPosition) {
// 1. 創(chuàng)建總方塊的數(shù)量(固定方塊) = lineNumber * lineNumber
// lineNumber = 方塊的行數(shù)
// fixedBlock = 固定方塊 類 ->>關(guān)注2
mfixedBlocks = new fixedBlock[lineNumber * lineNumber];
// 2. 創(chuàng)建方塊
for (int i = 0; i < mfixedBlocks.length; i++) {
// 創(chuàng)建固定方塊 & 保存到數(shù)組中
mfixedBlocks[i] = new fixedBlock();
// 對固定方塊對象里的變量進(jìn)行賦值
mfixedBlocks[i].index = i;
// 對方塊是否顯示進(jìn)行判斷
// 若該方塊的位置 = 移動方塊的初始位置,則隱藏;否則顯示
mfixedBlocks[i].isShow = initPosition == i ? false : true;
mfixedBlocks[i].rectF = new RectF();
}
// 3. 創(chuàng)建移動的方塊(1個) ->>關(guān)注3
mMoveBlock = new MoveBlock();
mMoveBlock.rectF = new RectF();
mMoveBlock.isShow = false;
// 4. 關(guān)聯(lián)外部方塊的位置
// 因為外部的方塊序號 ≠ 0、1、2…排列,通過 next變量(指定其下一個),一個接一個連接 外部方塊 成圈
// ->>關(guān)注4
relate_OuterBlock(mfixedBlocks, isClock_Wise);
}
// 此步驟結(jié)束
/**
* 關(guān)注2:固定方塊 類(內(nèi)部類)
*/
private class fixedBlock {
// 存儲方塊的坐標(biāo)位置參數(shù)
RectF rectF;
// 方塊對應(yīng)序號
int index;
// 標(biāo)志位:判斷是否需要繪制
boolean isShow;
// 指向下一個需要移動的位置
fixedBlock next;
// 外部的方塊序號 ≠ 0、1、2…排列,通過 next變量(指定其下一個),一個接一個連接 外部方塊 成圈
}
// 請回到原處
/**
* 關(guān)注3
*:移動方塊類(內(nèi)部類)
*/
private class MoveBlock {
// 存儲方塊的坐標(biāo)位置參數(shù)
RectF rectF;
// 方塊對應(yīng)序號
int index;
// 標(biāo)志位:判斷是否需要繪制
boolean isShow;
// 旋轉(zhuǎn)中心坐標(biāo)
// 移動時的旋轉(zhuǎn)中心(X,Y)
float cx;
float cy;
}
// 請回到原處
/**
* 關(guān)注4:將外部方塊的位置關(guān)聯(lián)起來
* 算法思想: 按照第1行、最后1行、第1列 & 最后1列的順序,分別讓每個外部方塊的next屬性 == 下一個外部方塊的位置,最終對整個外部方塊的位置進(jìn)行關(guān)聯(lián)
* 注:需要考慮移動方向變量isClockwise( 順 Or 逆時針)
*/
private void relate_OuterBlock(fixedBlock[] fixedBlocks, boolean isClockwise) {
int lineCount = (int) Math.sqrt(fixedBlocks.length);
// 情況1:關(guān)聯(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:關(guān)聯(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:關(guān)聯(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:關(guān)聯(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:設(shè)置方塊初始位置
// 該步驟寫在onSizeChanged()
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
// 調(diào)用時刻:onCreate之后onDraw之前調(diào)用;view的大小發(fā)生改變就會調(diào)用該方法
// 使用場景:用于屏幕的大小改變時,需要根據(jù)屏幕寬高來決定的其他變量可以在這里進(jìn)行初始化操作
super.onSizeChanged(w, h, oldw, oldh);
int measuredWidth = getMeasuredWidth();
int measuredHeight = getMeasuredHeight();
// 1. 設(shè)置移動方塊的旋轉(zhuǎn)中心坐標(biāo)
int cx = measuredWidth / 2;
int cy = measuredHeight / 2;
// 2. 設(shè)置固定方塊的位置 ->>關(guān)注1
fixedBlockPosition(mfixedBlocks, cx, cy, blockInterval, half_BlockWidth);
// 3. 設(shè)置移動方塊的位置 ->>關(guān)注2
MoveBlockPosition(mfixedBlocks, mMoveBlock, initPosition, isClock_Wise);
}
// 此步驟結(jié)束
/**
* 關(guān)注1:設(shè)置 固定方塊位置
*/
private void fixedBlockPosition(fixedBlock[] fixedBlocks, int cx, int cy, float dividerWidth, float halfSquareWidth) {
// 1. 確定第1個方塊的位置
// 分為2種情況:行數(shù) = 偶 / 奇數(shù)時
// 主要是是數(shù)學(xué)知識,此處不作過多描述
float squareWidth = halfSquareWidth * 2;
int lineCount = (int) Math.sqrt(fixedBlocks.length);
float firstRectLeft = 0;
float firstRectTop = 0;
// 情況1:當(dāng)行數(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:當(dāng)行數(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);
}
}
}
}
// 回到原處
/**
* 關(guān)注2:設(shè)置移動方塊的位置
*/
private void MoveBlockPosition(fixedBlock[] fixedBlocks,
MoveBlock moveBlock, int initPosition, boolean isClockwise) {
// 移動方塊位置 = 設(shè)置初始的空出位置 的下一個位置(next)
// 下一個位置 通過 連接的外部方塊位置確定
fixedBlock fixedBlock = fixedBlocks[initPosition];
moveBlock.rectF.set(fixedBlock.next.rectF);
}
// 回到原處
步驟4:繪制方塊
// 此步驟寫到onDraw()中
@Override
protected void onDraw(Canvas canvas) {
// 1. 繪制內(nèi)部方塊(固定的)
for (int i = 0; i < mfixedBlocks.length; i++) {
// 根據(jù)標(biāo)志位判斷是否需要繪制
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:設(shè)置動畫
實現(xiàn)該動畫的步驟包括:設(shè)置平移動畫、旋轉(zhuǎn)動畫 & 組合動畫。
1.設(shè)置平移動畫
private ValueAnimator createTranslateValueAnimator(fixedBlock currEmptyfixedBlock,
fixedBlock moveBlock) {
float startAnimValue = 0;
float endAnimValue = 0;
PropertyValuesHolder left = null;
PropertyValuesHolder top = null;
// 1. 設(shè)置移動速度
ValueAnimator valueAnimator = new ValueAnimator().setDuration(moveSpeed);
// 2. 設(shè)置移動方向
// 情況分為:4種,分別是移動方塊向左、右移動 和 上、下移動
// 注:需考慮 旋轉(zhuǎn)方向(isClock_Wise),即順逆時針 ->>關(guān)注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;
}
// 設(shè)置屬性值
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;
}
// 設(shè)置屬性值
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);
}
// 實時更新旋轉(zhuǎn)中心 ->>關(guān)注2
setMoveBlockRotateCenter(mMoveBlock, isClock_Wise);
// 更新繪制
invalidate();
}
});
return valueAnimator;
}
// 此步驟分析完畢
/**
* 關(guān)注1:判斷移動方向
* 即上下 or 左右
*/
private boolean isNextRollLeftOrRight(fixedBlock currEmptyfixedBlock, fixedBlock rollSquare) {
if (currEmptyfixedBlock.rectF.left - rollSquare.rectF.left == 0) {
return false;
} else {
return true;
}
}
// 回到原處
/**
* 關(guān)注2:實時更新移動方塊的旋轉(zhuǎn)中心
* 因為方塊在平移旋轉(zhuǎn)過程中,旋轉(zhuǎn)中心也會跟著改變,因此需要改變MoveBlock的旋轉(zhuǎn)中心(cx,cy)
*/
private void setMoveBlockRotateCenter(MoveBlock moveBlock, boolean isClockwise) {
// 情況1:以移動方塊的左上角為旋轉(zhuǎn)中心
if (moveBlock.index == 0) {
moveBlock.cx = moveBlock.rectF.right;
moveBlock.cy = moveBlock.rectF.bottom;
// 情況2:以移動方塊的右下角為旋轉(zhuǎn)中心
} else if (moveBlock.index == lineNumber * lineNumber - 1) {
moveBlock.cx = moveBlock.rectF.left;
moveBlock.cy = moveBlock.rectF.top;
// 情況3:以移動方塊的左下角為旋轉(zhuǎn)中心
} else if (moveBlock.index == lineNumber * (lineNumber - 1)) {
moveBlock.cx = moveBlock.rectF.right;
moveBlock.cy = moveBlock.rectF.top;
// 情況4:以移動方塊的右上角為旋轉(zhuǎn)中心
} else if (moveBlock.index == lineNumber - 1) {
moveBlock.cx = moveBlock.rectF.left;
moveBlock.cy = moveBlock.rectF.bottom;
}
//以下判斷與旋轉(zhuǎn)方向有關(guān):即順 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. 設(shè)置旋轉(zhuǎn)動畫
private ValueAnimator createMoveValueAnimator() {
// 通過屬性動畫進(jìn)行設(shè)置
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. 設(shè)置組合動畫
private void setAnimation() {
// 1. 獲取固定方塊當(dāng)前的空位置,即移動方塊當(dāng)前位置
fixedBlock currEmptyfixedBlock = mfixedBlocks[mCurrEmptyPosition];
// 2. 獲取移動方塊的到達(dá)位置,即固定方塊當(dāng)前空位置的下1個位置
fixedBlock movedBlock = currEmptyfixedBlock.next;
// 3. 設(shè)置動畫變化的插值器
mAnimatorSet.setInterpolator(move_Interpolator);
mAnimatorSet.playTogether(translateConrtroller, moveConrtroller);
mAnimatorSet.addListener(new AnimatorListenerAdapter() {
// 4. 動畫開始時進(jìn)行一些設(shè)置
@Override
public void onAnimationStart(Animator animation) {
// 每次動畫開始前都需要更新移動方塊的位置 ->>關(guān)注1
updateMoveBlock();
// 讓移動方塊的初始位置的下個位置也隱藏 = 兩個隱藏的方塊
mfixedBlocks[mCurrEmptyPosition].next.isShow = false;
// 通過標(biāo)志位將移動的方塊顯示出來
mMoveBlock.isShow = true;
}
// 5. 結(jié)束時進(jìn)行一些設(shè)置
@Override
public void onAnimationEnd(Animator animation) {
isMoving = false;
mfixedBlocks[mCurrEmptyPosition].isShow = true;
mCurrEmptyPosition = mfixedBlocks[mCurrEmptyPosition].next.index;
// 將移動的方塊隱藏
mMoveBlock.isShow = false;
// 通過標(biāo)志位判斷動畫是否要循環(huán)播放
if (mAllowRoll) {
startMoving();
}
}
});
// 此步驟分析完畢
/**
* 關(guān)注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ù)標(biāo)志位 & 視圖是否可見確定是否需要啟動動畫
// 此處設(shè)置是為了方便手動 & 自動停止動畫
if (isMoving || getVisibility() != View.VISIBLE ) {
return;
}
// 2. 設(shè)置標(biāo)記位:以便是否停止動畫
isMoving = true;
mAllowRoll = true;
// 3. 啟動動畫
mAnimatorSet.start();
// 停止動畫
public void stopMoving() {
// 通過標(biāo)記位來設(shè)置
mAllowRoll = false;
}
- 至此,該款小清新加載等待的自定義控件源碼分析完畢
- 完整源碼地址:https://github.com/Carson-Ho/Kawaii_LoadingView
7. 貢獻(xiàn)代碼
- 希望你們能和我一起完善這款清新 & 小資風(fēng)格的自定義控件,具體請看:貢獻(xiàn)代碼說明
- 關(guān)于該開源項目的意見 & 建議可在Issue上提出。歡迎
Star!
8. 總結(jié)
- 相信你一定會喜歡上 這款可愛、清新 & 小資風(fēng)格的加載等待自定義控件
已在
Github上開源:Kawaii_LoadingView,歡迎Star!

示意圖
- 此外,我還有一些有趣的自定義
View實例講解,有興趣可以繼續(xù)關(guān)注Carson_Ho的安卓開發(fā)筆記
a. 手把手教你實現(xiàn)一個簡單好用的搜索框(含歷史搜索記錄)
b. 你需要一款簡單實用的SuperEditText(一鍵刪除&自定義樣式))
c. Android 自定義View實戰(zhàn)系列 :時間軸
- Carson帶你學(xué)自定義View文章系列:
Carson帶你學(xué)自定義View:自定義View基礎(chǔ)
Carson帶你學(xué)自定義View:一文梳理自定義View工作流程
Carson帶你學(xué)自定義View:Measure過程
Carson帶你學(xué)自定義View:Layout過程
Carson帶你學(xué)自定義View:Draw過程
Carson帶你學(xué)自定義View:手把手教你寫一個完整的自定義View
Carson帶你學(xué)自定義View:Canvas類全面解析
Carson帶你學(xué)自定義View:Path類全面解析