我们经常用的Loading动画居然还有这种姿势

30次阅读

共计 7555 个字符,预计需要花费 19 分钟才能阅读完成。

1 背景
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 解耦。
2 如何解耦?
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 可切换,且不需要改动页面代码
</pre>

由于每个页面或 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 等情况下的使用 </pre>

为了不侵入 UI,将加载失败点击重试的点击功能放在 Adapter.getView 中实现
与 Android 系统中的 Adapter 不同的是,我们的 Adapter 是全局使用的,而失败重试所需执行逻辑每个页面都不一样
因为 Holder 可以持有每个具体的 LoadingView,可以将 retryTask 通过 Holder 传递给 Adapter
只需要在 Adapter.getView 时将 Holder 作为参数传入,即可在创建 LoadingView 时获取该 retryTask 对象,并在点击重试按钮时执行 retryTask
同理,可以通过 Holder 传递一些附加参数给 Adapter,以兼容在不同页面上布局的细微差异

(已实现)支持加载失败页面中点击重试
(已实现)兼容不同页面显示的 UI 有细微差别(例如提示文字可能不同)
3 使用 Gloading 来轻松实现低耦合的全局 LoadingView</pre>
Gloading 是一个基于 Adapter 思路实现的深度解耦 App 中全局 LoadingView 的轻量级工具(只有一个 java 文件,不到 300 行,其中注释占 100+ 行,aar 仅 6K)
https://github.com/luckybilly…
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…
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/…
更多 Demo 示例代码请查看 Gloading Demo,也可下载 Demo apk 体验
https://github.com/luckybilly…
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’
]
]
}
4 演示
1. 为 Activity 添加加载状态

为 View 添加加载状态

总结
本文介绍了全局 LoadingView 在实际使用过程中可能存在的一些耦合情况,并指出了由此会影响多个 App 的 LoadingView 的 UI 风格不一致导致页面难以复用的问题,同时给出了解决思路。
另外,本文着重介绍了如何使用 Gloading 来轻松实现低耦合的全局 LoadingView,喜欢的同学请顺手甩个 star 支持一下 :)
https://github.com/luckybilly…

正文完
 0