頁面多狀態佈局是開發中常見的需求,即頁面在不一樣狀態須要顯示不一樣的佈局,實現的方式也比較多,最簡單粗暴的方式就是在 XML 中先將不一樣狀態對應的佈局隱藏起來,根據須要改變其可見狀態,若是多個界面公用相同的狀態佈局,缺點也很明顯,繁瑣、重複、不優雅等,相似的實現也可使用 ViewStub,這樣性能會更好些。因此咱們要作的就是儘量避免這些方式所致使的問題,更加高效、優雅的管理不一樣的狀態佈局。java
咱們要實現的 StatusView 要實現的主要功能以下:android
效果預覽以下: git
這裏只對實現過程當中一些比較重要的點進行分析。github
首先有一個最重要的知識點須要明確,XML 佈局中的每一個View都有其對應的父 View,必然在其父View中都有固定的位置,若是是 Activity 對應的 XML,那XML根佈局View的父View是誰呢?其實就是一個 id 爲android.R.id.content
的 View,若是是 Fragment 對應的 XML,那 XML 根佈局 View 的父 View 能夠經過fragment.getView()
方法獲得。因此如今咱們能夠獲得XML 中每個View和對應的 LayoutParams 位置信息。api
既然有了 View 和其對應的 LayoutParams 位置信息,就能夠經過其父 View 將指定的子 View 移除掉,而後將 StatusView 添加到被移除的 View 的位置,進而就能夠控制 StatusView 來切換不一樣的狀態佈局。ide
簡單總結下,就是用 StatusView 替換掉要進行多狀態佈局切換的 View,這個 View 能夠時 XML 中的任意 View。這也是直接在 Activity、Fragment 中使用 StatusView 要作的核心初始化工做。佈局
那麼 StatusView 又是個什麼呢?其實就是一個繼承了FrameLayout
的 ViewGroup,之因此要繼承 FrameLayout,由於 StatusView 此時僅僅是做爲父容器存在的,並不關心內部各類狀態 View 的具體狀況,因此使用 FrameLayout 就夠了,更有通用性。這樣 StatusView 也就能夠在 XML 中使用了性能
先將上邊這部份內容轉化成代碼:spa
public class StatusView extends FrameLayout {
......
/** * 在 Activity 中的初始化方法,默認頁面的根佈局使用多狀態佈局 */
public static StatusView init(Activity activity) {
View contentView = ((ViewGroup) activity.findViewById(android.R.id.content)).getChildAt(0);
return init(contentView);
}
/** * 在 Activity 中的初始化方法 * @param viewId 使用多狀態佈局的 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 使用多狀態佈局的 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 替換要使用多狀態佈局的 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 如何進行初始化呢,天然是經過onFinishInflate()
方法:code
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if (getChildCount() == 1) {
View view = getChildAt(0);
setContentView(view);
}
}
複製代碼
StatusView 默認支持 Loading、Empty、Error 三種狀態佈局,加上原始的頁面內容佈局,一共四種。切換狀態佈局時,咱們作法是直接從 StatusView 中移除掉正在顯示的狀態佈局,而後添加要顯示的狀態佈局:
private void switchStatusView(View statusView) {
if (statusView == currentView) {
return;
}
removeView(currentView);
currentView = statusView;
addView(currentView);
}
複製代碼
在APP使用環境良好的狀況下,有些狀態佈局可能根本沒有顯示的機會,若是在初始化時一股腦的加載出來天然不可取,影響性能,因此咱們要作的就是按需加載,即僅在狀態佈局初次顯示時加載並初始化,以後複用便可:
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;
}
複製代碼
通常的多狀態佈局管理都會提供默認的 Loading、Empty、Error 三種狀態佈局,並能夠自定義對應的狀態佈局, 並提供對應的開放 api。但這樣會有些侷限性,若是有其它業務場景的狀態佈局,雖然佈局文件能夠自定義,但原有的api方法調用起來不免會有違和感,並不友好!因此有必要在經常使用業務場景的基礎上再提供更加通用的api方法,並不侷限於特定的場景。
目前的作法是用狀態佈局和對應的索引之間的關係來實現:
// 添加指定索引對應的狀態佈局
statusView.setStatusView(int index, @LayoutRes int layoutId)
// 爲指定索引的狀態佈局設置初次顯示的監聽事件,用來進行狀態佈局的相關初始化
statusView.setOnStatusViewConvertListener(int index, StatusViewConvertListener listener)
// 顯示指定索引的狀態佈局
statusView.showStatusView(int index)
複製代碼
主要的點就這麼多了,剩下的就是些屬性配置的內容,其實挺簡單的,更多細節和用法可參考GitHub:StatusView