美文网首页
ListView的HeaderView、FooterView和E

ListView的HeaderView、FooterView和E

作者: 与猎犬赛跑的野兔 | 来源:发表于2018-01-29 16:06 被阅读0次

    前言:顾名思义HeaderView、FooterView就是显示在 ListView 的头部跟尾部的一个或多个 View(/ ViewGroup),而当 ListView为空的时候,显示的是EmptyView。但是,HeaderView和 FooterView又是怎么加进 ListView里面去的呢?用过 BaseAdapter应该会很容易理解,就是加一个外包的adapter就行了。但是进一步的问题是,如果 HeaderView和 FooterView算是外包 adapter里面的一个 item,那么在判断 ListView是否为空的时候算不算进去呢?这将会影响到 EmptyView的显示。还有,EmptyView是不是也是外包 adapter里面的一个item呢?

    一、结论

    不多BB直接上结论,有兴趣可以往看下面的源码分析。 适用 Android-26

    1. HeaderView 、FooterView 和 EmptyView 都只是把 View 的引用或者相关数据存在 ListView 的对象里面,等需要它们展示的时候直接显示出来。
    2. 但是,对于 headerView 和 footerView 来说,当设置它们之后,ListView 就会把原来的 adapter 用 HeaderViewListAdapter 包装起来,然后 headerView 和 footerView 也会进入ListView(adapterView) 的回收重复利用的机制,即 Adapter#getView。所以,它们也算是 ListView 的某一个item,这也是为什么它们的 parent 要设置为相应的 ListView,params 也要是 absListView#params。
    3. 而 emptyView 就只是单纯的把引用存进去,当 ListView 为空的时候,ListView 的观察者 AdapterDataSetObserver 就会去刷新状态,当 ListView 为空且 emptyView 不为空的时候, 就将 ListView 设为不可见(包括 headerView 和footerView )。
    4. 特别的对于 emptyView 有两点,第3个结论里面说的 ListView 为空,是指 adapter 里面的数据,需要强调这不是指包装之后的
      HeaderViewListAdapter (因为 headerView、footerView 的引用也在里面,它的getCount 也是把 headerView、footerView算进去的,因为要保证回收机制),也就是说判断 ListView 是否为空不会计算 headerView 和 footerView ,它判断的依据是 adapter.isEmpty() 或 HeaderViewListAdapter.isEmpty() (外包之后用它,但是它的 isEmpty 做了保护),而它们两个都不会计算 headerView 和 footerView 是否存在。
    5. 但是当 emptyView = null 时,isEmpty = true;此时 ListView 不会设置为不可见,因为 ListView 的观察者调用的刷新方法里加了一层判断,只有当 emptyView!= null 时才会设置 ListView 不可见,也就是说此时,我们原先设置的adapter 没有数据了,但是ListView 还是可见的,意味着如果此时有 headerView 和 footerView ,那它们还是可见。

    二、EmptyView

    ListView#setEmptyView 不是自己的方法 它是使用了 AdapterView#setEmptyView 。
    直接看代码:

    ListView extends AbsListView
    class AbsListView extends AdapterView<ListAdapter>
    class AdapterView<T extends Adapter> extends ViewGroup{
      /**
         * View to show if there are no items to show.
         */
        private View mEmptyView;
    
        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);
        }
    
      /**
         * 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);     // 将 emptyView 置为可见
                    setVisibility(View.GONE);                   // 将 ListView 设置不可见
                } else {
                    // If the caller just removed our empty view, make sure the list view is visible
                    setVisibility(View.VISIBLE);                // 当内容为空时,即使没有emptyView也会将ListView置为不可见
                }
    
                // 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);
            }
        }
    
       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 : NOT_FOCUSABLE);
            if (mEmptyView != null) {
                updateEmptyStatus((adapter == null) || adapter.isEmpty());
            }
        }
    }
    
    updateEmptyStatus() 只用以下两个地方用到
    1. public void setEmptyView(View emptyView) 
    2. checkFocus()
    
    checkFocus() 在以下情况会用到
    1. ListView#setAdapter
    2. AdapterView#AdapterDataSetObserver#onInvalidted
    3. AdapterView#AdapterDataSetObserve@onchange
    即只要数据修改就会 调一次checkFocus,但是如果 mEmptyView = null,则不更新
    

    然后总结划个重点:

    1. ListView 里面的 mEmptyView 只是一个引用,它并不是放在 ListView 里面,它跟 ListView 里面的内容没有任何关系。ListView只是存在机制去影响其可见度。
    2. 当 ListView 的数据变化的时候,就回去刷新一遍,当ListView 的内容为空且存在 emptyView 的时候,ListView 设为不可见,emptyView 就设为可见。
    3. 特别的,当未设置 emptyView 或 emptyView = null 的时候,此时若 ListView 的内容为空,ListView 不会设置为不可见。这个时候,如果 ListView 存在 headerView 和 footerView 也会可见。请参见上述源码 updateEmptyStatus() 里面的内容。

    三、HeaderView、FooterView

    FooterView 跟 HeaderView相同,所以一起说了

    ListView 源码里面一共有两个方法加入HeaderView:

     /**
         * Add a fixed view to appear at the top of the list. If addHeaderView is
         * called more than once, the views will appear in the order they were
         * added. Views added using this call can take focus if they want.
         * <p>
         * Note: When first introduced, this method could only be called before
         * setting the adapter with {@link #setAdapter(ListAdapter)}. Starting with
         * {@link android.os.Build.VERSION_CODES#KITKAT}, this method may be
         * called at any time. If the ListView's adapter does not extend
         * {@link HeaderViewListAdapter}, it will be wrapped with a supporting
         * instance of {@link WrapperListAdapter}.
         *
         * @param v The view to add.
         */
    public void addHeaderView(View v) {
            addHeaderView(v, null, true);
        }
    /**
         * 一样 
         * @param v The view to add.
         * @param data Data to associate with this view
         * @param isSelectable whether the item is selectable
         */
    public void addHeaderView(View v, Object data, boolean isSelectable){
       // 稍后分析
    }
    // addFooterView 一样
    

    主要先看注释,有三点:

    1. addHeaderView(param...) 可以多次调用,相当于添加多个headerView,比如调用 addHeaderView( View1 );addHeaderView(View2) 的话,ListView 最开始是 View1 -> View2 -> ListView 我们加入的数据 -> FooterView1 -> FooterView2。
    2. android.os.Build.VERSION_CODES#KITKAT(API 19)之前的版本,listView#setAdapter 必须在 addHeaderView/ addFooterView之后,否者就会引发冲突。注释里面也讲清楚了,API 19 及之后的版本就没有这个限制了,会把setAdapter() 中的 adapter用一个WrapperListAdapter来包装一遍。WrapperListAdapter是一个非常普通的包装 adapter,简单到只有一个方法(ListView 中用的是HeaderViewListAdapter(extends WrapperListAdapter)附录):
      public interface WrapperListAdapter extends ListAdapter {
      /**
       * Returns the adapter wrapped by this list adapter.
       * 返回被包装的adapter
       *
       * @return The {@link android.widget.ListAdapter} wrapped by this adapter.
       */
      public ListAdapter getWrappedAdapter();
      }
      
    3. 设置一个 HeaderView / FooterView 最多需要三个参数,v,data,isSelectable,它们的作用分别是,view对象;data是关于view的数据,使用 HeaderViewListAdapter#getItem() 返回。isSelectable 即说明 View 是否可以被选中。它们也被封装好在 ListView.class 中。
       // ListView.class
       /**
        * A class that represents a fixed view in a list, for example a header at the top
        * or a footer at the bottom.
        */
       public class FixedViewInfo {
           /** The view to add to the list */
           public View view;
           /** The data backing the view. This is returned from {@link ListAdapter#getItem(int)}. */
           public Object data;
           /** <code>true</code> if the fixed view should be selectable in the list */
           public boolean isSelectable;
       }
      
       // ListView.class
       ArrayList<FixedViewInfo> mHeaderViewInfos = Lists.newArrayList();
       ArrayList<FixedViewInfo> mFooterViewInfos = Lists.newArrayList();
      
       // HeaderViewListAdapter.class  完整代码见附录
       // These two ArrayList are assumed to NOT be null.
       // They are indeed created when declared in ListView and then shared.
       ArrayList<ListView.FixedViewInfo> mHeaderViewInfos;
       ArrayList<ListView.FixedViewInfo> mFooterViewInfos;
      
      具体这几个变量有什么用,稍后接着看。

    我们先看看addHeaderView()发生了什么。

    public void addHeaderView(View v, Object data, boolean isSelectable) {
            if (v.getParent() != null && v.getParent() != this) {
                if (Log.isLoggable(TAG, Log.WARN)) {
                    Log.w(TAG, "The specified child already has a parent. "
                               + "You must call removeView() on the child's parent first.");
                }
            }
            final FixedViewInfo info = new FixedViewInfo();
            info.view = v;
            info.data = data;
            info.isSelectable = isSelectable;
            mHeaderViewInfos.add(info);
            mAreAllItemsSelectable &= isSelectable;
    
            // Wrap the adapter if it wasn't already wrapped.
            if (mAdapter != null) {
                if (!(mAdapter instanceof HeaderViewListAdapter)) {
                    wrapHeaderListAdapterInternal();
                }
    
                // In the case of re-adding a header view, or adding one later on,
                // we need to notify the observer.
                if (mDataSetObserver != null) {
                    mDataSetObserver.onChanged();
                }
            }
        }
    

    在这里面干了四件事:

    1. 往 ListView#mHeaderViewInfos 里面塞数据。
    2. ListView#mAreAllItemsSelectable 顾名思义是判断全部是否都可选,目前没用过。
    3. 将 adapter 加一件“外套”。
    4. 通知 Observer 数据变化了。

    我们看一下“披上外套”的方法 wrapHeaderListAdapterInternal(); 里面具体怎么干的:

     /** @hide */
        protected void wrapHeaderListAdapterInternal() {
            mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, mAdapter);
        }
    
     /** @hide */
        protected HeaderViewListAdapter wrapHeaderListAdapterInternal(
                ArrayList<ListView.FixedViewInfo> headerViewInfos,
                ArrayList<ListView.FixedViewInfo> footerViewInfos,
                ListAdapter adapter) {
            return new HeaderViewListAdapter(headerViewInfos, footerViewInfos, adapter);
        }
    
    

    也就是说在ListView#addHeaderView(...)之后,ListView 的 mAdapter 就变成了 HeaderViewListAdapter,请注意 mAdapter 是写在AbsListView 里面的,所以你打开源码是找不到mAdapter的,它们之间的关系有点绕,下面用代码简单说一下:

    ListView extends AbsListView{
        public ListAdapter getAdapter() { return mAdapter;}
    }
    class AbsListView extends AdapterView<ListAdapter>{
        ListAdapter mAdapter // 即mAdapter 包可见,但是我们在外部可以用 ListView#getAdapter 或 AbsListView#getAdapter获取
    }
    
    class AdapterView{
      public abstract T getAdapter();
    }
    HeaderViewListAdapter implements WrapperListAdapter
    interface WrapperListAdapter extends ListAdapter
    interface ListAdapter extends Adapter
    interface Adapter
    

    然后,在ListView里面共三个地方用到了 void wrapHeaderListAdapterInternal():

     public void setAdapter(ListAdapter adapter) {
         //...
         if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
                mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, adapter);
            } else {
                mAdapter = adapter;
            }
          //...
    }
    public void addHeaderView(View v, Object data, boolean isSelectable){ // ...}
    public void addFooterView(View v, Object data, boolean isSelectable){ //...}
    
    

    注:这是android-26的源码了,所以说已经解决了之前 setAdapter 和 addHeaderView / addFooterView 的问题,也就是当 ListView 里面存在 headerView 或 footerView 之后就会给它包一个外包。

    好了,说了那么多,再看看HeaderViewListAdapter,headerViews 和 footerViews 充当什么角色。

    public class HeaderViewListAdapter implements WrapperListAdapter, Filterable {
      private final boolean mIsFilterable;  // 控制 getFilter() 的,不清楚可以看一下  Filterable  源码 和 
                                            // 用例 https://gist.github.com/DeepakRattan/26521c404ffd7071d0a4
      private final ListAdapter mAdapter;   // 原先的 adapter
      static final ArrayList<ListView.FixedViewInfo> EMPTY_INFO_LIST =
            new ArrayList<ListView.FixedViewInfo>();    // 空的list 应该是为了预防 NPE
      boolean mAreAllFixedViewsSelectable;    //应该和外面ListView的mAreAllItemsSelectable 一样吧
     
      public HeaderViewListAdapter(ArrayList<ListView.FixedViewInfo> headerViewInfos,
                                     ArrayList<ListView.FixedViewInfo> footerViewInfos,
                                     ListAdapter adapter) {
            mAdapter = adapter;
            mIsFilterable = adapter instanceof Filterable;
    
            if (headerViewInfos == null) {
                mHeaderViewInfos = EMPTY_INFO_LIST;
            } else {
                mHeaderViewInfos = headerViewInfos;
            }
    
            if (footerViewInfos == null) {
                mFooterViewInfos = EMPTY_INFO_LIST;
            } else {
                mFooterViewInfos = footerViewInfos;
            }
    
            mAreAllFixedViewsSelectable =
                    areAllListInfosSelectable(mHeaderViewInfos)
                    && areAllListInfosSelectable(mFooterViewInfos);
        }
    
      public int getCount() {     // 可以看出这里获取数量的时候是把 headerView 和 emptyView 算进去的
            if (mAdapter != null) {
                return getFootersCount() + getHeadersCount() + mAdapter.getCount();
            } else {
                return getFootersCount() + getHeadersCount();
            }
        }
    
      public Object getItem(int position) {   // 这里就是返回对应的数据,header / footer 返回的就是我们设置data
            // Header (negative positions will throw an IndexOutOfBoundsException)
            int numHeaders = getHeadersCount();
            if (position < numHeaders) {
                return mHeaderViewInfos.get(position).data;
            }
    
            // Adapter
            final int adjPosition = position - numHeaders;
            int adapterCount = 0;
            if (mAdapter != null) {
                adapterCount = mAdapter.getCount();
                if (adjPosition < adapterCount) {
                    return mAdapter.getItem(adjPosition);
                }
            }
    
            // Footer (off-limits positions will throw an IndexOutOfBoundsException)
            return mFooterViewInfos.get(adjPosition - adapterCount).data;
        }
    
      public View getView(int position, View convertView, ViewGroup parent) {
            // Header (negative positions will throw an IndexOutOfBoundsException)
            // 这里可以看到 header 和 footer 直接把 View 拿出来展示了,data只是储存一些备份的数据,不能在这里动态变化的,当然,它会被回收
            int numHeaders = getHeadersCount();
            if (position < numHeaders) {
                return mHeaderViewInfos.get(position).view;
            }
    
            // Adapter
            final int adjPosition = position - numHeaders;
            int adapterCount = 0;
            if (mAdapter != null) {
                adapterCount = mAdapter.getCount();
                if (adjPosition < adapterCount) {
                    return mAdapter.getView(adjPosition, convertView, parent);
                }
            }
    
            // Footer (off-limits positions will throw an IndexOutOfBoundsException)
            return mFooterViewInfos.get(adjPosition - adapterCount).view;
        }
      
      public int getItemViewType(int position) {
            int numHeaders = getHeadersCount();
            if (mAdapter != null && position >= numHeaders) {
                int adjPosition = position - numHeaders;
                int adapterCount = mAdapter.getCount();
                if (adjPosition < adapterCount) {
                    return mAdapter.getItemViewType(adjPosition);
                }
            }
    
            return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
        }
      
      public int getViewTypeCount() {  
            if (mAdapter != null) {
                return mAdapter.getViewTypeCount();   
                // 这里我本来觉得应该+1的,但是后来想了想,getViewTypeCount()
                // 一般是给开发者配合 getView 使用的,那么如果把 AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER; 
                // 也给算进去的话,可能有些开发者设计的 adapter 没有处理headerView 反而麻烦了。
            }
            return 1;
        }
    
      public boolean isEmpty() { 
            // 这个是配合EmptyView 的 从这里可以看出这里计算是否为空不会把 headerView 、footerView 算进去
            return mAdapter == null || mAdapter.isEmpty();
        }
    }
    

    相关文章

      网友评论

          本文标题:ListView的HeaderView、FooterView和E

          本文链接:https://www.haomeiwen.com/subject/eqvlaxtx.html