Android項(xiàng)目中Loading對話框的優(yōu)化

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_TIMEMIN_DELAY,分別表示顯示的最短時間和延遲顯示的時間播掷,值都是500ms审编。mDelayedShowmDelayedHide是兩個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迎提出,一起交流酵熙。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末轧简,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子匾二,更是在濱河造成了極大的恐慌哮独,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件假勿,死亡現(xiàn)場離奇詭異借嗽,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)转培,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門恶导,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人浸须,你說我怎么就攤上這事惨寿。” “怎么了删窒?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵裂垦,是天一觀的道長。 經(jīng)常有香客問我肌索,道長蕉拢,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任诚亚,我火速辦了婚禮晕换,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘站宗。我一直安慰自己闸准,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布梢灭。 她就那樣靜靜地躺著夷家,像睡著了一般蒸其。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上库快,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天摸袁,我揣著相機(jī)與錄音,去河邊找鬼缺谴。 笑死但惶,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的湿蛔。 我是一名探鬼主播膀曾,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼阳啥!你這毒婦竟也來了添谊?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤察迟,失蹤者是張志新(化名)和其女友劉穎斩狱,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體扎瓶,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡所踊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了概荷。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片秕岛。...
    茶點(diǎn)故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖误证,靈堂內(nèi)的尸體忽然破棺而出继薛,到底是詐尸還是另有隱情,我是刑警寧澤愈捅,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布遏考,位于F島的核電站,受9級特大地震影響蓝谨,放射性物質(zhì)發(fā)生泄漏灌具。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一譬巫、第九天 我趴在偏房一處隱蔽的房頂上張望咖楣。 院中可真熱鬧,春花似錦缕题、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽瘪松。三九已至,卻和暖如春锨阿,著一層夾襖步出監(jiān)牢的瞬間宵睦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工墅诡, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留壳嚎,地道東北人。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓末早,卻偏偏與公主長得像烟馅,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子然磷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評論 2 345