前言:顾名思义HeaderView、FooterView就是显示在 ListView 的头部跟尾部的一个或多个 View(/ ViewGroup),而当 ListView为空的时候,显示的是EmptyView。但是,HeaderView和 FooterView又是怎么加进 ListView里面去的呢?用过 BaseAdapter应该会很容易理解,就是加一个外包的adapter就行了。但是进一步的问题是,如果 HeaderView和 FooterView算是外包 adapter里面的一个 item,那么在判断 ListView是否为空的时候算不算进去呢?这将会影响到 EmptyView的显示。还有,EmptyView是不是也是外包 adapter里面的一个item呢?
一、结论
不多BB直接上结论,有兴趣可以往看下面的源码分析。 适用 Android-26
- HeaderView 、FooterView 和 EmptyView 都只是把 View 的引用或者相关数据存在 ListView 的对象里面,等需要它们展示的时候直接显示出来。
- 但是,对于 headerView 和 footerView 来说,当设置它们之后,ListView 就会把原来的 adapter 用 HeaderViewListAdapter 包装起来,然后 headerView 和 footerView 也会进入ListView(adapterView) 的回收重复利用的机制,即 Adapter#getView。所以,它们也算是 ListView 的某一个item,这也是为什么它们的 parent 要设置为相应的 ListView,params 也要是 absListView#params。
- 而 emptyView 就只是单纯的把引用存进去,当 ListView 为空的时候,ListView 的观察者 AdapterDataSetObserver 就会去刷新状态,当 ListView 为空且 emptyView 不为空的时候, 就将 ListView 设为不可见(包括 headerView 和footerView )。
- 特别的对于 emptyView 有两点,第3个结论里面说的 ListView 为空,是指 adapter 里面的数据,需要强调这不是指包装之后的
HeaderViewListAdapter (因为 headerView、footerView 的引用也在里面,它的getCount 也是把 headerView、footerView算进去的,因为要保证回收机制),也就是说判断 ListView 是否为空不会计算 headerView 和 footerView ,它判断的依据是 adapter.isEmpty() 或 HeaderViewListAdapter.isEmpty() (外包之后用它,但是它的 isEmpty 做了保护),而它们两个都不会计算 headerView 和 footerView 是否存在。- 但是当 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
class AdapterView 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,则不更新
然后总结划个重点:
- ListView 里面的 mEmptyView 只是一个引用,它并不是放在 ListView 里面,它跟 ListView 里面的内容没有任何关系。ListView只是存在机制去影响其可见度。
- 当 ListView 的数据变化的时候,就回去刷新一遍,当ListView 的内容为空且存在 emptyView 的时候,ListView 设为不可见,emptyView 就设为可见。
- 特别的,当未设置 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.
*
* 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 一样
主要先看注释,有三点:
- addHeaderView(param...) 可以多次调用,相当于添加多个headerView,比如调用 addHeaderView( View1 );addHeaderView(View2) 的话,ListView 最开始是 View1 -> View2 -> ListView 我们加入的数据 -> FooterView1 -> FooterView2。
- 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(); }
- 设置一个 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; /**
true
if the fixed view should be selectable in the list */ public boolean isSelectable; }// ListView.class ArrayList
mHeaderViewInfos = Lists.newArrayList(); ArrayList 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
mHeaderViewInfos; ArrayList 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();
}
}
}
在这里面干了四件事:
- 往 ListView#mHeaderViewInfos 里面塞数据。
- ListView#mAreAllItemsSelectable 顾名思义是判断全部是否都可选,目前没用过。
- 将 adapter 加一件“外套”。
- 通知 Observer 数据变化了。
我们看一下“披上外套”的方法 wrapHeaderListAdapterInternal(); 里面具体怎么干的:
/** @hide */
protected void wrapHeaderListAdapterInternal() {
mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, mAdapter);
}
/** @hide */
protected HeaderViewListAdapter wrapHeaderListAdapterInternal(
ArrayList headerViewInfos,
ArrayList 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 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 EMPTY_INFO_LIST =
new ArrayList(); // 空的list 应该是为了预防 NPE
boolean mAreAllFixedViewsSelectable; //应该和外面ListView的mAreAllItemsSelectable 一样吧
public HeaderViewListAdapter(ArrayList headerViewInfos,
ArrayList 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();
}
}