Android通用的EmptyLayout-展示不用状态的界面

前言

在做项目的时候,经常会遇到列表数据为空的时候展示的空布局,如果你用的是ListView ,目测会经常使用ListView的一个方法setEmptyView ,如果你用的是RecyclerView,你也许会用自定义View来实现,但是,这些方法虽然使用起来简单,但是如果你提供一个复杂的布局,例如:

在数据加载失败后,添加一个Button让用户可以选择重新加载数据。

你肯定会说,findviewbyId找到这个button,给它设置点击事件,一个两个可以接受,但是,界面多了呢? 那你说了那么多,有没有好的解决办法呢? 当然有 而且是几行代码搞定的

自定义View

接下来就是重头戏 开始编码了 ,首先我们需要继承FrameLayout来实现这样的布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class EmptyLayout extends FrameLayout {
public EmptyLayout(Context context) {
this(context, null);
}
public EmptyLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public EmptyLayout(Context context, AttributeSet attrs, int defStyleAttr) {
}
}

为了灵活性,我自定义属性来添加所需要的布局,values下面新建attrs.xml

1
2
3
4
5
6
7
8
<resources>
<declare-styleable name="EmptyLayout">
<attr name="elEmptyLayout" format="reference"/>
<attr name="elErrorLayout" format="reference"/>
<attr name="elLoadingLayout" format="reference"/>
</declare-styleable>
</resources>

然后我们以此找到这些布局,并且添加进去

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class EmptyLayout extends FrameLayout {
private Context mContext;
private View mEmptyView;
private View mBindView;
private View mErrorView;
private Button mBtnReset;
private View mLoadingView;
private View loadingView;
private TextView mEmptyText;
private TextView tvLoadingText;
public EmptyLayout(Context context) {
this(context, null);
}
public EmptyLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public EmptyLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext=context;
LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
//居中
params.gravity = Gravity.CENTER;
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.EmptyLayout, 0, 0);
//数据为空时的布局
int emptyLayout = ta.getResourceId(R.styleable.EmptyLayout_elEmptyLayout, R.layout.layout_empty);
mEmptyView = View.inflate(context, emptyLayout, null);
mEmptyText =(TextView)mEmptyView.findViewById(R.id.tvEmptyText);
addView(mEmptyView,params);
//加载中的布局
int loadingLayout = ta.getResourceId(R.styleable.EmptyLayout_elLoadingLayout, R.layout.layout_loading);
mLoadingView = View.inflate(context, loadingLayout, null);
tvLoadingText =(TextView)mLoadingView.findViewById(R.id.tvLoadingText);
addView(mLoadingView,params);
//错误时的布局
int errorLayout = ta.getResourceId(R.styleable.EmptyLayout_elErrorLayout, R.layout.layout_error);
mErrorView = View.inflate(context, errorLayout, null);
mBtnReset =(Button)mErrorView.findViewById(R.id.btnReset);
addView(mErrorView, params);
//全部隐藏
setGone();
}
/**
* 全部隐藏
*/
private void setGone() {
mEmptyView.setVisibility(View.GONE);
mErrorView.setVisibility(View.GONE);
mLoadingView.setVisibility(View.GONE);
}
}

简单说下几个变量的作用
mEmptyView 表示数据为空的时候展示给用户
mEmptyText 数据为空提示的文字
mErrorView 加载错误展示给用户
mBtnReset 加载错误重新加载的按钮
mLoadingView 加载中展示给用户
tvLoadingText 加载中提示的文字
mBindView 我们要绑定的view

至此还需要重写一个方法,

1
2
3
4
@Override
public boolean canScrollVertically(int direction) {
return mBindView.getVisibility()==VISIBLE?mBindView.canScrollVertically(direction):super.canScrollVertically(direction);
}

如果加载成功,过度滑动会调用mBindViewcanScrollVertically,没有则调用父类的canScrollVertically

如果没有重写,添加SwipeRefreshLayout下拉刷新会报错,

在此谢谢丁大哥提出来的bug

好了,首先我们找到布局,然后添加进去,如果没有,添加默认的布局。至此,布局已经完成,那怎么控制呢?我们想要的是什么效果呢?

在数据正在加载的时候调用loading方法,显示正在加载中的文本。
在数据加载成,隐藏该view。
在数据加载失败,显示加载失败的文本,并提供一个按钮去刷新数据。

ok,我们按照这个条目一个个的来实现,首先是loading。

1
2
3
4
5
6
7
8
9
10
11
public void showLoading(String text) {
if (mBindView != null) mBindView.setVisibility(View.GONE);
if (!TextUtils.isEmpty(text)) tvLoadingText.setText(text);
setGone();
mLoadingView.setVisibility(View.VISIBLE);
}
public void showLoading() {
showLoading(null);
}

首先判断下我们要绑定view是不是为空,不为空则隐藏它,隐藏其他布局,然后展示loadingview

那加载失败了呢?同样简单!

1
2
3
4
5
6
7
8
9
10
public void showError() {
showError(null);
}
public void showError(String text) {
if (mBindView != null) mBindView.setVisibility(View.GONE);
if (!TextUtils.isEmpty(text)) mBtnReset.setText(text);
setGone();
mErrorView.setVisibility(View.VISIBLE);
}

这个同上

继续看看加载成功的方法,这个更简单啦。

1
2
3
4
5
public void showSuccess() {
if (mBindView != null) mBindView.setVisibility(View.VISIBLE);
setGone();
}

至此,我们整个效果就完成了,在加载数据的时候调用showLoading方法来显示加载中的文本,加载失败后,调用showError来显示加载失败的文本和刷新的按钮,在加载成功后直接隐藏控件

控件倒是完成了,我们还不知道mBindView怎么来的,其实也很简单。我们在代码中需要调用bindView(View view)方法来指定。

1
2
3
4
public void bindView(View view) {
mBindView = view;
}

那么问题来了,我加载失败后,按钮的点击事件怎么做呢?有人会说用反射,这样既省了代码行数,看着又舒服,但是这样是有个问题存在的,大家都知道,一个项目的上线,都会进行混淆代码的,为了就是防止他人剽窃我们的劳动成果,可是混淆过后哪些class全部变成a,b,c ,这样如果用反射的话就会导致点击事件失效,因为找不到这个类,所以,我们还是老老实实的用onclick事件吧

1
2
3
4
public void setOnButtonClick(OnClickListener listener) {
mBtnReset.setOnClickListener(listener);
}

这样,一个简单的EmptyLayout就诞生了,接下来我们来看看怎么使用

先看xml布局

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
27
28
29
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="me.weyye.emptylayout.MainActivity">
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/srl"
android:layout_width="match_parent"
android:layout_height="match_parent">
<me.weyye.library.EmptyLayout xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/emptyLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:elEmptyLayout="@layout/layout_empty"
app:elErrorLayout="@layout/layout_error"
app:elLoadingLayout="@layout/layout_loading">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"></android.support.v7.widget.RecyclerView>
</me.weyye.library.EmptyLayout>
</android.support.v4.widget.SwipeRefreshLayout>
</RelativeLayout>

在看看Activity中怎么调用

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public class MainActivity extends Activity {
private EmptyLayout emptyLayout;
private RecyclerView recyclerView;
private List<String> list = new ArrayList<>();
private MyAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
loadData();
}
private Handler mHandler = new Handler();
private void initView() {
emptyLayout = (EmptyLayout) findViewById(R.id.emptyLayout);
recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
srl = (SwipeRefreshLayout) findViewById(R.id.srl);
recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
recyclerView.setAdapter(adapter = new MyAdapter(list));
//绑定
emptyLayout.bindView(recyclerView);
emptyLayout.setOnButtonClick(new View.OnClickListener() {
@Override
public void onClick(View v) {
//重新加载数据
loadData();
}
});
srl.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
srl.setRefreshing(false);
loadData();
}
});
}
private void loadData() {
//模拟加载数据
emptyLayout.showLoading("正在加载,请稍后");
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
//为了防止重复调用
mHandler.removeCallbacks(this);
Random r = new Random();
int res = r.nextInt(10);
if (res % 2 == 0) {
// 失败
emptyLayout.showError("加载失败,点击重新加载"); // 显示失败
} else {
// 成功
emptyLayout.showSuccess();
for (int i = 0; i < 15; i++) {
list.add("测试" + i);
}
adapter.notifyDataSetChanged();
}
}
}, 3000);
}
}

首页我们找到控件,然后给recyclerView设置adapter,然后我们调用emptyLayout.bindView(recyclerView);来设置要绑定的view,当然这里是recyclerView,接下来,通过emptyLayout.setOnButtonClick()来设置重新加载的时候执行哪个方法,在loadData()中延迟3秒获取数据,数据成功失败都是随机的,当失败的时候会调用emptyLayout.showError(),成功就调用emptyLayout.showSuccess();就这么简单,来看看运行效果

效果展示

Github:https://github.com/yewei02538/EmptyLayout