Android ListView.setEmptyView
Posted 漩樱
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android ListView.setEmptyView相关的知识,希望对你有一定的参考价值。
概述
ListView:一个可以垂直滑动的列表视图。
setEmptyView()接口继承至ListView的父类AdapterView。可想而知,ListView为空时,才会显示EmptyView,这与ListView的数据适配器有间接的联系。
使用场景
List使用非常广泛,用于具有相同数据类型的数据模型显示,也可以自定义List以符合实际的需求。
本文主要介绍List.setEmptyView()接口。使用场景为,当客户端当前显示窗口中显示一个ListView,ListView需要通过Adapter将数据关联到列表上显示出来。这就会出现一个场景,就是当未设置Adapter或Adapter里面的数据为空时,如果给用户一个友好的提示,提升用户体验。
android官方源码中已经提供了这样的一个接口,通过这个接口,可以在使用ListView的过程中,利用内在的逻辑帮我们实现这个功能,减少代码,让并且是代码更加的清晰,易懂。
实现方式:
EmptyView的添加包括两种方式,一种是在创建布局的时候将EmptyView直接写进去,这种方式的缺点在于缺乏灵活性。另一种是通过代码将创建EmptyView,这种方式相对布局来添加更加具有灵活性,可以自定义EmptyView,动态的修改。当然即便通过布局的方式添加了EmptyView,也可以再次通过代码添加。
1.布局文件实现EmptyView
<ViewGroup
…
<TextView
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/tip_of_empty"
android:visibility="gone" />
…
</ViewGroup>
2.代码中实现
TextView emptyView = new TextView(context);
emptyView.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
emptyView.setText(R.string.tip_of_empty);
emptyView.setVisibility(View.GONE);
((ViewGroup)list.getParent()).addView(emptyView);
list.setEmptyView(emptyView);
注:添加EmptyView有一个前提条件,所添加的EmptyView必须存在于ListView的父级容器中,或者说同一个视图树中,才能生效,这涉及到List.setEmptyView()的原理。
时间点
- ListView初始化
当列表初始化的时候,将EmptyView创建出来,添加进ListView中,该时间点会有一个不好的用户体验,就是在给ListView添加数据前,或者网络请求数据未返回前,ListView会隐藏,显示EmptyView,当数据到达并更新ListView后,EmptyView会隐藏,ListView显示,会有一种闪现的效果。不建议在初始化的时间点添加EmptyView. - 设置Adapter
该时间点设置EmptyView在部分时候会有相同的显示效果,当Adapter为空就关联ListView会和ListView初始化是一样的,当在数据请求完,设置Adapter时,就可以刚好显示当前的数据情况,这就是第三种时间点。 - 监听网络请求返回数据
在获取数据之后添加EmptyView可以恰当的反应当前的状态,也是最佳的使用方式。
本次将主要介绍其中的一点即ListView的初始化,其他请大家去看源码,最后会发现,原理都是一样的。
实现原理
EmptyView显示的原理如上图所示,当Adapter为null,或Adapter的数据为空时,即ListView没有数据进行显示时,ListVewi会被设置为View.GONE,而EmptyView被设置为View.VISABLE;当数据不为空时,逻辑相反。只有当EmptyView在当前的布局层级中,才能有这样的效果,如上图所示。
ListView初始化:
ListActivity.java中的源码实现:作为官网的例子,可以看出其具体的使用方法与逻辑,包括在何时进行EmptyView的添加,以及EmptyView添加的流程关系。
ListActivity创建的布局层级:ListActivity,顾名思义,该Activity为使用者维护了一个ListView,但当创建ListActivity时,并没有在布局层级中出现。
因此,去查看ListActivity的源码,源码中介绍,在使用ListActivity时,需调用setContentView()/setListAdapter ()进行初始化。
从源码中可以看出,在setListAdapter方法调用了ensureList()方法。
在ensureList()方法中会对ListActivity维护的ListView进行判断,如果为null,会调用setContentView,否则返回继续执行。接着看setContentView。
setContentView()方法调用完后,发现并没有我们想要的结果,也没有对listView进行初始化等等,陷入僵局。但是我们仔细看源码,发现setContentView()中调用了Window. setContentView()。通过各种方式,最后发现,Activity这个类实现了Window.Callback接口,当Activity调用setContentView()后,会回调onContentChanged()方法。
在onContentChanged()中,回去初始化ListView,EmptyView。
通过ListActivity,EmptyView添加的具体流程为:
接下来看ListView.setEmptyView()的实现源码。
源码分析:
/**
* Sets the view to show if the adapter is empty
*/
@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);
}
在setEmptyView中,可以看到两点,即ListView中的mEmptyView对传入的View对象是一个引用关系,第二点就是:对empty的定义,当adapter==null,或者adapter.isEmpty(),然后传入updateEmptyStatus()。
/**
* Update the status of the list based on the empty parameter. If empty is true and
* we have an empty view, display it. In all the other cases, make sure that the listview
* is VISIBLE and that the empty view is GONE (if it's not null).
*/
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);
}
}
updateEmptyStatus()方法的原理就相当于前面绘制的原理图,通过设置View的visibility属性,实现EmptyView的逻辑。然而,setEmpty只是在添加的时候进行一个界面更新,当有数据之后,Adapter必须通知ListView,再去更新当前的visibility属性,所以去看下和Adapter相关的两个数据更新方法。
/**
* Sets the data behind this ListView.
*
* The adapter passed to this method may be wrapped by a {@link WrapperListAdapter},
* depending on the ListView features currently in use. For instance, adding
* headers and/or footers will cause the adapter to be wrapped.
*
* @param adapter The ListAdapter which is responsible for maintaining the
* data backing this list and for producing a view to represent an
* item in that data set.
*
* @see #getAdapter()
*/
@Override
public void setAdapter(ListAdapter adapter) {
if (mAdapter != null && mDataSetObserver != null) {
mAdapter.unregisterDataSetObserver(mDataSetObserver);
}
resetList();
mRecycler.clear();
if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter);
} else {
mAdapter = adapter;
}
mOldSelectedPosition = INVALID_POSITION;
mOldSelectedRowId = INVALID_ROW_ID;
// AbsListView#setAdapter will update choice mode states.
super.setAdapter(adapter);
if (mAdapter != null) {
mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();
mOldItemCount = mItemCount;
mItemCount = mAdapter.getCount();
checkFocus();
mDataSetObserver = new AdapterDataSetObserver();
mAdapter.registerDataSetObserver(mDataSetObserver);
mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
int position;
if (mStackFromBottom) {
position = lookForSelectablePosition(mItemCount - 1, false);
} else {
position = lookForSelectablePosition(0, true);
}
setSelectedPositionInt(position);
setNextSelectedPositionInt(position);
if (mItemCount == 0) {
// Nothing selected
checkSelectionChanged();
}
} else {
mAreAllItemsSelectable = true;
checkFocus();
// Nothing selected
checkSelectionChanged();
}
requestLayout();
}
SetAdapter()方法中,有两点,一个是checkFocus,另一个是为Adapter注册了一个数据观察者,后面源码会介绍到,当adapter数据发送变化时,会回调观察者的onChanged()方法。
checkFocus()源码:
void checkFocus() {
final T adapter = getAdapter();
final boolean empty = adapter == null || adapter.getCount() == 0;
final boolean focusable = !empty || isInFilterMode();
// The order in which we set focusable in touch mode/focusable may matter
// for the client, see View.setFocusableInTouchMode() comments for more
// details
super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState);
super.setFocusable(focusable && mDesiredFocusableState);
if (mEmptyView != null) {
updateEmptyStatus((adapter == null) || adapter.isEmpty());
}
}
可以看出,checkFocus()方法中,调用了updateEmptyStatus(),即在设置数据适配器的时候,会对EmptyView进行更新。
接下来看注册registerDataSetObserver数据观察者源码:
public void registerDataSetObserver(DataSetObserver observer) {
mDataSetObservable.registerObserver(observer);
}
到此,setAdapter()方法的逻辑就结束了,然后setEmptyView()和setAdapter()方法只会在数据初始化的时候调用一次,当数据发送变化的时候,需要手动去更新Adapter调用notifyDataSetChanged()。
notifyDataSetChanged()源码:
/**
* Notifies the attached observers that the underlying data has been changed
* and any View reflecting the data set should refresh itself.
*/
public void notifyDataSetChanged() {
mDataSetObservable.notifyChanged();
}
Adapter调用notifyDataSetChanged()方法,实质上是调用mDataSetObservable. notifyChanged()方法。继续跟踪下去。
/**
* Invokes {@link DataSetObserver#onChanged} on each observer.
* Called when the contents of the data set have changed. The recipient
* will obtain the new contents the next time it queries the data set.
*/
public void notifyChanged() {
synchronized(mObservers) {
// since onChanged() is implemented by the app, it could do anything, including
// removing itself from {@link mObservers} - and that could cause problems if
// an iterator is used on the ArrayList {@link mObservers}.
// to avoid such problems, just march thru the list in the reverse order.
for (int i = mObservers.size() - 1; i >= 0; i--) {
mObservers.get(i).onChanged();
}
}
}
在notifyChanged()方法中,会遍历所有注册的数据观察者,并回调观察者的onChanged()方法,通过源码可以看到,在Adapter源码中,创建了一个内部类AdapterDataSetObserver,并重写了onChanged()方法。
class AdapterDataSetObserver extends DataSetObserver {
private Parcelable mInstanceState = null;
@Override
public void onChanged() {
mDataChanged = true;
mOldItemCount = mItemCount;
mItemCount = getAdapter().getCount();
// Detect the case where a cursor that was previously invalidated has
// been repopulated with new data.
if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
&& mOldItemCount == 0 && mItemCount > 0) {
AdapterView.this.onRestoreInstanceState(mInstanceState);
mInstanceState = null;
} else {
rememberSyncState();
}
checkFocus();
requestLayout();
}
}
onChanged()中有两点,一点是mDataChanged,在updateEmptyStatus()方法中会去判断该变量的状态,当为true时,会更新ListView布局大小,而在onChanged()方法中会将mDataChanged置为true,通知布局更新,另一点是调用了checkFocus()方法,间接调用updateEmptyStatus()进行EmptyView的更新。
至此,EmptyView的设置基本的逻辑已经很清晰了,总结下。EmptyView的更新主要在updateEmptyStatus()中进行,在初始化ListView的Adapter以及数据更新后回调Adapter.notifyDataSetChanged()方法,其实质也是回调notifyDataSetChanged()方法。
总结
- SetEmpty原理:即动态更新ListView与EmptyView的Visibility属性。
- 使用条件:EmptyView需在ListView的布局层级中。
- 注意事项:在使用代码添加EmptyView的时候,需要注意不可以循环添加EmptyView,因为EmptyView会被添加进布局层级中,ListView只是持有一个引用。
纯属个人学习总结,有不正确的地方,肯定高人指正,谢谢!
以上是关于Android ListView.setEmptyView的主要内容,如果未能解决你的问题,请参考以下文章
Android 逆向Android 权限 ( Android 逆向中使用的 android.permission 权限 | Android 系统中的 Linux 用户权限 )