Android 頁面多狀態佈局管理

SheHuan發表於2018-10-15

一、現狀

頁面多狀態佈局是開發中常見的需求,即頁面在不同狀態需要顯示不同的佈局,實現的方式也比較多,最簡單粗暴的方式就是在 XML 中先將不同狀態對應的佈局隱藏起來,根據需要改變其可見狀態,如果多個介面公用相同的狀態佈局,缺點也很明顯,繁瑣、重複、不優雅等,類似的實現也可以使用 ViewStub,這樣效能會更好些。所以我們要做的就是儘可能避免這些方式所導致的問題,更加高效、優雅的管理不同的狀態佈局。

二、目標

我們要實現的 StatusView 要實現的主要功能如下:

  • 可在 Activity、Fragment 、XML 中使用,可作用於XML的根佈局View或其子View
  • 支援預設的狀態佈局,可進行常規配置
  • 可自定義狀態佈局
  • 狀態佈局懶載入,僅在初次顯示時初始化

效果預覽如下:

preview

三、實現

這裡只對實現過程中一些比較重要的點進行分析。

3.1、初始化

首先有一個最重要的知識點需要明確,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 位置資訊。

既然有了 View 和其對應的 LayoutParams 位置資訊,就可以通過其父 View 將指定的子 View 移除掉,然後將 StatusView 新增到被移除的 View 的位置,進而就可以控制 StatusView 來切換不同的狀態佈局。

簡單總結下,就是用 StatusView 替換掉要進行多狀態佈局切換的 View,這個 View 可以時 XML 中的任意 View。這也是直接在 Activity、Fragment 中使用 StatusView 要做的核心初始化工作。

那麼 StatusView 又是個什麼呢?其實就是一個繼承了FrameLayout的 ViewGroup,之所以要繼承 FrameLayout,因為 StatusView 此時僅僅是作為父容器存在的,並不關心內部各種狀態 View 的具體情況,所以使用 FrameLayout 就夠了,更有通用性。這樣 StatusView 也就可以在 XML 中使用了

先將上邊這部分內容轉化成程式碼:

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()方法:

@Override
protected void onFinishInflate() {
    super.onFinishInflate();
    if (getChildCount() == 1) {
        View view = getChildAt(0);
        setContentView(view);
    }
}
複製程式碼
3.2、狀態佈局的切換

StatusView 預設支援 Loading、Empty、Error 三種狀態佈局,加上原始的頁面內容佈局,一共四種。切換狀態佈局時,我們做法是直接從 StatusView 中移除掉正在顯示的狀態佈局,然後新增要顯示的狀態佈局:

private void switchStatusView(View statusView) {
    if (statusView == currentView) {
        return;
    }
    removeView(currentView);
    currentView = statusView;
    addView(currentView);
}
複製程式碼
3.3、狀態佈局的懶載入

在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;
    }
複製程式碼
3.4、更自由的用法

一般的多狀態佈局管理都會提供預設的 Loading、Empty、Error 三種狀態佈局,並可以自定義對應的狀態佈局, 並提供對應的開放 api。但這樣會有些侷限性,如果有其它業務場景的狀態佈局,雖然佈局檔案可以自定義,但原有的api方法呼叫起來難免會有違和感,並不友好!所以有必要在常用業務場景的基礎上再提供更加通用的api方法,並不侷限於特定的場景。

目前的做法是用狀態佈局和對應的索引之間的關係來實現:

// 新增指定索引對應的狀態佈局
statusView.setStatusView(int index, @LayoutRes int layoutId)
// 為指定索引的狀態佈局設定初次顯示的監聽事件,用來進行狀態佈局的相關初始化
statusView.setOnStatusViewConvertListener(int index, StatusViewConvertListener listener)
// 顯示指定索引的狀態佈局
statusView.showStatusView(int index)
複製程式碼
3.5、注意事項
  • 當 Fragment 佈局檔案的根 View 使用 StatusView 時,為避免出現的異常問題,建議在 XML 中初始化!
  • 當直接在 Fragment 中使用時,init()方法需要在onCreateView()之後的生命週期方法中執行!
  • 由於StatusView 繼承自 FrameLayout,所以會多一層佈局巢狀。

主要的點就這麼多了,剩下的就是些屬性配置的內容,其實挺簡單的,更多細節和用法可參考GitHub:StatusView

相關文章