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

时间点

  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只是持有一个引用。

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

以上是关于Android ListView.setEmptyView的主要内容,如果未能解决你的问题,请参考以下文章

Android 逆向Android 权限 ( Android 逆向中使用的 android.permission 权限 | Android 系统中的 Linux 用户权限 )

android 21 是啥版本

Android逆向-Android基础逆向(2-2)

【Android笔记】android Toast

图解Android - Android核心机制

Android游戏开发大全的目录