Android 頁面多狀態(tài)布局管理

一竹椒、現(xiàn)狀

頁面多狀態(tài)布局是開發(fā)中常見的需求,即頁面在不同狀態(tài)需要顯示不同的布局,實(shí)現(xiàn)的方式也比較多爆惧,最簡單粗暴的方式就是在 XML 中先將不同狀態(tài)對應(yīng)的布局隱藏起來,根據(jù)需要改變其可見狀態(tài),如果多個(gè)界面公用相同的狀態(tài)布局秃殉,缺點(diǎn)也很明顯,繁瑣、重復(fù)樱哼、不優(yōu)雅等唇礁,類似的實(shí)現(xiàn)也可以使用 ViewStub惨篱,這樣性能會(huì)更好些砸讳。所以我們要做的就是盡可能避免這些方式所導(dǎo)致的問題琢融,更加高效、優(yōu)雅的管理不同的狀態(tài)布局克胳。

二捏雌、目標(biāo)

我們要實(shí)現(xiàn)的 StatusView 要實(shí)現(xiàn)的主要功能如下:

  • 可在 Activity满败、Fragment 宵荒、XML 中使用面粮,可作用于XML的根布局View或其子View
  • 支持默認(rèn)的狀態(tài)布局袁翁,可進(jìn)行常規(guī)配置
  • 可自定義狀態(tài)布局
  • 狀態(tài)布局懶加載狐树,僅在初次顯示時(shí)初始化

效果預(yù)覽如下:


preview

三幻件、實(shí)現(xiàn)

這里只對實(shí)現(xiàn)過程中一些比較重要的點(diǎn)進(jìn)行分析贺待。

3.1、初始化

首先有一個(gè)最重要的知識點(diǎn)需要明確,XML 布局中的每個(gè)View都有其對應(yīng)的父 View,必然在其父View中都有固定的位置,如果是 Activity 對應(yīng)的 XML,那XML根布局View的父View是誰呢赡麦?其實(shí)就是一個(gè) id 為android.R.id.content的 View肮疗,如果是 Fragment 對應(yīng)的 XML珠增,那 XML 根布局 View 的父 View 可以通過fragment.getView()方法得到凝垛。所以現(xiàn)在我們可以得到XML 中每一個(gè)View和對應(yīng)的 LayoutParams 位置信息桃焕。

既然有了 View 和其對應(yīng)的 LayoutParams 位置信息,就可以通過其父 View 將指定的子 View 移除掉,然后將 StatusView 添加到被移除的 View 的位置笔横,進(jìn)而就可以控制 StatusView 來切換不同的狀態(tài)布局。

簡單總結(jié)下,就是用 StatusView 替換掉要進(jìn)行多狀態(tài)布局切換的 View琅关,這個(gè) View 可以時(shí) XML 中的任意 View新症。這也是直接在 Activity隆嗅、Fragment 中使用 StatusView 要做的核心初始化工作丽焊。

那么 StatusView 又是個(gè)什么呢凫乖?其實(shí)就是一個(gè)繼承了FrameLayout的 ViewGroup披泪,之所以要繼承 FrameLayout艾少,因?yàn)?StatusView 此時(shí)僅僅是作為父容器存在的误堡,并不關(guān)心內(nèi)部各種狀態(tài) View 的具體情況,所以使用 FrameLayout 就夠了描焰,更有通用性步绸。這樣 StatusView 也就可以在 XML 中使用了

先將上邊這部分內(nèi)容轉(zhuǎn)化成代碼:

public class StatusView extends FrameLayout {
    ......
    /**
     * 在 Activity 中的初始化方法募舟,默認(rèn)頁面的根布局使用多狀態(tài)布局
     */
    public static StatusView init(Activity activity) {
        View contentView = ((ViewGroup) activity.findViewById(android.R.id.content)).getChildAt(0);
        return init(contentView);
    }

    /**
     * 在 Activity 中的初始化方法
     * @param viewId   使用多狀態(tài)布局的 ViewId
     */
    public static StatusView init(Activity activity, @IdRes int viewId) {
        View rootView = ((ViewGroup) activity.findViewById(android.R.id.content)).getChildAt(0);
        View contentView = rootView.findViewById(viewId);
        return init(contentView);
    }

    /**
     * 在Fragment中的初始化方法
     * @param viewId   使用多狀態(tài)布局的 ViewId
     */
    public static StatusView init(Fragment fragment, @IdRes int viewId) {
        View rootView = fragment.getView();
        View contentView = null;
        if (rootView != null) {
            contentView = rootView.findViewById(viewId);
        }
        return init(contentView);
    }

    /**
     * 用 StatusView 替換要使用多狀態(tài)布局的 View
     */
    private static StatusView init(View contentView) {
        if (contentView == null) {
            throw new RuntimeException("ContentView can not be null!");
        }
        ViewGroup parent = (ViewGroup) contentView.getParent();
        if (parent == null) {
            throw new RuntimeException("ContentView must have a parent view!");
        }
        ViewGroup.LayoutParams lp = contentView.getLayoutParams();
        int index = parent.indexOfChild(contentView);
        parent.removeView(contentView);
        StatusView statusView = new StatusView(contentView.getContext());
        statusView.addView(contentView);
        statusView.setContentView(contentView);
        parent.addView(statusView, index, lp);
        return statusView;
    }
    ......
}

如果在 XML 中使用 StatusView 如何進(jìn)行初始化呢赘娄,自然是通過onFinishInflate()方法:

@Override
protected void onFinishInflate() {
    super.onFinishInflate();
    if (getChildCount() == 1) {
        View view = getChildAt(0);
        setContentView(view);
    }
}
3.2鹏浅、狀態(tài)布局的切換

StatusView 默認(rèn)支持 Loading幽纷、Empty、Error 三種狀態(tài)布局友浸,加上原始的頁面內(nèi)容布局峰尝,一共四種。切換狀態(tài)布局時(shí)收恢,我們做法是直接從 StatusView 中移除掉正在顯示的狀態(tài)布局武学,然后添加要顯示的狀態(tài)布局:

private void switchStatusView(View statusView) {
    if (statusView == currentView) {
        return;
    }
    removeView(currentView);
    currentView = statusView;
    addView(currentView);
}
3.3、狀態(tài)布局的懶加載

在APP使用環(huán)境良好的情況下派诬,有些狀態(tài)布局可能根本沒有顯示的機(jī)會(huì)劳淆,如果在初始化時(shí)一股腦的加載出來自然不可取,影響性能默赂,所以我們要做的就是按需加載沛鸵,即僅在狀態(tài)布局初次顯示時(shí)加載并初始化,之后復(fù)用即可:

private View generateStatusView(@LayoutRes int layoutId) {
        View statusView = viewArray.get(layoutId);
        if (statusView == null) {
            statusView = inflate(layoutId);
            viewArray.put(layoutId, statusView);
            configStatusView(layoutId, statusView);
        }
        return statusView;
    }
3.4、更自由的用法

一般的多狀態(tài)布局管理都會(huì)提供默認(rèn)的 Loading曲掰、Empty疾捍、Error 三種狀態(tài)布局,并可以自定義對應(yīng)的狀態(tài)布局栏妖, 并提供對應(yīng)的開放 api吊趾。但這樣會(huì)有些局限性宛裕,如果有其它業(yè)務(wù)場景的狀態(tài)布局,雖然布局文件可以自定義论泛,但原有的api方法調(diào)用起來難免會(huì)有違和感揩尸,并不友好!所以有必要在常用業(yè)務(wù)場景的基礎(chǔ)上再提供更加通用的api方法屁奏,并不局限于特定的場景岩榆。

目前的做法是用狀態(tài)布局和對應(yīng)的索引之間的關(guān)系來實(shí)現(xiàn):

// 添加指定索引對應(yīng)的狀態(tài)布局
statusView.setStatusView(int index, @LayoutRes int layoutId)
// 為指定索引的狀態(tài)布局設(shè)置初次顯示的監(jiān)聽事件,用來進(jìn)行狀態(tài)布局的相關(guān)初始化
statusView.setOnStatusViewConvertListener(int index, StatusViewConvertListener listener)
// 顯示指定索引的狀態(tài)布局
statusView.showStatusView(int index)
3.5坟瓢、注意事項(xiàng)
  • 當(dāng) Fragment 布局文件的根 View 使用 StatusView 時(shí)勇边,為避免出現(xiàn)的異常問題,建議在 XML 中初始化折联!
  • 當(dāng)直接在 Fragment 中使用時(shí)粒褒,init()方法需要在onCreateView()之后的生命周期方法中執(zhí)行!
  • 由于StatusView 繼承自 FrameLayout崭庸,所以會(huì)多一層布局嵌套怀浆。

主要的點(diǎn)就這么多了,剩下的就是些屬性配置的內(nèi)容怕享,其實(shí)挺簡單的,更多細(xì)節(jié)和用法可參考GitHub:StatusView

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末镰踏,一起剝皮案震驚了整個(gè)濱河市函筋,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌奠伪,老刑警劉巖跌帐,帶你破解...
    沈念sama閱讀 216,997評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異绊率,居然都是意外死亡谨敛,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評論 3 392
  • 文/潘曉璐 我一進(jìn)店門滤否,熙熙樓的掌柜王于貴愁眉苦臉地迎上來脸狸,“玉大人,你說我怎么就攤上這事〈都祝” “怎么了泥彤?”我有些...
    開封第一講書人閱讀 163,359評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長卿啡。 經(jīng)常有香客問我吟吝,道長,這世上最難降的妖魔是什么颈娜? 我笑而不...
    開封第一講書人閱讀 58,309評論 1 292
  • 正文 為了忘掉前任剑逃,我火速辦了婚禮,結(jié)果婚禮上官辽,老公的妹妹穿的比我還像新娘蛹磺。我一直安慰自己,他們只是感情好野崇,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,346評論 6 390
  • 文/花漫 我一把揭開白布称开。 她就那樣靜靜地躺著,像睡著了一般乓梨。 火紅的嫁衣襯著肌膚如雪鳖轰。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,258評論 1 300
  • 那天扶镀,我揣著相機(jī)與錄音蕴侣,去河邊找鬼。 笑死臭觉,一個(gè)胖子當(dāng)著我的面吹牛昆雀,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蝠筑,決...
    沈念sama閱讀 40,122評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼狞膘,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了什乙?” 一聲冷哼從身側(cè)響起挽封,我...
    開封第一講書人閱讀 38,970評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎臣镣,沒想到半個(gè)月后辅愿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,403評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡忆某,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,596評論 3 334
  • 正文 我和宋清朗相戀三年点待,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片弃舒。...
    茶點(diǎn)故事閱讀 39,769評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡癞埠,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情燕差,我是刑警寧澤遭笋,帶...
    沈念sama閱讀 35,464評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站徒探,受9級特大地震影響瓦呼,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜测暗,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,075評論 3 327
  • 文/蒙蒙 一央串、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧碗啄,春花似錦质和、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,705評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至胆描,卻和暖如春瘫想,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背昌讲。 一陣腳步聲響...
    開封第一講書人閱讀 32,848評論 1 269
  • 我被黑心中介騙來泰國打工国夜, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人短绸。 一個(gè)月前我還...
    沈念sama閱讀 47,831評論 2 370
  • 正文 我出身青樓车吹,卻偏偏與公主長得像,于是被迫代替她去往敵國和親醋闭。 傳聞我的和親對象是個(gè)殘疾皇子窄驹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,678評論 2 354

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,093評論 25 707
  • 用兩張圖告訴你,為什么你的 App 會(huì)卡頓? - Android - 掘金 Cover 有什么料证逻? 從這篇文章中你...
    hw1212閱讀 12,715評論 2 59
  • 專業(yè)矮子二十年瑟曲,我為矮子做代言!欲問矮子有啥好豪治,多的讓你數(shù)不完洞拨! 作為一名合格的矮子,我來講講“矮子王國”的獨(dú)有“...
    周木槿閱讀 2,104評論 0 1
  • 送完站和Puppy回到家中负拟,睡意全無烦衣,心中塞滿了不舍與離別的傷感,屋子顯得更大了,也許離別就是思念的開始……...
    AprilFriday閱讀 124評論 1 2
  • 新疆的秋天過去了花吟,烏市的溫度降到了個(gè)位數(shù)秸歧。放假風(fēng)波后,終于得到消息還是可以回家的衅澈。 很早之前就訂了回家的火車票键菱,妹...
    Wind_bca3閱讀 138評論 0 0