優雅地管理 loading 頁面和標題欄

前言

如今絕大多數 App 都須要請求網絡數據,loading 動畫必不可少,而網絡請求會存在失敗的狀況,因此一般有 loading 頁面也會有加載失敗或者別的頁面。java

這些頁面的樣式大多時候是統一的,通常會想到的處理方式是把這些界面 include 到一個佈局裏,而後經過顯示隱藏的方式來切換所需的視圖。這樣會寫不少重複代碼,可能會把這部分切換的邏輯抽取到 Activity 基類中或者抽取到一個自定義 View 裏管理。android

不過這些封裝都不能解決自己存在的問題。不方便修改樣式,樣式有變更就須要修改已經封裝好的代碼。總要在佈局上增長 include 代碼或添加一個處理切換界面的自定義 View,多多少少有些冗餘。git

後面有大佬寫了工具把這些處理好了,好比目前都有上千個 star 的庫 LoadSirGloading 。這兩個庫都是很不錯的,使用起來都很靈活易用。不過也有點小小的不足,由於佈局上一般會有標題,咱們就只能每次都對標題欄下方的子 View 進行 loading。因爲標題欄樣式大多時候是統一的,一般會在佈局 include 一個標題欄,而後把標題欄的初始化封裝在 Activity 基類中……有沒發現和前面說的很像?這麼寫會存在着與前面相同的問題。github

既然標題欄會影響到 loading 的區域,並且標題欄的一般寫法所存在的問題也和 loading 界面的類似,那麼若是一個 loading 庫把標題欄一塊兒管理了,會怎麼樣呢?服務器

解決方案

LoadingHelper 是基於 Adapter 思想 和 ActionBar 原理實現的一個深度解耦 loading 界面和標題欄的工具,只用了一個 Kotlin 文件實現,不算上註釋只有 200 多行代碼。markdown

基礎用法

build.gradle 添加依賴:網絡

dependencies {
    implementation 'com.dylanc:loadinghelper:2.1.0'
}
複製代碼

第一步,建立一個適配器繼承 LoadingHelper.Adapter<VH extends ViewHolder>,寫法與 RecyclerView.Adapter 相似。若是須要實現點擊從新請求數據,能夠在點擊事件調用 holder.getOnReloadListener.onReload() 方法。app

public class LoadingAdapter extends LoadingHelper.Adapter<LoadingHelper.ViewHolder> {
  
  @NonNull
  @Override
  public LoadingHelper.ViewHolder onCreateViewHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {
    return new LoadingHelper.ViewHolder(inflater.inflate(R.layout.lce_layout_loading_view, parent, false));
  }

  @Override
  public void onBindViewHolder(@NonNull LoadingHelper.ViewHolder holder) {
	
  }
}
複製代碼

第二步,註冊適配器,傳一個視圖類型。有五個默認類型,也能夠傳任意類型數據進行註冊。ide

LoadingHelper loadingHelper = new LoadingHelper(this);
loadingHelper.register(ViewType.LOADING, new LoadingAdapter());
// 當須要支持點擊從新請求數據時
loadingHelper.setOnReloadListener(() -> {})
複製代碼

若是想註冊成全局的適配器,須要配置默認的適配器池。工具

LoadingHelper.setDefaultAdapterPool(adapterPool -> {
  adapterPool.register(ViewType.LOADING, new LoadingAdapter());
  return Unit.INSTANCE;
});
複製代碼

第三步,顯示對應類型的視圖。

loadingHelper.showView(viewType);
loadingHelper.showLoadingView(); // 對應視圖類型 ViewType.LOADING
loadingHelper.showContentView(); // 對應視圖類型 ViewType.CONTENT
loadingHelper.showErrorView(); // 對應視圖類型 ViewType.ERROR
loadingHelper.showEmptyView(); // 對應視圖類型 ViewType.EMPTY
複製代碼

動態更新已顯示視圖

在顯示了視圖以後,能夠對視圖進行更改刷新。用法和 RecyclerView.Adapter 同樣,調用 notifyDataSetChanged() 後,會執行適配器的 onBindViewHolder() 方法。

ErrorAdapter errorAdapter = loadingHelper.getAdapter(ViewType.ERROR);
errorAdapter.errorText = "服務器繁忙,請稍後重試";
errorAdapter.notifyDataSetChanged();
複製代碼

高級用法

管理標題欄

若是是普通的標題欄,就是簡單地在內容的上方添加標題欄。

這就要用到添加裝飾頭部的方法,設置以前須要註冊一個繼承 LoadingHelper.Adapter<VH extends ViewHolder> 的適配器,設置後就會在內容的上方添加該適配器建立的 View 了,能夠添加多個。

loadingHelper.register(ViewType.TITLE, new TitleAdapter("標題名"));
loadingHelper.register(VIEW_TYPE_SEARCH, new SearchHeaderAdapter(onSearchListener));
loadingHelper.setDecorHeader(ViewType.TITLE, VIEW_TYPE_SEARCH);
複製代碼

若是是特殊的標題欄,好比有聯動效果,就不能直接使用上面的方式了。

先實現一個不含內容的佈局。

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true">

  <com.google.android.material.appbar.AppBarLayout android:id="@+id/app_bar" android:layout_width="match_parent" android:layout_height="@dimen/app_bar_height" android:fitsSystemWindows="true" android:theme="@style/AppTheme.AppBarOverlay">

    <com.google.android.material.appbar.CollapsingToolbarLayout android:id="@+id/toolbar_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" app:contentScrim="?attr/colorPrimary" app:layout_scrollFlags="scroll|exitUntilCollapsed" app:toolbarId="@+id/toolbar">

      <androidx.appcompat.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:layout_collapseMode="pin" app:popupTheme="@style/AppTheme.PopupOverlay" />

    </com.google.android.material.appbar.CollapsingToolbarLayout>
  </com.google.android.material.appbar.AppBarLayout>

  <FrameLayout android:id="@+id/content_parent" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>
複製代碼

建立一個類繼承另外一個適配器 LoadingHelper.DecorAdapter ,加載實現的佈局,並指定一個 contentParent。

public class ScrollDecorAdapter extends LoadingHelper.DecorAdapter {
  @NotNull
  @Override
  public View onCreateDecorView(@NotNull LayoutInflater inflater) {
    return inflater.inflate(R.layout.layout_scrolling, null);
  }

  @NotNull
  @Override
  public ViewGroup getContentParent(@NotNull View decorView) {
    return decorView.findViewById(R.id.content_parent);
  }
}
複製代碼

最後調用一下設置裝飾適配器的方法。

loadingHelper.setDecorAdapter(new ScrollDecorAdapter());
複製代碼

上述的兩種使用方式都是能夠進行屢次設置,不過每次設置會把上一次設置的樣式給替換掉。

對內容進行解耦

雖然本庫主要是用來管理加載界面,但最終目的是爲了顯示加載好的內容,內容其實也是個視圖,那麼應該也能夠經過 Adapter 進行註冊管理。不過配置管理加載界面主要是由於樣式一般是統一的,而內容界面基本不同,會有什麼使用場景嗎?固然是有的,咱們多多少少封裝過一些 Activity 基類吧,由於有一些統一的重複操做。

如今能夠將基類對內容的操做解耦出來,方便配置管理。並且咱們封裝了一個 Activity 基類後,一般會再封裝一個相同功能的 Fragment 基類。這兩個基類會有不少相同的操做,咱們解耦後就能很容易複用,後續若是要修改也只是改一份代碼。

用法和前面的差很少,不過是建立一個適配器繼承 LoadingHelper.ContentAdapter。若是想要使用 Activity 對象,能夠在構造方法傳入或者經過 contentView 對象得到。

public class CommonContentAdapter extends LoadingHelper.ContentAdapter<LoadingHelper.ViewHolder> {
  @Override
  public LoadingHelper.ViewHolder onCreateViewHolder(@NonNull View contentView) {
    return new LoadingHelper.ViewHolder(contentView);
  }

  @Override
  public void onBindViewHolder(@NonNull LoadingHelper.ViewHolder holder) {
    View contentView = holder.getRootView();
  }
}
複製代碼

在建立 LoadingHelper 對象時傳入 ContentAdapter 對象,就會當即對內容視圖進行處理。

loadingHelper= new LoadingHelper(this, new CommonContentAdapter());
複製代碼

終極用法

LoadingHelper 能夠解耦加載中、加載失敗的界面,解耦標題欄,解耦內容的重複操做,由於最初封裝的目的就是想將視圖層的常見重複代碼進行深度解耦。

下面分享一下我的結合了本庫的特性所封裝的基類用法,展現一下使用 LoadingHelper 對視圖層進行封裝能達到怎樣的效果。

首先配置默認的適配器,否則後續使用可能會由於找不到適配器報錯。

public class App extends Application {
    
  @Override
  public void onCreate() {
    super.onCreate();
    LoadingHelper.setDefaultAdapterPool(adapterPool -> {
      adapterPool.register(ViewType.LOADING, new LoadingAdapter());
      adapterPool.register(ViewType.ERROR, new ErrorAdapter());
      adapterPool.register(ViewType.EMPTY, new EmptyAdapter());
      return Unit.INSTANCE;
    });
  }
}
複製代碼

而後就能繼承 BaseActivity 進行使用。

public class MainActivity extends BaseActivity {
  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
    setContentView(R.layout.activity_main);
    setToolbar("標題名", TitleConfig.Type.BACK, "完成", v -> {});
    loadData()
  }
  
  // 若是適配器調用 holder.getOnReloadListener.onReload(),會執行此方法從新請求
  @Override
  public void onReload() {
    loadData()
  }

  public void loadData() {
    showLoadingView(); // 展現加載視圖
    // (發起請求,回調請求成功或請求失敗的方法)
  }

  private void requestDataSuccess(String data){
    showContentView(); // 展現內容視圖
    Toast.makeText(this, data, Toast.LENGTH_SHORT).show();
  }

  private void requestDataFailure(String msg){
    showErrorView(); // 展現錯誤視圖
    Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
  }
}
複製代碼

當須要對該頁面的樣式進行定製時,只需增長少許代碼。

public class MainActivity extends BaseActivity {

  private static final int VIEW_TYPE_SEARCH = 0;

  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
    // 重載的第二個參數是子 View 的 id,配置後會對該 View 進行 loading,可用於如 DrawLayout 等較複雜佈局
    // 重載的第三個參數是用於替換默認管理內容的適配器
    setContentView(R.layout.activity_main, R.id.content_view, new CustomContentAdapter());
    
    getLoadingHelper().register(ViewType.TITLE, new CustomTitleAdapter()); // 替換默認標題欄
    getLoadingHelper().register(ViewType.LOADING, new CustomLoadingAdapter()); // 替換默認加載視圖
    getLoadingHelper().register(ViewType.ERROR, new CustomErrorAdapter()); // 替換默認錯誤視圖
    getLoadingHelper().register(VIEW_TYPE_SEARCH, new SearchHeaderAdapter()); // 配置搜索視圖
    
    getLoadingHelper().setDecorHeader(ViewType.TITLE, VIEW_TYPE_SEARCH); // 添加自定義標題欄和搜索頭部
    loadData()
  }
  
  @Override
  public void onReload() {
    loadData()
  }

  @Override
  public void loadData() {
    showLoadingView();
    // (發起請求,回調請求成功或請求失敗的方法)
  }

  private void requestDataSuccess(String data) {
    showContentView();
    Toast.makeText(this, data, Toast.LENGTH_SHORT).show();
  }

  private void requestDataFailure(String msg) {
    showErrorView();
    Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
  }
}
複製代碼

不是什麼複雜的封裝,主要是結合了些我的以爲比較好的用法。好比沒有用 getLayoutId() 而是保留重載了原有的 setContentView() 方法,經過佈局和控件 id 來肯定 contentView,而後用 showContentView() 方法展現出來。前面 set 了後面 show,代碼閱讀起來也很合理。

感謝

  • luckbilly/Gloading 站在了巨人肩膀上優化了本庫,很是感謝! 該庫給了我不少啓發,我我的花了不少的時間也摸索出用適配器封裝比較好,實現原理和思路都是相似的,可是該庫竟然用不到 200 行代碼來實現。當時我寫的代碼量是這個的數倍,由於有點貪什麼功能都想作,並且爲了一些我的使用習慣增長了很多代碼。後面明確了定位,作了大量的減法。最終優化成用一個 200 多行 Kotlin 代碼的工具實現了原有的核心功能,同時保證了靈活性和易用性。
  • drakeet/MultiType 我的一直在使用的列表庫,參考了該庫註冊配置多適配器的思想和用法。本庫原來用法是隻實現一個適配器,使用方式更相似 RecyclerView,學習成本更低。不過綜合考慮後以爲仍是拆分多個適配器註冊使用更加靈活方便。

總結

本文講述了一些管理 loading 界面的方案,分析了一些存在的不足。重點介紹了能同時管理 loading 界面和標題欄的工具 LoadingHelper,相對於同類型的 loading 庫,該有的功能都有,還能解耦標題欄和解耦對內容的重複操做,可以進一步解耦視圖層。最後展現告終合本庫對基類進行封裝使用能達到怎麼樣的效果。

若是你以爲本庫還不錯的話但願能給個 star 支持一下哦~

相關文章
相關標籤/搜索