歡迎光臨
每天分享高質量文章

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

作者:齊翊

連結:https://juejin.im/post/5c9649145188252d665f5229

背景

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、定義一個(或多個)顯示不同載入狀態的控制元件或者xml佈局檔案(例如:LoadingView)

2、定義一個工具類(LoadingUtil)來管理LoadingView,不同狀態顯示不同的UI(或者在多個View之間切換顯示)

3、在BaseActivity/BaseFragment中對LoadingUtil的使用進行封裝,暴露給子類以下方法:

 

  1. void showLoading(); //呼叫此方法顯示載入中的動畫

  2. void showLoadFailed(); //呼叫此方法顯示載入失敗介面

  3. void showEmpty(); //呼叫此方法顯示空頁面

  4. void onClickRetry(); //子類中實現,點選重試的回呼方法

  5. abstract int getContainerId(); //子類中實現,LoadingUtil動態建立LoadingView並新增到該方法傳回id對應的控制元件中

 

4、在BaseActivity/BaseFragment的子類中可透過上一步的封裝比較方便地使用載入狀態顯示功能

這種封裝的好處是透過封裝動態地建立LoadingView並新增到指定的父容器中,讓具體頁面無需關註LoadingView的實現,只需要指定在哪個容器中顯示即可,很大程度地進行瞭解耦。

如果公司只在一個App中使用,這基本上就夠了。

但是,這種封裝方式還是存在耦合:頁面與它所使用的LoadingView仍然存在系結關係。如果需要復用到其它App中,因為每個App的UI風格可能不同,對應的LoadingView佈局也可能會不一樣,要想復用必須先將頁面與LoadingView解耦。

如何解耦?

1、梳理一下我們需要實現的效果

 

  1. 頁面的LoadingView可切換,且不需要改動頁面程式碼

  2. 頁面中可指定LoadingView的顯示區域(例如導航欄Title不希望被LoadingView改寫)

  3. 支援在Fragment中使用

  4. 支援載入失敗頁面中點選重試

  5. 相容不同頁面顯示的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)

https://github.com/luckybilly/Gloading

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層執行註冊,耦合度更低

https://github.com/luckybilly/AutoRegister

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

https://luckybilly.github.io/Gloading/

更多Demo示例程式碼請檢視 Gloading Demo, 也可下載Demo apk體驗

https://github.com/luckybilly/Gloading/tree/master/app/src/main/java/com/billy/android/loadingstatusview

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());,具體頁面中的使用程式碼無需改動。

註:如果使用AutoRegister,則只需在不同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'
       ]
   ]
}

演示


1、為Activity新增載入狀態

 

為View新增載入狀態

 

總結

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

另外,本文著重介紹瞭如何使用Gloading來輕鬆實現低耦合的全域性LoadingView,喜歡的同學請順手甩個star支援一下 :)

https://github.com/luckybilly/Gloading

已同步到看一看
贊(0)

分享創造快樂