1.ContentLoadingProgressBar介紹
最近在學(xué)習(xí)開源項(xiàng)目的時候偶然看到了ContentLoadingProgressBar這個控件,此前我沒有接觸過搔谴,就想著了解一下它的功能干旁。從名稱上看驶沼,ContentLoadingProgressBar應(yīng)該和ProgressBar有著什么聯(lián)系,項(xiàng)目中也是把它當(dāng)做ProgressBar來使用的争群,點(diǎn)進(jìn)源碼一看回怜,果然ContentLoadingProgressBar是繼承自ProgressBar的。
public class ContentLoadingProgressBar extends ProgressBar {
// ...
}
既然是繼承自ProgressBar换薄,那么肯定是在ProgressBar的基礎(chǔ)上添加了特殊的功能玉雾,先來看一下類的注釋:
/**
* ContentLoadingProgressBar implements a ProgressBar that waits a minimum time to be
* dismissed before showing. Once visible, the progress bar will be visible for
* a minimum amount of time to avoid "flashes" in the UI when an event could take
* a largely variable time to complete (from none, to a user perceivable amount)
*/
從注釋中可以看出,ContentLoadingProgressBar在ProgressBar的基礎(chǔ)上添加了以下特性:
- 在顯示之前會等待一段時間來被隱藏
- 一旦顯示轻要,ContentLoadingProgressBar會在一段時間內(nèi)都保持可見
這兩個特性的共同作用就是避免UI視圖的“閃爍”現(xiàn)象复旬,這是什么意思呢,相信大家在項(xiàng)目開發(fā)中都遇到過這樣一種情況冲泥,在進(jìn)行網(wǎng)絡(luò)請求之前顯示Loading對話框驹碍,請求完成之后再隱藏,如果網(wǎng)絡(luò)請求耗時很短凡恍,那么就會導(dǎo)致對話框在短時間內(nèi)顯示和隱藏志秃,造成“閃爍”現(xiàn)象,如下圖所示:
結(jié)合上述場景嚼酝,ContentLoadingProgressBar的這兩個特性就很好理解了洽损,首先在顯示之前等待一段時間(當(dāng)然這段時間很短,否則會產(chǎn)生卡頓現(xiàn)象)革半,如果在這段時間內(nèi)被隱藏,那么就不會顯示出ContentLoadingProgressBar流码。此外又官,一旦顯示出了ContentLoadingProgressBar,還要保證其顯示時間不能太短漫试,否則同樣會造成“閃爍”現(xiàn)象六敬。在這兩點(diǎn)的共同作用下就不會出現(xiàn)ContentLoadingProgressBar剛顯示就被隱藏的問題了,從而避免了“閃爍”現(xiàn)象驾荣。
清楚了ContentLoadingProgressBar的特性和作用后我們來簡單看一下它是如何實(shí)現(xiàn)的外构,完整代碼如下:
public class ContentLoadingProgressBar extends ProgressBar {
private static final int MIN_SHOW_TIME = 500; // ms
private static final int MIN_DELAY = 500; // ms
long mStartTime = -1; // 開始顯示時的時間
boolean mPostedHide = false;
boolean mPostedShow = false;
boolean mDismissed = false;
private final Runnable mDelayedHide = new Runnable() {
@Override
public void run() {
mPostedHide = false;
mStartTime = -1;
setVisibility(View.GONE);
}
};
private final Runnable mDelayedShow = new Runnable() {
@Override
public void run() {
mPostedShow = false;
if (!mDismissed) {
mStartTime = System.currentTimeMillis();
setVisibility(View.VISIBLE);
}
}
};
public ContentLoadingProgressBar(@NonNull Context context) {
this(context, null);
}
public ContentLoadingProgressBar(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs, 0);
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
removeCallbacks();
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
removeCallbacks();
}
private void removeCallbacks() {
removeCallbacks(mDelayedHide);
removeCallbacks(mDelayedShow);
}
/**
* Hide the progress view if it is visible. The progress view will not be
* hidden until it has been shown for at least a minimum show time. If the
* progress view was not yet visible, cancels showing the progress view.
*/
public synchronized void hide() {
mDismissed = true;
removeCallbacks(mDelayedShow);
mPostedShow = false;
long diff = System.currentTimeMillis() - mStartTime;
if (diff >= MIN_SHOW_TIME || mStartTime == -1) {
// ContentLoadingProgressBar的顯示時間已經(jīng)超過了500ms或者還沒有顯示
setVisibility(View.GONE);
} else {
// ContentLoadingProgressBar的顯示時間不足500ms
if (!mPostedHide) {
postDelayed(mDelayedHide, MIN_SHOW_TIME - diff);
mPostedHide = true;
}
}
}
/**
* Show the progress view after waiting for a minimum delay. If
* during that time, hide() is called, the view is never made visible.
*/
public synchronized void show() {
// Reset the start time.
mStartTime = -1;
mDismissed = false;
removeCallbacks(mDelayedHide);
mPostedHide = false;
if (!mPostedShow) {
postDelayed(mDelayedShow, MIN_DELAY);
mPostedShow = true;
}
}
}
ContentLoadingProgressBar中定義了兩個int類型的常量MIN_SHOW_TIME和MIN_DELAY,分別表示顯示的最短時間和延遲顯示的時間播掷,值都是500ms审编。mDelayedShow和mDelayedHide是兩個Runable任務(wù),分別對應(yīng)延時顯示和延時隱藏歧匈。在控制ContentLoadingProgressBar的顯示和隱藏時不能使用setVisibility()
方法垒酬,這樣就和使用ProgressBar沒有區(qū)別了,而是需要使用show()
和hide()
方法,我們來分別看一下這兩個方法勘究。
首先是show()
方法矮湘,這里首先會做一些狀態(tài)的恢復(fù)處理,將mStartTime恢復(fù)為-1口糕,mStartTime記錄了ContentLoadingProgressBar開始顯示的時間缅阳,接著將延時隱藏任務(wù)mDelayedHide從任務(wù)隊(duì)列中移除。方法最后會判斷mPostedShow的值景描,如果為false就調(diào)用postDelayed()
方法延遲MIN_DELAY(500ms)后執(zhí)行mDelayedShow任務(wù)十办。mPostedShow用于標(biāo)記mDelayedShow是否已添加到任務(wù)隊(duì)列中,防止任務(wù)的重復(fù)執(zhí)行伏伯。mDelayedShow任務(wù)的邏輯很簡單橘洞,主要就是記錄開始顯示的時間并執(zhí)行setVisibility(View.VISIBLE)
將ContentLoadingProgressBar顯示出來。
我們再來看hide()
方法说搅,和show()
方法類似炸枣,首先將延時顯示任務(wù)mDelayedShow從任務(wù)隊(duì)列中移除,因此如果調(diào)用show()
和hide()
方法之間的間隔時間小于MIN_DELAY(500ms)弄唧,mDelayedShow就不會執(zhí)行了适肠,ContentLoadingProgressBar也就不會顯示了。接下來會計(jì)算System.currentTimeMillis() - mStartTime
的值候引,即此時ContentLoadingProgressBar的顯示時間侯养,如果此時mStartTime的值為-1(ContentLoadingProgressBar還沒有顯示)或者顯示時間超過了MIN_SHOW_TIME(500ms),直接執(zhí)行setVisibility(View.GONE)
隱藏ContentLoadingProgressBar澄干;反之則說明ContentLoadingProgressBar的顯示時間沒有達(dá)到最短時間500ms逛揩,計(jì)算剩余的時間,延時執(zhí)行隱藏任務(wù)麸俘,保證ContentLoadingProgressBar最短可以顯示500ms辩稽。這里的mPostedHide作用同樣是防止延時隱藏任務(wù)的重復(fù)執(zhí)行。mDelayedHide任務(wù)的邏輯也比較簡單从媚,將mStartTime恢復(fù)為-1逞泄,執(zhí)行setVisibility(View.GONE)
隱藏ContentLoadingProgressBar。
ContentLoadingProgressBar實(shí)現(xiàn)的基本原理還是比較簡單的拜效,看到這里不知道大家是否和我一樣受到了啟發(fā)呢喷众,我們是不是也可以仿照ContentLoadingProgressBar來定義一個Loading對話框,解決“閃爍”問題呢紧憾?
2.Loading對話框的優(yōu)化
ContentLoadingProgressBar給了我們很好的思路到千,解決Loading對話框“閃爍”問題需要做到以下兩點(diǎn):
- 顯示Loading對話框之前先等待一段時間
- 隱藏Loading對話框時判斷顯示時間是否達(dá)到了最短顯示時間,如果沒有達(dá)到就延時執(zhí)行隱藏任務(wù)
清楚思路后就可以優(yōu)化Loading對話框了赴穗,直接附上完整代碼:
import android.app.AlertDialog;
import android.content.Context;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
public class LoadingDialog extends AlertDialog {
private static final int MIN_SHOW_TIME = 500;
private static final int MIN_DELAY = 500;
private TextView tvMessage;
private long mStartTime = -1;
private boolean mPostedHide = false;
private boolean mPostedShow = false;
private boolean mDismissed = false;
private Handler mHandler = new Handler();
private final Runnable mDelayedHide = new Runnable() {
@Override
public void run() {
mPostedHide = false;
mStartTime = -1;
dismiss();
}
};
private final Runnable mDelayedShow = new Runnable() {
@Override
public void run() {
mPostedShow = false;
if (!mDismissed) {
mStartTime = System.currentTimeMillis();
show();
}
}
};
public LoadingDialog(@NonNull Context context) {
super(context, R.style.Theme_AppCompat_Dialog);
View loadView = LayoutInflater.from(getContext()).inflate(R.layout.dialog_loading, null);
setView(loadView);
tvMessage = loadView.findViewById(R.id.tv_message);
}
public void showDialog(String message) {
tvMessage.setText(message);
mStartTime = -1;
mDismissed = false;
mHandler.removeCallbacks(mDelayedHide);
mPostedHide = false;
if (!mPostedShow) {
mHandler.postDelayed(mDelayedShow, MIN_DELAY);
mPostedShow = true;
}
}
public void hideDialog() {
mDismissed = true;
mHandler.removeCallbacks(mDelayedShow);
mPostedShow = false;
long diff = System.currentTimeMillis() - mStartTime;
if (diff >= MIN_SHOW_TIME || mStartTime == -1) {
dismiss();
} else {
if (!mPostedHide) {
mHandler.postDelayed(mDelayedHide, MIN_SHOW_TIME - diff);
mPostedHide = true;
}
}
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
mHandler.removeCallbacks(mDelayedHide);
mHandler.removeCallbacks(mDelayedShow);
}
}
布局文件我就不展示了父阻,就是一個ProgressBar和一個TextView愈涩,用于展示提示信息。其實(shí)基本上都是照抄的ContentLoadingProgressBar,區(qū)別只是需要定義一個Handler對象來處理延時任務(wù)〕贪控制Loading對話框的顯示和隱藏直接使用showDialog()
和hideDialog()
方法就可以了。為了簡單示例毁腿,我這里自定義的Dialog直接繼承自AlertDialog,大家項(xiàng)目中使用的可能是自己定義的Dialog或者第三方Dialog苛茂,又或者是DialogFragment已烤,都沒關(guān)系,只需要清楚思路妓羊,自行修改一下即可胯究,注意要在適當(dāng)?shù)臅r機(jī)移除延時任務(wù),防止內(nèi)存泄漏躁绸。
優(yōu)化完成后我們可以簡單地測試一下裕循,添加兩個按鈕,點(diǎn)擊按鈕時調(diào)用showDialog()
方法延時顯示Loading對話框净刮,之后分別延時300ms和600ms后調(diào)用hideDialog()
方法隱藏Loading對話框剥哑,模擬網(wǎng)絡(luò)請求過程,運(yùn)行效果如下圖所示:
可以看出淹父,延時300ms的情況由于調(diào)用顯示和隱藏方法的間隔時間小于MIN_DELAY株婴,因此不會顯示出Loading對話框;延時600ms的情況會顯示出Loading對話框暑认,由于調(diào)用hideDialog()
方法時Loading對話框顯示的時間大約為600 - MIN_DELAY = 100ms不足MIN_SHOW_TIME困介,因此會延時顯示一段時間后再隱藏。
補(bǔ)充一下蘸际,我這里定義的Loading對話框的延時顯示時間和最短顯示時間都是使用的500ms逻翁,和ContentLoadingProgressBar一樣,大家也可以修改成自己認(rèn)為合適的值捡鱼,尤其是延時顯示時間,500ms可能有些長酷愧,容易給用戶造成卡頓的感覺驾诈,可以適當(dāng)?shù)販p小延時時間,比如調(diào)整為300ms溶浴。
3.總結(jié)
本文通過分析ContentLoadingProgressBar的原理引出了項(xiàng)目開發(fā)中Loading對話框的一種優(yōu)化方式乍迄,避免對話框顯示和隱藏間隔時間太短導(dǎo)致的“閃爍”現(xiàn)象。其實(shí)這可能也不算什么問題士败,不做處理也沒關(guān)系闯两,但既然解決起來很簡單褥伴,又能給用戶帶來更好的使用體驗(yàn),為什么不去做呢漾狼。提到優(yōu)化我們往往想到的都是運(yùn)行性能重慢、內(nèi)存等等方面,代碼邏輯上的優(yōu)化很容易被忽略逊躁,但恰恰這才是我們要首先考慮也是最容易著手的似踱。最后,限于自身水平稽煤,文中有些地方可能分析得不是很準(zhǔn)確核芽,或者大家有什么更好的想法都?xì)g迎提出,一起交流酵熙。