作者:齊翊
連結: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的使用進行封裝,暴露給子類以下方法:
-
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)
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
朋友會在“發現-看一看”看到你“在看”的內容