背景
Loading動畫幾乎每個Android App中都有。
一般在需要使用者等待的場景,顯示一個Loading動畫可以讓使用者知道App正在加載資料,而不是程式卡死,進而給使用者較好的使用體驗。
同樣的道理,當加載的資料為空時顯示一個資料為空的視圖、在資料加載失敗時顯示加載失敗對應的UI并支援點選重試會比白屏的使用者體驗更好一些。
加載中、加載失敗、空資料的UI風格,一般來說在App内的所有頁面中需要保持一緻,也就是需要做到全局統一。
1. 傳統的做法
- 定義一個(或多個)顯示不同加載狀态的控件或者xml布局檔案(例如:
)LoadingView
- 每個頁面的布局中都寫上這個view
- 在
中封裝BaseActivity/BaseFragment
的初始化邏輯,并封裝加載狀态切換時的UI顯示邏輯,暴露給子類以下方法:LoadingView
-
//調用此方法顯示加載中的動畫void showLoading();
-
//調用此方法顯示加載失敗界面void showLoadFailed();
-
//調用此方法顯示空頁面void showEmpty();
-
//子類中實作,點選重試的回調方法void onClickRetry();
-
-
的子類中可通過上一步的封裝比較友善地使用加載狀态顯示功能BaseActivity/BaseFragment
這種使用方式耦合度太高,每個頁面的布局檔案中都需要添加
LoadingView
,使用起來不友善而且維護成本較高,一旦UI設計師需要更改布局,修改起來成本較高。
2. 好一點的封裝方法
-
LoadingView
- 定義一個工具類(
)來管理LoadingUtil
,不同狀态顯示不同的UI(或者在多個View之間切換顯示)LoadingView
-
中對BaseActivity/BaseFragment
的使用進行封裝,暴露給子類以下方法:LoadingUtil
-
void showLoading();
-
void showLoadFailed();
-
void showEmpty();
-
void onClickRetry();
- abstract int getContainerId(); //子類中實作,LoadingUtil動态建立LoadingView并添加到該方法傳回id對應的控件中
-
-
BaseActivity/BaseFragment
這種封裝的好處是通過封裝動态地建立
LoadingView
并添加到指定的父容器中,讓具體頁面無需關注
LoadingView
的實作,隻需要指定在哪個容器中顯示即可,很大程度地進行了解耦。如果公司隻在一個App中使用,這基本上就夠了。
但是,這種封裝方式還是存在耦合:頁面與它所使用的
LoadingView
仍然存在綁定關系。如果需要複用到其它App中,因為每個App的UI風格可能不同,對應的
LoadingView
布局也可能會不一樣,要想複用必須先将頁面與
LoadingView
解耦。
如何解耦?
1. 梳理一下我們需要實作的效果
- 頁面的
可切換,且不需要改動頁面代碼LoadingView
- 頁面中可指定
的顯示區域(例如導航欄Title不希望被LoadingView
覆寫)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視圖 |
---|---|---|---|
為View添加加載狀态
單個View | 多個View | 用于GridView | 用于RecyclerView 并且不顯示文字 |
---|---|---|---|
總結
本文介紹了全局LoadingView在實際使用過程中可能存在的一些耦合情況,并指出了由此會影響多個App的LoadingView的UI風格不一緻導緻頁面難以複用的問題,同時給出了解決思路。
另外,本文着重介紹了如何使用
來輕松實作低耦合的全局LoadingView,喜歡的同學請順手甩個star支援一下 :)