LoadingBar - 如何更优雅的使用Loading

Github地址:
https://github.com/xiandanin/LoadingBar

前言

Loading是很普遍的需求,比如请求的时候需要显示Loading,请求完成以后再取消Loading,而一般的实现方式是在布局xml里添加一个ProgressBar,但是这样写就有很多不便,每个页面的layout都要写一个ProgressBar,显示的位置也固定了,还耦合了很多代码。
而LoadingBar就是为了跟方便的操作Loading而生,高度解耦,样式全部通过工厂类决定。

结构介绍

LoadingBar - 适合一些显示数据的操作,比如请求列表
LoadingDialog - 适合一些提交数据的操作,比如注册,登录
Factory - 决定了loading的样式,自定义样式只需实现Factory

快速开始

Android Studio - 在build.gradle中引入

1
2
3
4
compile 'com.dyhdyh.loadingbar:loadingbar:1.4.7'

//appcompat
implementation "com.android.support:appcompat-v7:xxx"

LoadingBar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//默认样式 loading将会覆盖在parent的内容上面
LoadingBar.make(parent).show();

//自定义样式
//提供两种形式,loadingView更简便,loadingFactory自由度更高
LoadingBar.make(parent,loadingView).show();
LoadingBar.make(parent,loadingFactory).show();

//完全自定义
LoadingBar.make(parent,loadingFactory)
.setOnClickListener(clickListener)//点击事件
.setOnLoadingBarListener(loadingBarListener)//当loadingbar取消的时候回调
.show();

//取消Loading
LoadingBar.cancel(parent);

LoadingDialog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//默认样式
LoadingDialog.make(context).show();

//自定义样式
LoadingDialog.make(context, dialogFactory).show();

//完全自定义
LoadingDialog.make(context, dialogFactory)
.setMessage(message)//提示消息
.setCancelable(cancelable)
.show();

//设置更多属性
Dialog dialog = LoadingDialog.make(context, dialogFactory)
.setMessage(message)//提示消息
.setCancelable(cancelable)
.create();
dialog.setOnCancelListener(cancelListener);
dialog.set...
dialog.show();

//取消Loading
LoadingDialog.cancel();

自定义Factory

1
2
3
4
5
6
7
8
public class CustomLoadingFactory implements LoadingFactory {

@Override
public View onCreateView(ViewGroup parent) {
View loadingView = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_custom, parent,false);
return loadingView;
}
}

全局配置

1
2
//自定义样式并应用于全局
LoadingConfig.setFactory(loadingFactory,dialogFactory);

资源释放

其实LoadingBar在cancel的时候已经释放掉了,可以不用手动释放,但是这里也提供释放的方法,根据自己需要选择

在Activity onDestroy调用,个人建议在BaseActivity,资源释放只会释放无效的资源

1
LoadingBar.release();

源码解析

定义结构

首先我一开始就想好了,这得有三样东西,LoadingBar与LoadingDialog,用Factory来生产loading所需要的View和Dialog

定义接口

两者的共同点是都会有显示,所以我定义了一个共用的接口

1
2
3
public interface ILoading {
void show();
}

LoadingBar除了有show还得有cancel
1
2
3
public interface ILoadingBar extends ILoading {
void cancel();
}

LoadingDialog最终都是操作Dialog,所以它得有create,再附加一些设置常用属性的方法
1
2
3
4
5
6
7
8
public interface ILoadingDialog extends ILoading {

Dialog create();

ILoadingDialog setCancelable(boolean flag);

ILoadingDialog setMessage(CharSequence message);
}

LoadingFactory onCreateView返回的View就决定了Loading长什么样
1
2
3
4
public interface LoadingFactory {

View onCreateView(ViewGroup parent);
}

DialogFactory 主要是onCreateDialog,这个方法决定了Dialog长什么样,在这里实现创建Dialog
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface DialogFactory {
/**
* 创建dialog
* @param context
* @return
*/
Dialog onCreateDialog(Context context);

/**
* 设置提示消息
* @param dialog
* @param message
*/
void setMessage(Dialog dialog,CharSequence message);

/**
* 进入退出的动画id
* @return
*/
@StyleRes int getAnimateStyleId();
}

LoadingBar的实现

其实就是需要两个View,mView就是factory.onCreateView返回的LoadingView,mParent就是现实

1
2
3
4
private LoadingBar(ViewGroup parent, LoadingFactory factory) {
mParent = parent;
mView = factory.onCreateView(mParent);
}

然后把mView添加到mParent里,这样mView就处于最上层,覆盖着内容,这样就达到了Loading的效果
1
2
3
4
5
6
7
8
9
public void show() {
if (mView != null) {
mView.setVisibility(View.VISIBLE);
if (mView.getParent() != null) {
mParent.removeView(mView);
}
mParent.addView(mView);
}
}

取消很简单,就直接把mView移除掉就好了
1
2
3
4
5
6
7
8
9
10
public void cancel() {
if (mView != null) {
mView.setVisibility(View.GONE);
mParent.removeView(mView);
mView = null;
if (this.mListener != null) {
this.mListener.onCancel(mParent);
}
}
}

值得一说的还有findSuitableParent,因为Loading是要在可覆盖的布局上才有作用的,而当parent传的是非覆盖的布局(例如LinearLayout),这个方法会一直往外层寻找可覆盖的布局
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static ViewGroup findSuitableParent(View parent) {
if (parent == null) {
return null;
}
View suitableParent = parent;
do {
if (suitableParent instanceof FrameLayout || suitableParent instanceof RelativeLayout ||
"android.support.v4.widget.DrawerLayout".equals(suitableParent.getClass().getName()) ||
"android.support.design.widget.CoordinatorLayout".equals(suitableParent.getClass().getName()) ||
"android.support.v7.widget.CardView".equals(suitableParent.getClass().getName())) {
return (ViewGroup) suitableParent;
} else {
final ViewParent viewParent = suitableParent.getParent();
suitableParent = viewParent instanceof View ? (View) viewParent : null;
return (ViewGroup) suitableParent;
}
} while (suitableParent != null);
}

LoadingDialog的实现

构造方法先用factory创建了dialog,如果有动画设置动画

1
2
3
4
5
6
7
8
public LoadingDialog(Context context, DialogFactory factory) {
this.mDialog = factory.onCreateDialog(context);
this.mFactory = factory;
int animateStyleId = this.mFactory.getAnimateStyleId();
if (animateStyleId > 0) {
this.mDialog.getWindow().setWindowAnimations(animateStyleId);
}
}

因为Dialog是单例,如果在Activity已经finish了再去操作做个Dialog的话,就会抛异常,所以在show与cancel的时候要先检查是否能够操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void show() {
if (isValid() && !mDialog.isShowing()) {
mDialog.show();
}
}

public void cancelDialog() {
if (isValid() && mDialog.isShowing()) {
mDialog.cancel();
}
}

private boolean isValid() {
if (mDialog != null) {
Context context = mDialog.getContext();
if (context instanceof ContextWrapper){
context = ((ContextWrapper) context).getBaseContext();
}
if (context instanceof Activity) {
if (!((Activity) context).isFinishing()) {
return true;
}
}
}
return false;
}

总结

使用场景不局限于请求,其实还有很多异步操作都能用上
比如压缩图片,也可以用LoadingDialog
更多玩法等你挖掘,有问题可以去Github的issue提出