前言:顧名思義HeaderView泥彤、FooterView就是顯示在 ListView 的頭部跟尾部的一個(gè)或多個(gè) View(/ ViewGroup)衔沼,而當(dāng) ListView為空的時(shí)候蝌借,顯示的是EmptyView。但是指蚁,HeaderView和 FooterView又是怎么加進(jìn) ListView里面去的呢菩佑?用過 BaseAdapter應(yīng)該會(huì)很容易理解,就是加一個(gè)外包的adapter就行了欣舵。但是進(jìn)一步的問題是擎鸠,如果 HeaderView和 FooterView算是外包 adapter里面的一個(gè) item,那么在判斷 ListView是否為空的時(shí)候算不算進(jìn)去呢缘圈?這將會(huì)影響到 EmptyView的顯示劣光。還有袜蚕,EmptyView是不是也是外包 adapter里面的一個(gè)item呢?
一绢涡、結(jié)論
不多BB直接上結(jié)論牲剃,有興趣可以往看下面的源碼分析。 適用 Android-26
- HeaderView 雄可、FooterView 和 EmptyView 都只是把 View 的引用或者相關(guān)數(shù)據(jù)存在 ListView 的對象里面凿傅,等需要它們展示的時(shí)候直接顯示出來。
- 但是数苫,對于 headerView 和 footerView 來說聪舒,當(dāng)設(shè)置它們之后,ListView 就會(huì)把原來的 adapter 用 HeaderViewListAdapter 包裝起來虐急,然后 headerView 和 footerView 也會(huì)進(jìn)入ListView(adapterView) 的回收重復(fù)利用的機(jī)制箱残,即 Adapter#getView。所以止吁,它們也算是 ListView 的某一個(gè)item被辑,這也是為什么它們的 parent 要設(shè)置為相應(yīng)的 ListView,params 也要是 absListView#params敬惦。
- 而 emptyView 就只是單純的把引用存進(jìn)去盼理,當(dāng) ListView 為空的時(shí)候,ListView 的觀察者 AdapterDataSetObserver 就會(huì)去刷新狀態(tài)俄删,當(dāng) ListView 為空且 emptyView 不為空的時(shí)候宏怔, 就將 ListView 設(shè)為不可見(包括 headerView 和footerView )。
- 特別的對于 emptyView 有兩點(diǎn)抗蠢,第3個(gè)結(jié)論里面說的 ListView 為空举哟,是指 adapter 里面的數(shù)據(jù),需要強(qiáng)調(diào)這不是指包裝之后的
HeaderViewListAdapter (因?yàn)?headerView迅矛、footerView 的引用也在里面,它的getCount 也是把 headerView潜叛、footerView算進(jìn)去的秽褒,因?yàn)橐WC回收機(jī)制),也就是說判斷 ListView 是否為空不會(huì)計(jì)算 headerView 和 footerView 威兜,它判斷的依據(jù)是 adapter.isEmpty() 或 HeaderViewListAdapter.isEmpty() (外包之后用它销斟,但是它的 isEmpty 做了保護(hù)),而它們兩個(gè)都不會(huì)計(jì)算 headerView 和 footerView 是否存在椒舵。- 但是當(dāng) emptyView = null 時(shí)蚂踊,isEmpty = true;此時(shí) ListView 不會(huì)設(shè)置為不可見笔宿,因?yàn)?ListView 的觀察者調(diào)用的刷新方法里加了一層判斷犁钟,只有當(dāng) emptyView棱诱!= null 時(shí)才會(huì)設(shè)置 ListView 不可見,也就是說此時(shí)涝动,我們原先設(shè)置的adapter 沒有數(shù)據(jù)了迈勋,但是ListView 還是可見的,意味著如果此時(shí)有 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 設(shè)置不可見
} else {
// If the caller just removed our empty view, make sure the list view is visible
setVisibility(View.VISIBLE); // 當(dāng)內(nèi)容為空時(shí)厦凤,即使沒有emptyView也會(huì)將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() 只用以下兩個(gè)地方用到
1. public void setEmptyView(View emptyView)
2. checkFocus()
checkFocus() 在以下情況會(huì)用到
1. ListView#setAdapter
2. AdapterView#AdapterDataSetObserver#onInvalidted
3. AdapterView#AdapterDataSetObserve@onchange
即只要數(shù)據(jù)修改就會(huì) 調(diào)一次checkFocus,但是如果 mEmptyView = null育苟,則不更新
然后總結(jié)劃個(gè)重點(diǎn):
- ListView 里面的 mEmptyView 只是一個(gè)引用较鼓,它并不是放在 ListView 里面,它跟 ListView 里面的內(nèi)容沒有任何關(guān)系宙搬。ListView只是存在機(jī)制去影響其可見度笨腥。
- 當(dāng) ListView 的數(shù)據(jù)變化的時(shí)候,就回去刷新一遍勇垛,當(dāng)ListView 的內(nèi)容為空且存在 emptyView 的時(shí)候脖母,ListView 設(shè)為不可見,emptyView 就設(shè)為可見闲孤。
- 特別的谆级,當(dāng)未設(shè)置 emptyView 或 emptyView = null 的時(shí)候,此時(shí)若 ListView 的內(nèi)容為空讼积,ListView 不會(huì)設(shè)置為不可見肥照。這個(gè)時(shí)候,如果 ListView 存在 headerView 和 footerView 也會(huì)可見勤众。請參見上述源碼 updateEmptyStatus() 里面的內(nèi)容舆绎。
三、HeaderView们颜、FooterView
FooterView 跟 HeaderView相同吕朵,所以一起說了
ListView 源碼里面一共有兩個(gè)方法加入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 一樣
主要先看注釋,有三點(diǎn):
- addHeaderView(param...) 可以多次調(diào)用窥突,相當(dāng)于添加多個(gè)headerView努溃,比如調(diào)用 addHeaderView( View1 );addHeaderView(View2) 的話阻问,ListView 最開始是 View1 -> View2 -> ListView 我們加入的數(shù)據(jù) -> FooterView1 -> FooterView2梧税。
- android.os.Build.VERSION_CODES#KITKAT(API 19)之前的版本,listView#setAdapter 必須在 addHeaderView/ addFooterView之后,否者就會(huì)引發(fā)沖突第队。注釋里面也講清楚了哮塞,API 19 及之后的版本就沒有這個(gè)限制了,會(huì)把setAdapter() 中的 adapter用一個(gè)WrapperListAdapter來包裝一遍斥铺。WrapperListAdapter是一個(gè)非常普通的包裝 adapter彻桃,簡單到只有一個(gè)方法(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(); }
- 設(shè)置一個(gè) HeaderView / FooterView 最多需要三個(gè)參數(shù),v晾蜘,data邻眷,isSelectable,它們的作用分別是剔交,view對象肆饶;data是關(guān)于view的數(shù)據(jù),使用 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();
具體這幾個(gè)變量有什么用竭鞍,稍后接著看板惑。// 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()發(fā)生了什么。
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 里面塞數(shù)據(jù)偎快。
- ListView#mAreAllItemsSelectable 顧名思義是判斷全部是否都可選冯乘,目前沒用過。
- 將 adapter 加一件“外套”晒夹。
- 通知 Observer 數(shù)據(jù)變化了裆馒。
我們看一下“披上外套”的方法 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的,它們之間的關(guān)系有點(diǎn)繞读跷,下面用代碼簡單說一下:
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里面共三個(gè)地方用到了 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的源碼了效览,所以說已經(jīng)解決了之前 setAdapter 和 addHeaderView / addFooterView 的問題些膨,也就是當(dāng) ListView 里面存在 headerView 或 footerView 之后就會(huì)給它包一個(gè)外包。
好了钦铺,說了那么多,再看看HeaderViewListAdapter肢预,headerViews 和 footerViews 充當(dāng)什么角色矛洞。
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 應(yīng)該是為了預(yù)防 NPE
boolean mAreAllFixedViewsSelectable; //應(yīng)該和外面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() { // 可以看出這里獲取數(shù)量的時(shí)候是把 headerView 和 emptyView 算進(jìn)去的
if (mAdapter != null) {
return getFootersCount() + getHeadersCount() + mAdapter.getCount();
} else {
return getFootersCount() + getHeadersCount();
}
}
public Object getItem(int position) { // 這里就是返回對應(yīng)的數(shù)據(jù),header / footer 返回的就是我們設(shè)置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只是儲(chǔ)存一些備份的數(shù)據(jù)噩峦,不能在這里動(dòng)態(tài)變化的,當(dāng)然抽兆,它會(huì)被回收
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();
// 這里我本來覺得應(yīng)該+1的识补,但是后來想了想,getViewTypeCount()
// 一般是給開發(fā)者配合 getView 使用的辫红,那么如果把 AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
// 也給算進(jìn)去的話凭涂,可能有些開發(fā)者設(shè)計(jì)的 adapter 沒有處理headerView 反而麻煩了。
}
return 1;
}
public boolean isEmpty() {
// 這個(gè)是配合EmptyView 的 從這里可以看出這里計(jì)算是否為空不會(huì)把 headerView 贴妻、footerView 算進(jìn)去
return mAdapter == null || mAdapter.isEmpty();
}
}