Android ListView.setEmptyView

来源:互联网 发布:淘宝好的第三方活动 编辑:程序博客网 时间:2024/06/05 20:46

概述

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()的原理。

时间点

  1. ListView初始化
    当列表初始化的时候,将EmptyView创建出来,添加进ListView中,该时间点会有一个不好的用户体验,就是在给ListView添加数据前,或者网络请求数据未返回前,ListView会隐藏,显示EmptyView,当数据到达并更新ListView后,EmptyView会隐藏,ListView显示,会有一种闪现的效果。不建议在初始化的时间点添加EmptyView.
  2. 设置Adapter
    该时间点设置EmptyView在部分时候会有相同的显示效果,当Adapter为空就关联ListView会和ListView初始化是一样的,当在数据请求完,设置Adapter时,就可以刚好显示当前的数据情况,这就是第三种时间点。
  3. 监听网络请求返回数据
    在获取数据之后添加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()方法。

总结

  1. SetEmpty原理:即动态更新ListView与EmptyView的Visibility属性。
  2. 使用条件:EmptyView需在ListView的布局层级中。
  3. 注意事项:在使用代码添加EmptyView的时候,需要注意不可以循环添加EmptyView,因为EmptyView会被添加进布局层级中,ListView只是持有一个引用。

纯属个人学习总结,有不正确的地方,肯定高人指正,谢谢!

0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 产后50天小肚子突出怎么办 肚子像怀孕一样大怎么办 蹲厕所拉不出来怎么办 生气导致回奶了怎么办 老公每晚要吃奶才睡觉怎么办 分分钟想把老公杀掉怎么办 老公出轨闹的厉害离家出走怎么办? 儿子故意杀人一审判死刑怎么办 被家暴时妻子杀了丈夫该怎么办 丈夫挣钱不给妻子怎么办 白色皮鞋染了色怎么办 刺扎到手里拿不出来怎么办 军官证解锁片丢了怎么办 六安市人民医院药品停用了怎么办 信无法寄到该怎么办 5个月的宝宝光有屎沫怎么办 胸牌的别针坏了怎么办 工资表税金扣多了怎么办? 装修公司不发放工程怎么办 公司不给开收入证明怎么办 装修公司不付工人工资怎么办 收入证明少500元怎么办 dnf二级输错了怎么办 如果受到法律的伤害怎么办 86岁了还怕死怎么办 风衣的腰带丢了怎么办 成为伪娘身上的毛怎么办 军官升不上去了怎么办 王者荣耀代练封号怎么办 cf淘宝代练封号怎么办 买音乐会的票过期怎么办 十年多年前被怨错拘留了怎么办 与室友关系闹僵怎么办 开庭后被告威胁我们证人怎么办 开车撞了豪车怎么办 我把人撞了全责怎么办 开车撞伤人没钱赔怎么办 开车撞伤无证驾驶人怎么办 开车把人撞伤了只买交强险怎么办 开车撞伤人赔不起怎么办 如果车撞死人了怎么办