ListView的HeaderView、FooterView和EmptyView(源码)

前言:顾名思义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();
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容