一竹椒、現(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ù)覽如下:
三幻件、實(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