天天看點

我們經常用的Loading動畫居然還有這種姿勢

背景

Loading動畫幾乎每個Android App中都有。

一般在需要使用者等待的場景,顯示一個Loading動畫可以讓使用者知道App正在加載資料,而不是程式卡死,進而給使用者較好的使用體驗。

同樣的道理,當加載的資料為空時顯示一個資料為空的視圖、在資料加載失敗時顯示加載失敗對應的UI并支援點選重試會比白屏的使用者體驗更好一些。

加載中、加載失敗、空資料的UI風格,一般來說在App内的所有頁面中需要保持一緻,也就是需要做到全局統一。

1. 傳統的做法

  1. 定義一個(或多個)顯示不同加載狀态的控件或者xml布局檔案(例如:

    LoadingView

  2. 每個頁面的布局中都寫上這個view
  3. BaseActivity/BaseFragment

    中封裝

    LoadingView

    的初始化邏輯,并封裝加載狀态切換時的UI顯示邏輯,暴露給子類以下方法:
    • void showLoading();

      //調用此方法顯示加載中的動畫
    • void showLoadFailed();

      //調用此方法顯示加載失敗界面
    • void showEmpty();

      //調用此方法顯示空頁面
    • void onClickRetry();

      //子類中實作,點選重試的回調方法
  4. BaseActivity/BaseFragment

    的子類中可通過上一步的封裝比較友善地使用加載狀态顯示功能

這種使用方式耦合度太高,每個頁面的布局檔案中都需要添加

LoadingView

,使用起來不友善而且維護成本較高,一旦UI設計師需要更改布局,修改起來成本較高。

2. 好一點的封裝方法

  1. LoadingView

  2. 定義一個工具類(

    LoadingUtil

    )來管理

    LoadingView

    ,不同狀态顯示不同的UI(或者在多個View之間切換顯示)
  3. BaseActivity/BaseFragment

    中對

    LoadingUtil

    的使用進行封裝,暴露給子類以下方法:
    • void showLoading();

    • void showLoadFailed();

    • void showEmpty();

    • void onClickRetry();

    • abstract int getContainerId(); //子類中實作,LoadingUtil動态建立LoadingView并添加到該方法傳回id對應的控件中
  4. BaseActivity/BaseFragment

這種封裝的好處是通過封裝動态地建立

LoadingView

并添加到指定的父容器中,讓具體頁面無需關注

LoadingView

的實作,隻需要指定在哪個容器中顯示即可,很大程度地進行了解耦。如果公司隻在一個App中使用,這基本上就夠了。

但是,這種封裝方式還是存在耦合:頁面與它所使用的

LoadingView

仍然存在綁定關系。如果需要複用到其它App中,因為每個App的UI風格可能不同,對應的

LoadingView

布局也可能會不一樣,要想複用必須先将頁面與

LoadingView

解耦。

如何解耦?

1. 梳理一下我們需要實作的效果

  • 頁面的

    LoadingView

    可切換,且不需要改動頁面代碼
  • 頁面中可指定

    LoadingView

    的顯示區域(例如導航欄Title不希望被

    LoadingView

    覆寫)
  • 支援在Fragment中使用
  • 支援加載失敗頁面中點選重試
  • 相容不同頁面顯示的UI有細微差别(例如提示文字可能不同)

2. 确定思路

說到View的解耦,很容易聯想到Android系統中的AdapterView(我們常用的GridView和ListView都是它的子類)及support包裡提供的ViewPager、RecyclerView等,它們都是通過Adapter來解耦的,将自身的邏輯與需要動态變化的子View進行分離。我們也可以按照這個思路來解耦

LoadingView

:

  • 建立一個工具類,用于管理LoadingView各個狀态的UI展示
  • 建立一個Adapter接口,外部提供實作類,通過getView方法建立具體的LoadingView
  • 每個App提供一個Adapter的實作,并注冊到工具類中
  • 工具類從Adapter.getView擷取具體的LoadingView,是以頁面中使用的代碼無需改動
(已實作)頁面的LoadingView可切換,且不需要改動頁面代碼            
  • 由于每個頁面或View的加載狀态互相之間無關聯關系,需要建立一個用于管理具體某個LoadingView的狀态持有類:Holder
  • 指定LoadingView所需覆寫的View時,動态建立一個FrameLayout布局
  • 将原View從ParentView中移除,并用它的LayoutParams将FrameLayout添加到ParentView中替代原View在ParentView中的位置
  • 再将原View添加到FrameLayout中
  • 在Fragment.onCreateView/RecyclerView.Adapter.onCreateViewHolder等方法中建立的View時,由于View尚未添加到任何容器中,并無getParent()傳回null,此時需要用動态生成的FrameLayout代替原View作為方法的傳回值傳回

上代碼更容易了解:

public Holder wrap(View view) {
    FrameLayout wrapper = new FrameLayout(view.getContext());
    ViewGroup.LayoutParams lp = view.getLayoutParams();
    if (lp != null) {
        wrapper.setLayoutParams(lp);
    }
    if (view.getParent() != null) {
        ViewGroup parent = (ViewGroup) view.getParent();
        int index = parent.indexOfChild(view);
        parent.removeView(view);
        parent.addView(wrapper, index);
    }
    LayoutParams newLp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    wrapper.addView(view, newLp);
    return new Holder(mAdapter, view.getContext(), wrapper);
}           
(已實作)頁面中可指定LoadingView的顯示區域
(已實作)支援在Fragment中使用
另外,還順帶支援在RecyclerView、ListView、GridView、ViewPager等情況下的使用           
  • 為了不侵入UI,将加載失敗點選重試的點選功能放在

    Adapter.getView

    中實作
  • 與Android系統中的Adapter不同的是,我們的

    Adapter

    是全局使用的,而失敗重試所需執行邏輯每個頁面都不一樣
  • 因為

    Holder

    可以持有每個具體的

    LoadingView

    ,可以将

    retryTask

    通過

    Holder

    傳遞給

    Adapter

  • 隻需要在

    Adapter.getView

    時将

    Holder

    作為參數傳入,即可在建立

    LoadingView

    時擷取該

    retryTask

    對象,并在點選重試按鈕時執行

    retryTask

  • 同理,可以通過

    Holder

    傳遞一些附加參數給

    Adapter

    ,以相容在不同頁面上布局的細微差異
(已實作)支援加載失敗頁面中點選重試
(已實作)相容不同頁面顯示的UI有細微差别(例如提示文字可能不同)           

使用Gloading來輕松實作低耦合的全局LoadingView

Gloading

是一個基于Adapter思路實作的深度解耦App中全局LoadingView的輕量級工具(隻有一個java檔案,不到300行,其中注釋占100+行,aar僅6K)

1、 依賴Gloading

compile 'com.billy.android:gloading:1.0.0'           

2、 建立

Adapter

,在

getView

方法中實作建立各種狀态視圖(加載中、加載失敗、空資料等)的邏輯

Gloading不侵入UI布局,完全由使用者自定義。示例如下:

public class GlobalAdapter implements Gloading.Adapter {
    @Override
    public View getView(Gloading.Holder holder, View convertView, int status) {
        GlobalLoadingStatusView loadingStatusView = null;
        //convertView為可重用的布局
        //Holder中緩存了各狀态下對應的View
        //    如果status對應的View為null,則convertView為上一個狀态的View
        //    如果上一個狀态的View也為null,則convertView為null
        if (convertView != null && convertView instanceof GlobalLoadingStatusView) {
            loadingStatusView = (GlobalLoadingStatusView) convertView;
        }
        if (loadingStatusView == null) {
            loadingStatusView = new GlobalLoadingStatusView(holder.getContext(), holder.getRetryTask());
        }
        loadingStatusView.setStatus(status);
        return loadingStatusView;
    }
    
    class GlobalLoadingStatusView extends RelativeLayout {

        public GlobalLoadingStatusView(Context context, Runnable retryTask) {
            super(context);
            //初始化LoadingView
            //如果需要支援點選重試,在适當的時機給對應的控件添加點選事件
        }
        
        public void setStatus(int status) {
            //設定目前的加載狀态:加載中、加載失敗、空資料等
            //其中,加載失敗可判斷目前是否聯網,可現實無網絡的狀态
            //        屬于加載失敗狀态下的一個分支,可自行決定是否實作
        }
    }
}           

3、 初始化

Gloading

的預設

Adapter

Gloading.initDefault(new GlobalAdapter());           

注:可以用

AutoRegister

在Gloading類裝載進虛拟機時自動完成初始化注冊,無需在app層執行注冊,耦合度更低

4、在需要使用

LoadingView

的地方擷取

Holder

//在Activity中顯示, 父容器為: android.R.id.content
Gloading.Holder holder = Gloading.getDefault().wrap(activity);

//傳遞點選重試需要執行的task,該task在Adapter中用holder.getRetryTask()擷取
Gloading.Holder holder = Gloading.getDefault().wrap(activity).withRetry(retryTask);

//傳遞點選重試需要執行的task和一個任意類型的擴充參數,該參數在Adapter中用holder.getData()擷取
Gloading.Holder holder = Gloading.getDefault().wrap(activity).withRetry(retryTask).withData(obj);           

or

//為某個View顯示加載狀态
//Gloading會自動建立一個FrameLayout,将view包裹起來,LoadingView也顯示在其中
Gloading.Holder holder = Gloading.getDefault().wrap(view);

//傳遞點選重試需要執行的task,該task在Adapter中用holder.getRetryTask()擷取
Gloading.Holder holder = Gloading.getDefault().wrap(view).withRetry(retryTask);

//傳遞點選重試需要執行的task和一個任意類型的擴充參數,該參數在Adapter中用holder.getData()擷取
Gloading.Holder holder = Gloading.getDefault().wrap(view).withRetry(retryTask).withData(obj);           

5、 使用

Holder

來顯示各種加載狀态

//顯示加載中的狀态,通常是顯示一個加載動畫
holder.showLoading() 

//顯示加載成功狀态(一般是隐藏LoadingView)
holder.showLoadSuccess()

//顯示加載失敗狀态
holder.showFailed()

//資料加載完成,但資料為空
holder.showEmpty()

//如果以上預設提供的狀态不能滿足使用,可使用此方法調用其它狀态
holder.showLoadingStatus(status)           

更多API詳情請檢視

Gloading JavaDocs

更多Demo示例代碼請檢視

Gloading Demo

, 也可

下載下傳Demo apk

體驗

6、封裝到BaseActivity/BaseFragment中

  • 讓BaseActivity和BaseFragment的子類中使用LoadingView更友善
  • 子類中使用LoadingView的業務邏輯與實作分離
  • 如果原來就是封裝到BaseActivity/BaseFragment中的,那麼可以無縫切換到Gloading
  • 如果以後需要将Gloading移除替換成其它實作,也無需修改業務代碼

示例代碼如下:

public abstract class BaseActivity extends Activity {

    protected Gloading.Holder mHolder;

    /**
     * make a Gloading.Holder wrap with current activity by default
     * override this method in subclass to do special initialization
     * @see SpecialActivity
     */
    protected void initLoadingStatusViewIfNeed() {
        if (mHolder == null) {
            //bind status view to activity root view by default
            mHolder = Gloading.getDefault().wrap(this).withRetry(new Runnable() {
                @Override
                public void run() {
                    onLoadRetry();
                }
            });
        }
    }

    protected void onLoadRetry() {
        // override this method in subclass to do retry task
    }

    public void showLoading() {
        initLoadingStatusViewIfNeed();
        mHolder.showLoading();
    }

    public void showLoadSuccess() {
        initLoadingStatusViewIfNeed();
        mHolder.showLoadSuccess();
    }

    public void showLoadFailed() {
        initLoadingStatusViewIfNeed();
        mHolder.showLoadFailed();
    }

    public void showEmpty() {
        initLoadingStatusViewIfNeed();
        mHolder.showEmpty();
    }

}           

7、 相容多App場景下的頁面、View的複用

每個App的

LoadingView

可能會不同,隻需為每個App提供不同的

Adapter

,不同App調用不同的

Gloading.initDefault(new GlobalAdapter());

,具體頁面中的使用代碼無需改動。

注:如果使用

,則隻需在不同App中建立各自的

Adapter

實作類即可,無需手動注冊。隻需改動2處gradle檔案即可:

  • 修改根目錄build.gradle,添加對AutoRegister的依賴
buildscript {
    //...
    dependencies {
        //...
        classpath 'com.billy.android:autoregister:使用最新版'
    }
}           
  • 修改主application module下的build.gradle,添加如下代碼即可實作Adapter的自動注冊
apply plugin: 'auto-register'
autoregister {
   registerInfo = [
       [
           'scanInterface'             : 'com.billy.android.loading.Gloading$Adapter'
           , 'codeInsertToClassName'   : 'com.billy.android.loading.Gloading'
           , 'registerMethodName'      : 'initDefault'
       ]
   ]
}           

示範

為Activity添加加載狀态

加載成功

加載失敗

點選重試

無資料 個别頁面使用特殊的Loading視圖
我們經常用的Loading動畫居然還有這種姿勢
我們經常用的Loading動畫居然還有這種姿勢
我們經常用的Loading動畫居然還有這種姿勢
我們經常用的Loading動畫居然還有這種姿勢

為View添加加載狀态

單個View 多個View 用于GridView

用于RecyclerView

并且不顯示文字

我們經常用的Loading動畫居然還有這種姿勢
我們經常用的Loading動畫居然還有這種姿勢
我們經常用的Loading動畫居然還有這種姿勢
我們經常用的Loading動畫居然還有這種姿勢

總結

本文介紹了全局LoadingView在實際使用過程中可能存在的一些耦合情況,并指出了由此會影響多個App的LoadingView的UI風格不一緻導緻頁面難以複用的問題,同時給出了解決思路。

另外,本文着重介紹了如何使用

來輕松實作低耦合的全局LoadingView,喜歡的同學請順手甩個star支援一下 :)

繼續閱讀