咱們常常用的Loading動畫竟然還有這種姿式

背景

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

通常在須要用戶等待的場景,顯示一個Loading動畫可讓用戶知道App正在加載數據,而不是程序卡死,從而給用戶較好的使用體驗。android

一樣的道理,當加載的數據爲空時顯示一個數據爲空的視圖、在數據加載失敗時顯示加載失敗對應的UI並支持點擊重試會比白屏的用戶體驗更好一些。git

加載中、加載失敗、空數據的UI風格,通常來講在App內的全部頁面中須要保持一致,也就是須要作到全局統一。github

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的使用進行封裝,暴露給子類如下方法:
    • void showLoading(); //調用此方法顯示加載中的動畫
    • void showLoadFailed(); //調用此方法顯示加載失敗界面
    • void showEmpty(); //調用此方法顯示空頁面
    • void onClickRetry(); //子類中實現,點擊重試的回調方法
    • abstract int getContainerId(); //子類中實現,LoadingUtil動態建立LoadingView並添加到該方法返回id對應的控件中
  4. BaseActivity/BaseFragment的子類中可經過上一步的封裝比較方便地使用加載狀態顯示功能

這種封裝的好處是經過封裝動態地建立LoadingView並添加到指定的父容器中,讓具體頁面無需關注LoadingView的實現,只須要指定在哪一個容器中顯示便可,很大程度地進行了解耦。若是公司只在一個App中使用,這基本上就夠了。bash

可是,這種封裝方式仍是存在耦合:頁面與它所使用的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:app

  • 建立一個工具類,用於管理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做爲方法的返回值返回

上代碼更容易理解:ide

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)工具

一、 依賴Gloading

compile 'com.billy.android:gloading:1.0.0'
複製代碼

二、 建立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) {
            //設置當前的加載狀態:加載中、加載失敗、空數據等
            //其中,加載失敗可判斷當前是否聯網,可現實無網絡的狀態
            // 屬於加載失敗狀態下的一個分支,可自行決定是否實現
        }
    }
}
複製代碼

三、 初始化Gloading的默認Adapter

Gloading.initDefault(new GlobalAdapter());
複製代碼

注:能夠用AutoRegister在Gloading類裝載進虛擬機時自動完成初始化註冊,無需在app層執行註冊,耦合度更低

四、在須要使用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);
複製代碼

五、 使用Holder來顯示各類加載狀態

//顯示加載中的狀態,一般是顯示一個加載動畫
holder.showLoading() 

//顯示加載成功狀態(通常是隱藏LoadingView)
holder.showLoadSuccess()

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

//數據加載完成,但數據爲空
holder.showEmpty()

//若是以上默認提供的狀態不能知足使用,可以使用此方法調用其它狀態
holder.showLoadingStatus(status)
複製代碼

更多API詳情請查看 Gloading JavaDocs

更多Demo示例代碼請查看 Gloading Demo, 也可下載Demo apk體驗

六、封裝到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();
    }

}
複製代碼

七、 兼容多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會成爲做者開源的動力哦 :)

相關文章
相關標籤/搜索