正確處理listview的position

yangxi_001發表於2017-07-31

當ListView包含有HeaderView或FooterView時,傳入getView或者onItemClick的position是怎樣的,這是個值得探討的問題

先列出錯誤的用法

定義:

[java] view plain copy
  1. private MyAdapter mAdapter;  
  2.   
  3.     /** 
  4.      * 包含資料的list 
  5.      */  
  6.     private List<String> mDataList1 = new ArrayList<String>();  

錯誤用法一:

[java] view plain copy
  1. @Override  
  2.     public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {  
  3.         String item = (String) mDataList1.get(position);  
  4.         // doSomething...  
  5.     }  

錯誤用法二:

[java] view plain copy
  1. @Override  
  2.     public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {  
  3.         String item = (String) mAdapter.getItem(position);  
  4.         // doSomething...  
  5.     }  

當ListView沒有包含HeaderView和FooterView的時候,上面的用法沒有問題,一旦包含,那麼獲取的資料項可能不準。因為此時傳入的position是包含了HeaderView和FooterView的索引的:

[java] view plain copy
  1. mListView.addHeaderView(headerView);  
  2. mListView.addFooterView(footerView);  
  3.   
  4. mAdapter = new MyAdapter();  
  5. mAdapter.setDataList1(mDataList1);  
  6. mListView.setAdapter(mAdapter);  
  7.   
  8. mListView.setOnItemClickListener(this);  
  9. ...  
  10.   
  11. @Override  
  12. public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {  
  13.     String item = (String) mAdapter.getItem(position);  
  14.     // 當position=1的時候,取出的item是處在索引0位置的資料  
  15. }  

如果按照上面的方式編碼,則點選列表中的任意一項,獲取的資料項始終是position-1項。即這裡的position其實是一個包含了HeaderViews和FooterViews,以及我們的DataList的大List中的索引。

那麼正確獲取資料項的方法是:

[java] view plain copy
  1. public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {  
  2.     String item = (String) adapterView.getAdapter().getItem(position);  
  3.     // doSomething...  
  4. }  

當然你可以用判斷position==0,但是如果包含有多個HeaderView或者FooterView,這樣判斷既麻煩也容易出錯。按照上面的方法做,無需關心position值是什麼,都可以正確獲取資料項,Android已經幫我們處理了所有的情況。

看起來AdapterView.getAdapter().getItem()與Adapter.getItem()沒什麼不同,但實際上,當ListView包含了HeaderView的時候,AdapterView.getAdapter()獲取的Adapter不是我們定義的Adapter。

為了避免下面各種adapter的混淆,命名我們的adapter為myAdapter。

來看下ListView.setAdapter的原始碼,看一下Android對我們的myAdapter做了什麼:

[java] view plain copy
  1. // ListView.java  
  2. ...  
  3.     /** 
  4.      * Sets the data behind this ListView. 
  5.      * 
  6.      * The adapter passed to this method may be wrapped by a {@link WrapperListAdapter}, 
  7.      * depending on the ListView features currently in use. For instance, adding 
  8.      * headers and/or footers will cause the adapter to be wrapped. 
  9.      * 
  10.      * @param adapter The ListAdapter which is responsible for maintaining the 
  11.      *        data backing this list and for producing a view to represent an 
  12.      *        item in that data set. 
  13.      * 
  14.      * @see #getAdapter()  
  15.      */  
  16.     @Override  
  17.     public void setAdapter(ListAdapter adapter) {  
  18.         if (mAdapter != null && mDataSetObserver != null) {  
  19.             mAdapter.unregisterDataSetObserver(mDataSetObserver);  
  20.         }  
  21.   
  22.         resetList();  
  23.         mRecycler.clear();  
  24.   
  25.         if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {  
  26.             mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter);  
  27.         } else {  
  28.             mAdapter = adapter;  
  29.         }  
  30.         ...  
  31.         ...  

可以很清楚的看到,當呼叫ListView.setAdapter的時候,會先判斷是否已經包含了HeaderView和FooterView,如果包含,則ListView新建一個包裝類HeaderViewListAdapter,包含myAdapter,然後ListView內部的另一個adapter引用(AbsListView.mAdapter)指向這個物件,myAdapter並沒有被真的改變。


那麼當ListView包含了HeaderView的時候,呼叫的getItem方法又有什麼不同?來看看HeaderViewListAdapter.getItem(),原始碼如下:

[java] view plain copy
  1. // HeaderViewListAdapter.java  
  2. ...  
  3.   
  4. private final ListAdapter mAdapter;  
  5.   
  6. ...  
  7.   
  8. public HeaderViewListAdapter(ArrayList<ListView.FixedViewInfo> headerViewInfos,  
  9.                              ArrayList<ListView.FixedViewInfo> footerViewInfos,  
  10.                              ListAdapter adapter) {  
  11.     mAdapter = adapter;  
  12.     mIsFilterable = adapter instanceof Filterable;  
  13.   
  14.     if (headerViewInfos == null) {  
  15.         mHeaderViewInfos = EMPTY_INFO_LIST;  
  16.     } else {  
  17.         mHeaderViewInfos = headerViewInfos;  
  18.     }  
  19.   
  20.     if (footerViewInfos == null) {  
  21.         mFooterViewInfos = EMPTY_INFO_LIST;  
  22.     } else {  
  23.         mFooterViewInfos = footerViewInfos;  
  24.     }  
  25.   
  26.     mAreAllFixedViewsSelectable =  
  27.             areAllListInfosSelectable(mHeaderViewInfos)  
  28.             && areAllListInfosSelectable(mFooterViewInfos);  
  29. }  
  30.   
  31. ...  
  32.   
  33. public Object getItem(int position) {  
  34.     // Header (negative positions will throw an IndexOutOfBoundsException)  
  35.     int numHeaders = getHeadersCount();  
  36.     if (position < numHeaders) {  
  37.         return mHeaderViewInfos.get(position).data;  
  38.     }  
  39.   
  40.     // Adapter  
  41.     final int adjPosition = position - numHeaders;  
  42.     int adapterCount = 0;  
  43.     if (mAdapter != null) {  
  44.         adapterCount = mAdapter.getCount();  
  45.         if (adjPosition < adapterCount) {  
  46.             return mAdapter.getItem(adjPosition);  
  47.         }  
  48.     }  
  49.   
  50.     // Footer (off-limits positions will throw an IndexOutOfBoundsException)  
  51.     return mFooterViewInfos.get(adjPosition - adapterCount).data;  
  52. }  
  53.   
  54. ...  

該方法對position的各種情況做了判斷,如果包含有HeaderViews,則會先從position減掉HeaderView的size。看這一句:

[java] view plain copy
  1. return mAdapter.getItem(adjPosition);  

這裡的mAdapter,通過建構函式HeaderViewListAdapter賦值,結合ListView.setAdapter()原始碼可以知道就是myAdapter,所以此時的mAdapter.getItem=myAdapter.getItem,傳入的position範圍是0~DataList.size()。


需要注意的是AdapterView.getCount()返回的資料是包含有HeaderView和FooterView的個數的:

[java] view plain copy
  1. public int getCount() {  
  2.     if (mAdapter != null) {  
  3.         return getFootersCount() + getHeadersCount() + mAdapter.getCount();  
  4.     } else {  
  5.         return getFootersCount() + getHeadersCount();  
  6.     }  
  7. }  

那麼,在myAdapter中的getView,以及getItem傳入的position為什麼沒有受到影響呢?原因是類似的。

ListView最終在渲染item佈局的時候(具體流程不在這裡解釋),會呼叫mAdapter.getView,此處的mAdapter,包含HeaderView的時候是HeaderViewListAdapter,所以還是直接看HeaderViewListAdapter.getView的原始碼:

[java] view plain copy
  1. // HeaderViewListAdapter.java  
  2.   
  3. ...  
  4.   
  5. public View getView(int position, View convertView, ViewGroup parent) {  
  6.     // Header (negative positions will throw an IndexOutOfBoundsException)  
  7.     int numHeaders = getHeadersCount();  
  8.     if (position < numHeaders) {  
  9.         return mHeaderViewInfos.get(position).view;  
  10.     }  
  11.   
  12.     // Adapter  
  13.     final int adjPosition = position - numHeaders;  
  14.     int adapterCount = 0;  
  15.     if (mAdapter != null) {  
  16.         adapterCount = mAdapter.getCount();  
  17.         if (adjPosition < adapterCount) {  
  18.             return mAdapter.getView(adjPosition, convertView, parent);  
  19.         }  
  20.     }  
  21.   
  22.     // Footer (off-limits positions will throw an IndexOutOfBoundsException)  
  23.     return mFooterViewInfos.get(adjPosition - adapterCount).view;  
  24. }  

對於position的處理同getItem(),所以原因也很明瞭了。

瞭解了position與HeaderView之間的關係後,在編寫這部分程式碼的時候就應當特別注意一點:addHeaderView與addFooterView必須在setAdapter之前被呼叫。因為setAdapter中要對headers和footers做判斷的!

不過即使你粗心了,Android也拋異常會提醒你:

Caused by: java.lang.IllegalStateException: Cannot add header view to list — setAdapter has already been called.

相關文章