Android ListView.setEmptyView
2016-05-22 21:26
477 查看
概述
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只是持有一个引用。
纯属个人学习总结,有不正确的地方,肯定高人指正,谢谢!
相关文章推荐
- 使用C++实现JNI接口需要注意的事项
- Android IPC进程间通讯机制
- Android Manifest 用法
- [转载]Activity中ConfigChanges属性的用法
- Android之获取手机上的图片和视频缩略图thumbnails
- Android之使用Http协议实现文件上传功能
- Android学习笔记(二九):嵌入浏览器
- android string.xml文件中的整型和string型代替
- i-jetty环境搭配与编译
- android之定时器AlarmManager
- android wifi 无线调试
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- android 代码实现控件之间的间距
- android FragmentPagerAdapter的“标准”配置
- Android"解决"onTouch和onClick的冲突问题
- android:installLocation简析
- android searchView的关闭事件
- SourceProvider.getJniDirectories