Android打造不一样的EmptyView
source link: http://www.androidchina.net/3182.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
大家都对ListView非常熟悉,目测也会经常使用ListView的一个方法setEmptyView
,来设置当数据加载中或者数据加载失败的一个提醒的效果,这个方法虽然使用起来简单,但是如果你提供一个复杂的布局,例如:
在数据加载失败后,添加一个
Button
让用户可以选择重新加载数据。
那么,你可能会这么做,find这个button,然后给button设置点击事件,好吧。。。一个两个的还可以忍受,那多了呢?比如我遇到的这个情况,在测试阶段,老板让加一个刷新的功能,要是按照这种方法,估计现在现在我还在加班(2015/7/27 23:00),那有没有一种更加方便的方式,几行代码就可以搞定?而且不需要写那些烦人的setOnClickListener
?能不能提供一个不仅仅局限于ListView
的EmptyView
,因为我不仅仅在ListView
上使用。
答案是肯定的,这篇博客,我们就去实现一个这样的组件,在实现之间,我们来看看ListView
和他的EmptyView是怎么一个关系,首先定位到ListView.setEmptyView
方法:
@android
.view.RemotableViewMethod
public
void
setEmptyView(View emptyView) {
mEmptyView = emptyView;
// If not explicitly specified this view is important for accessibility.
if
(emptyView !=
null
&& emptyView.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
emptyView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
}
final
T adapter = getAdapter();
final
boolean
empty = ((adapter ==
null
) || adapter.isEmpty());
updateEmptyStatus(empty);
}
继续跟进代码updateEmptyStatus
private
void
updateEmptyStatus(
boolean
empty) {
if
(isInFilterMode()) {
empty =
false
;
}
if
(empty) {
if
(mEmptyView !=
null
) {
mEmptyView.setVisibility(View.VISIBLE);
setVisibility(View.GONE);
}
else
{
// If the caller just removed our empty view, make sure the list view is visible
setVisibility(View.VISIBLE);
}
// We are now GONE, so pending layouts will not be dispatched.
// Force one here to make sure that the state of the list matches
// the state of the adapter.
if
(mDataChanged) {
this
.onLayout(
false
, mLeft, mTop, mRight, mBottom);
}
}
else
{
if
(mEmptyView !=
null
) mEmptyView.setVisibility(View.GONE);
setVisibility(View.VISIBLE);
}
}
唉,原来也没啥,看代码31~37行,就是根据数据是否为空,来控制显示mEmptyView和ListView本身。
既然原理简单,那么我们完全可以自己实现一个。但是,我们的原理正好和ListView的这个相反:
ListView是通过绑定一个emptyView实现的
而我们,是通过EmptyView绑定ListView(其他view也ok)实现的。
我们的EmptyView提供一个通用的方式,加载中时提醒加载中,加载失败提醒加载失败,并提供一个Button供用户刷新使用。
分析完了,接下来就是编码了,首先我们继承一个RelativeLayout
来实现这么一个布局:
public
class
EmptyView
extends
RelativeLayout {
private
String mText;
private
String mLoadingText;
private
TextView mTextView;
private
Button mButton;
private
View mBindView;
...
}
简单说一下4个变量的作用。mText
表示数据为空时提醒的文本。mLoadingText
表示加载中提醒的文本。mTextView
显示提醒文本。mButton
提供给用户刷新的按钮。mBindView
我们要绑定的view。
ok,继续代码:
public
class
EmptyView
extends
RelativeLayout {
...
public
EmptyView(Context context, AttributeSet attrs) {
super
(context, attrs);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.EmptyView,
0
,
0
);
String text = ta.getString(R.styleable.EmptyView_android_text);
String buttonText = ta.getString(R.styleable.EmptyView_buttonText);
mLoadingText = ta.getString(R.styleable.EmptyView_loadingText);
ta.recycle();
init(text, buttonText);
}
...
}
为了灵活性,这些文本内容我们定义成可以在xml
中配置使用,哎?怎么还有一个buttonText,这个当然是按钮上的文字了。
继续代码,可以看到调用了init
方法。
来看看:
public
class
EmptyView
extends
RelativeLayout {
...
private
void
init(String text, String buttonText) {
if
(TextUtils.isEmpty(text)) text =
"暂无数据"
;
if
(TextUtils.isEmpty(buttonText)) buttonText =
"重试"
;
if
(TextUtils.isEmpty(mLoadingText)) mLoadingText =
"加载中..."
;
mText = text;
mTextView =
new
TextView(getContext());
mTextView.setText(text);
LayoutParams textViewParams =
new
LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
textViewParams.addRule(RelativeLayout.CENTER_IN_PARENT);
mTextView.setId(R.id.id_empty_text);
addView(mTextView, textViewParams);
mButton =
new
Button(getContext());
mButton.setText(buttonText);
LayoutParams buttonParams =
new
LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
buttonParams.addRule(RelativeLayout.CENTER_HORIZONTAL);
buttonParams.addRule(RelativeLayout.BELOW, R.id.id_empty_text);
addView(mButton, buttonParams);
}
...
}
在init
方法中,上来,我们去判断这些文本是否为空,如果为空,提供默认的文本。接下来new了一个TextView
和Button
并添加到该控件中,TextView
和Button
是上下排列的。至此,布局已经完成了,那怎么控制呢?我们想要的是什么效果呢?
在数据加载的时候调用
loading
方法,显示正在加载中的文本。
在数据加载成,隐藏该view。
在数据加载失败,显示加载失败的文本,并提供一个按钮去刷新数据。
ok,我们按照这个条目一个个的来实现,首先是loading
。
public
class
EmptyView
extends
RelativeLayout {
...
public
void
loading() {
if
(mBindView !=
null
) mBindView.setVisibility(View.GONE);
setVisibility(View.VISIBLE);
mButton.setVisibility(View.INVISIBLE);
mTextView.setText(mLoadingText);
}
...
}
loading
方法很简单,首先判断mBindView
是否为空,不为空则隐藏它,然后让该控件可见,继续让Button
不可见,因为在加载中的时候,我们不允许点击的发生。最后就是让TextView
显示正在加载中的文本。
继续看看加载成功的方法,这个更简单啦。
public
class
EmptyView
extends
RelativeLayout {
...
public
void
success() {
setVisibility(View.GONE);
if
(mBindView !=
null
) mBindView.setVisibility(View.VISIBLE);
}
...
}
只有两行代码,就是让该控件隐藏,让绑定的view显示。
那么加载失败呢?同样简单!
public
class
EmptyView
extends
RelativeLayout {
...
public
void
empty() {
if
(mBindView !=
null
) mBindView.setVisibility(View.GONE);
setVisibility(View.VISIBLE);
mButton.setVisibility(View.VISIBLE);
mTextView.setText(mText);
}
...
}
不多说了,唯一注意的就是我们让Button
显示了。
至此,我们整个效果就完成了,在加载数据的时候调用loading
方法来显示加载中的文本,加载失败后,调用empty
来显示加载失败的文本和刷新的按钮,在加载成功后直接隐藏控件!
控件倒是完成了,我们还不知道mBindView
怎么来的,其实也很简单。我们在代码中需要调用bindView(View view)
方法来指定。
public
class
EmptyView
extends
RelativeLayout {
...
public
void
bindView(View view) {
mBindView = view;
}
...
}
哈哈,剩下最后一个问题了,按钮的点击事件怎么做?难道要在使用的时候添加onClick
事件?哎,那样太麻烦了,要知道,我有很多文件要改的,我希望一行代码就可以搞定!
亮点来了:
public
class
EmptyView
extends
RelativeLayout {
...
public
void
buttonClick(
final
Object base,
final
String method,
final
Object... parameters) {
mButton.setOnClickListener(
new
OnClickListener() {
public
void
onClick(View v) {
int
length = parameters.length;
Class<?>[] paramsTypes =
new
Class<?>[length];
for
(
int
i =
0
; i < length; i++) {
paramsTypes[i] = parameters[i].getClass();
}
try
{
Method m = base.getClass().getDeclaredMethod(method, paramsTypes);
m.setAccessible(
true
);
m.invoke(base, parameters);
}
catch
(Exception e) {
e.printStackTrace();
}
}
});
}
...
}
利用反射去做,我们只需要指定调用哪个对象上的哪个方法,需要参数的话就传入参数即可。
这段代码简单说一下,首先我们给button设置了一个点击事件,在事件响应的时候,首先遍历参数,获取参数的类型。然后根据方法名反射出方法,最后直接invoke
去执行。这样我们使用起来就非常方便了,完成了我们一行代码搞定
的目标。
好激动,来测试一下吧:
先看xml
布局。
android:layout_width
=
"match_parent"
android:layout_height
=
"match_parent"
tools:context
=
".MainActivity"
>
<
loader.org.emptyview.EmptyView
android:id
=
"@+id/empty_view"
android:layout_width
=
"match_parent"
android:layout_height
=
"match_parent"
/>
<
TextView
android:id
=
"@+id/name"
android:layout_width
=
"match_parent"
android:layout_height
=
"wrap_content"
/>
</
RelativeLayout
>
我们没有使用ListView
,而是使用了一个TextView
,再来看看在Activity
中怎么调用:
public
class
MainActivity
extends
AppCompatActivity {
private
EmptyView mEmptyView;
private
TextView mTextView;
@Override
protected
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mEmptyView = (EmptyView) findViewById(R.id.empty_view);
mTextView = (TextView) findViewById(R.id.name);
mEmptyView.bindView(mTextView);
// 设置bindView
mEmptyView.buttonClick(
this
,
"loadData"
);
// 当button点击时调用哪个方法
loadData();
}
/**
* 加载数据
*/
private
void
loadData() {
mEmptyView.loading();
// 加载中
// 2s后出结果
new
Handler().postDelayed(
new
Runnable() {
@Override
public
void
run() {
Random r =
new
Random();
int
res = r.nextInt(
2
);
// 失败
if
(res ==
0
) {
mEmptyView.empty();
// 显示失败
}
else
{
// 成功
mEmptyView.success();
mTextView.setText(
"success"
);
}
}
},
2000
);
}
}
首先,我们通过mEmptyView.bindView(mTextView)
来设置要绑定的view
,这里当然是TextView
了。
接下来,通过mEmptyView.buttonClick(this, "loadData")
设置按钮点击后执行哪个方法,这里是当前对象上的loadData
方法,并且没有参数。
在getData
中模拟延迟2s后获取数据,数据的成功失败是随机的,当失败了,调用empty
方法,成功后调用success
方法。
哈哈,就是这么简单,来看看代码的效果:
ok~ok~,非常完美。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK