拆解PinnedHeaderListView源碼

本系列博客主要用於學習開源源碼中一些優秀的編碼思想技巧和一些不常見的開發方式。

千里之行,始於足下

明確學習意圖

首先明確學習研究PinnedHeaderListView這個源碼的用途,通過閱讀本博客你可以瞭解到以下幾個方面:(約定如下:官方的是HeaderView,自己定義的header叫pinnedHeaderView)

  1. 如何去監聽ListView,如何知道當前可見的item的位置,以及對其拓展的操作。
  2. 瞭解到ListView中的HeadView 和FooterView。
  3. 瞭解ViewGroup的繪製過程。
  4. 瞭解View的定位方法。
  5. 瞭解View的另類繪製方法。
  6. 瞭解PinnedHeaderListView的核心實現原理。
    帶着上面幾方面的疑惑去看下面的源碼分析或許能收穫更多

首先通過對代碼的解讀,發現其比較重要的4個類

  • PinnedHeaderListView: 實現組的頭部總是懸浮在頂部的listview
  • SectionedBaseAdapter: 封裝的adapter的抽象類
  • PinnedHeaderListViewMainActivity: 具體使用的activity
  • TestSectionedAdapter: 實現了抽象類SectionedBaseAdapter的adapter

我們先來解讀以下SectionedBaseAdapter

我們看到它實現了PinnedHeaderListView.PinnedSectionedHeaderAdapter 這個接口
其定義如下:

 public static interface PinnedSectionedHeaderAdapter {
     public boolean isSectionHeader(int position); //是否是組的頭部
     public int getSectionForPosition(int position); //根據位置判斷對應的組號
     public View getSectionHeaderView(int section, View convertView, ViewGroup parent); // 得到組的頭部view
      public int getSectionHeaderViewType(int section); 
      public int getCount();
    }

比較重要的方法後面都有了對應的註釋,源碼中寫的也比較簡單,在次不再細說,主要是getSectionHeaderView方法比較重要,是我們獲取headView的方法。此處不是我們研究的重點,感興趣的可以去深入研究一下。

下面我們來分析一下PinnedHeaderListView

我們看到它

public class PinnedHeaderListView extends ListView implements OnScrollListener , AdapterView.OnItemClickListener

主要是OnScrollListener 這個是我們監聽當前的Listview滾動狀態的監聽器。
我們先總體的看一下OnScroll裏面的方法,代碼如下:

@Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        if (mOnScrollListener != null) {
            mOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);//使外部也可以監聽到onScroll事件
        }
        headerCount = getHeaderViewsCount();//獲取headView
        if (mAdapter == null || mAdapter.getCount() == 0 || !mShouldPin || (firstVisibleItem < headerCount)) {
            mCurrentHeader = null;
            mHeaderOffset = 0.0f;
            for (int i = firstVisibleItem; i < firstVisibleItem + visibleItemCount; i++) {
                View header = getChildAt(i);
                if (header != null) {
                    header.setVisibility(VISIBLE);
                }
            }
            return;
        }

        firstVisibleItem -= getHeaderViewsCount();//去掉header view的影響

        int section = mAdapter.getSectionForPosition(firstVisibleItem); //得到組號
        int viewType = mAdapter.getSectionHeaderViewType(section);
        mCurrentHeader = getSectionHeaderView(section, mCurrentHeaderViewType != viewType ? null : mCurrentHeader);
        //layout header,使它在最頂端
        ensurePinnedHeaderLayout(mCurrentHeader);
        mCurrentHeaderViewType = viewType;
        mHeaderOffset = 0.0f;
        for (int i = firstVisibleItem; i < firstVisibleItem + visibleItemCount; i++) {
            if (mAdapter.isSectionHeader(i)) {
                View header = getChildAt(i - firstVisibleItem);
                float headerTop = header.getTop();
                float pinnedHeaderHeight = mCurrentHeader.getMeasuredHeight();
                header.setVisibility(VISIBLE);
                if (pinnedHeaderHeight >= headerTop && headerTop > 0) { // 下一個組的頭部快滑動到頂部,距離頂部的距離小於現在在頂部懸浮的head的高度了
                    mHeaderOffset = headerTop - header.getHeight(); //MheaderOffset是小於0的
                } else if (headerTop <= 0) { //下一個組的頭部滑動到了頂部了
                    header.setVisibility(INVISIBLE);
                }
            }
        }
        invalidate();
    }

此處的代碼比較多,我們一點一點的進行分析。

代碼細節

  • 設置外部監聽的回調
if (mOnScrollListener != null) {
            mOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);//使外部也可以監聽到onScroll事件
        }

這段代碼是讓用戶設置滑動監聽的時候也能監聽到對應的事件

  • 獲取Listview的HeaderView
 headerCount = getHeaderViewsCount(); //獲取headView

首先我們得了解一下什麼是headerView,怎麼使用。
headview是通過listview.addHeaderView()的方式添加的,此處可以添加的任何一種類型的view,而且此處的佈局是不需要經過adpter也就是說不需要經過getView()。
如果當你需要在Listview最上方設置一個佈局且不需要它隨着下面的數據的變動而改變使用此方法正合適。另一個與之對應的是addFooterView(),這個就是加底部的佈局的。

此方法是用於獲取headerView的數量的,從而我們也就知道了,headerView是可以設置多個的。(headerview會影響onItemClickLisener方法中的position)

  • 判斷的符合條件
if (mAdapter == null || mAdapter.getCount() == 0 || !mShouldPin || (firstVisibleItem < headerCount)) {
                 ...
                 return
}

如果滿足了這些條件的話就直接return ,這些屬於無效條件

  • 得到分組相關的數據
   firstVisibleItem -= getHeaderViewsCount();//去掉header view的影響
   int section = mAdapter.getSectionForPosition(firstVisibleItem); //得到組號
   int viewType = mAdapter.getSectionHeaderViewType(section);
   mCurrentHeader = getSectionHeaderView(section, mCurrentHeaderViewType != viewType ? null : mCurrentHeader);

之前我們說過headerView會影響我們的position,影響的內容爲getView中使用的postion和OnItemClickLisener中的postion不對應,所以我們需要消除headerView的影響,減去headerView的數量,就是其在getView中的位置

  • 重置數據並使headerView一直在最上方
   ensurePinnedHeaderLayout(mCurrentHeader);//重新對mCurrentHeader定位,讓其一直在最上方
   mCurrentHeaderViewType = viewType;
   mHeaderOffset = 0.0f;//移動的間距

比較重要的方法就是ensurePinnedHeaderLayout()我們稍後詳細的分析它。

  • 正真的核心代碼
    for (int i = firstVisibleItem; i < firstVisibleItem + visibleItemCount; i++) {
            if (mAdapter.isSectionHeader(i)) {
                View header = getChildAt(i - firstVisibleItem);
                float headerTop = header.getTop();
                float pinnedHeaderHeight = mCurrentHeader.getMeasuredHeight();
                header.setVisibility(VISIBLE);
                if (pinnedHeaderHeight >= headerTop && headerTop > 0) { // 下一個組的頭部快滑動到頂部,距離頂部的距離小於現在在頂部懸浮的head的高度了
                    mHeaderOffset = headerTop - header.getHeight(); //MheaderOffset是小於0的
                } else if (headerTop <= 0) { //下一個組的頭部滑動到了頂部了
                    header.setVisibility(INVISIBLE);
                }
            }
        }

從整體上看,這是遍歷可視範圍內的item,然後通過mAdapter.isSectionHeader(i)判斷是不是header,然後通過getChildAt(i - firstVisibleItem) 獲取當前listview對應位置的佈局,通過這兩個判斷同時成立的話就是獲取當前最上方的pinnedHeaderView 。而mCurrentHeader 是上一個組的pinnedHeaderView ,通過header.getTop() 獲取當前最上方的pinnedHeaderView 距離上方的位置,通過mCurrentHeader.getMeasuredHeight() 獲取上一個pinnedHeaderView 的高度,通過header.setVisibility(VISIBLE) 恢復header的顯示,這個是用於往回滾時的操作,與之對應的是header.setVisibility(INVISIBLE)
我們先看一下這個判斷條件if (pinnedHeaderHeight >= headerTop && headerTop > 0),噹噹前的mCurrentHeader高度大於當前最上方的pinnedHeaderView 的話說明此時下方已經和當前的mCurrentHeader將要交叉。通過mHeaderOffset = headerTop - header.getHeight() 獲取偏移量
另一個判斷(headerTop <= 0) 這個是噹噹前的header劃過頂部的時候,通過header.setVisibility(INVISIBLE);進行隱藏,所以我們也就知道了它正真的過程,是當劃過的時候將其隱藏了,回來的時候又重新設置可見了,而上方的View又是怎麼回事呢?我們來繼續分析

未完待續…

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章