1. 程式人生 > >listView在添加了headerView後的點選事件解析

listView在添加了headerView後的點選事件解析

listView中的item來自於adapter,我們在開發過程中會去例項化一個adapter往listView中新增資料。這就造成了一個習慣性的認知,那就是以為listView中的item全部來自於我們實現的adapter,item的位置與我們實現的adapter中包含的資料的位置是一一對應的。如是當我們想給listView新增點選事件的時候,就會有如下程式碼:

listview.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                Team team = teams.get(position);
                if (team != null) {
                    GroupActivity.start(GroupListActivity.this, team.getId(), null, SessionTypeEnum.Team);
                }
            }
        });
其中的teams是傳給adapter的一組資料。這段程式碼本來執行的好好的,當有一天需求改動了,需要往listview中新增headerView和footerView的時候,就出現了資料錯亂和陣列越界的錯誤。然後就是各種除錯、各種百度谷歌。。。  今天我詳細的看了一下相關的原始碼,在在這裡總結一下listview添加了headerView到底發生了什麼!

據我調查發現,當listview添加了headerView的時候,它實際持有的adapter並不是我們傳給它的adapter,而是將我們的adapter包裝成一個HeaderViewListAdapter,它實際持有的是這個玩意!下面是官方對這個玩意的解說明:

/**
 * ListAdapter used when a ListView has header views. This ListAdapter
 * wraps another one and also keeps track of the header views and their
 * associated data objects.
 *<p>This is intended as a base class; you will probably not need to
 * use this class directly in your own code.
 */
注:不知道有沒有朋友和我一樣關注到了ListAdapter used when a ListView has header views.中的 header views,當時我就納悶了,這個只有在listView含有headerView的時候才出現嗎,那footerView呢,這兩個東西不是一類嗎。然後我實驗了一下,發現footerView也是可以的,原來是我TM想多了,headerView和footerView就是當做一種View來考慮的,我的理解有問題...

HeaderViewListAdapter是對普通adapter的一個包裝,為什麼會有這個操作呢?上面說到了,listview中的view都是來自於adapter,但是我們在實現自己的adapter的時候,並沒有繪製headerView啊,我們直接把headerView新增給了listview,好像不關adapter的事。假象!假象!都是假象!機智的谷歌自己強行給含有headerView的listview的adapter給包裝成了HeaderViewListAdapter,用這個HeaderViewListAdapter來管理所有的view,包含headerView、你建立的adapter返回的view和footerView。這樣。listview所有的內容又都是adapter返回的了,果然是機智的谷歌(這一點很佩服,我覺得主要是從設計模式方面來考慮的),但是HeaderViewListAdapter對於我們開發者來說幾乎是透明的,像我這樣的不踩坑估計永遠都不知道還有這個玩意!

這時候listview實際的老相好是HeaderViewListAdapter,我們還傻乎乎的拿自己建立的adapter來說話,就有點...尷尬!

我們來詳細的瞭解一下HeaderViewListAdapter,這玩意包含了你新增的headerView和footerView的資訊,當然它肯定包含你建立的adapter。它主要的功能就是保證從上到下繪製headerView、你建立的adapter中返回的view和footerView。原始碼如下

public View getView(int position, View convertView, ViewGroup parent) {
    // Header (negative positions will throw an IndexOutOfBoundsException)
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;
}
MD這時候從源頭上知道為啥會出現資料錯亂和陣列越界的錯誤了,原來是onItemClick(AdapterView<?> parent, View view, int position, long id)中的position是listview整個的view陣列的下標,包含headerView和footerView,既如果有兩個headerView,那麼listview中正常的item的view是從2開始,而不是從零開始。到這裡,我們已經知道position的具體含義了。那接下來就是想辦法解決問題了。

像上面那段很容易出錯的程式碼,我們是不能再用了,此position非彼position嘛。我們很容就想到對position做一下處理,如下:

//將得到的position減去headerView的數目,然後再判斷指向的是否是我們提供的資料之內
                int realPosition=position-listview.getHeaderViewsCount();
                if (realPosition>=0&&realPosition<teams.size()){
                    //....
                }
這樣我們就不怕資料錯亂和越界問題了。這是一種辦法,上面說過,HeaderViewListAdapter 是用來管理headerView的,那麼對於一些常規的方法,它也進行了一些處理。例如HeaderViewListAdapter 的getItem(int position)這個函式還是返回該position對應的資料,HeaderViewListAdapter 會自動處理,當position指向headerView或者footerView時,返回的是headerView和footerView對應的資料,position指向正常的view時,返回該view對應的資料。例如:當有兩個headerView的時候,getItem(0)和getItem(1)返回的都是headerView對應的資料,如果沒有就返回的是null,getItem(2)返回的是listview中第一個資料(HeaderViewListAdapter 會自動減去headerView的數目,然後去原adapter中找)。原始碼如下:
public Object getItem(int position) {
    // 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;
}
這個方法就不錯了,我們不用處理position的問題了,可以直接通過這個方法來取得資料,當然你肯定要判斷一下null,空指標這種問題,那就不多說了。

說的都很簡單,但是實際用的時候就臥槽了,那麼問題來了,這個方法是HeaderViewListAdapter 中的,那HeaderViewListAdapter 在哪呢?可以通過AdapterView提供的getAdapter()方法獲得該AdapterView的adapter,如果該AdapterView含有headerView,那麼返回的就是HeaderViewListAdapter 啦。其實在onItemClick(AdapterView<?> parent, View view, int position, long id)中就返回了AdapterView,那麼我們的程式碼就可以這樣寫了:

 listview.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
               Team team = (Team) parent.getAdapter().getItem(position);
                if (team != null) {
                    GroupActivity.start(GroupListActivity.this, team.getId(), null, SessionTypeEnum.Team);
                }
            }
        });
怎麼樣,這樣的程式碼我個人感覺還是比較簡潔的。

當然了,這只是針對position的,還有id可以用呢,檢視原始碼就會發現,普通item的id還是返回原adapter的getItemId()方法返回的id,headerView和footerView則是返回-1,

所以可以通過id來實現點選事件。

上面的方法都是解決通過setOnItemClickListener方法給listview新增點選事件的問題,還有一個辦法能從本質上解決什麼headerView、footerView導致position錯亂的問題,z這些不用考慮,嘿嘿,想必你也知道了,直接在adapter的getView中給item新增點選事件就行了。簡單粗暴,哪來那麼多問題,寫個程式碼還要分析這個分析那個,這樣做不就是多出了一大堆匿名內部類嘛,GC多跑幾次就行了。大笑