PinnedHeaderListView

組件介紹

所有的View分組器一,每一組都有一個Header,上下滑動到某一個組的時候,它的Header都會懸浮在頂部

  1. 當組的頭部從屏幕頂部消失笼痛,而且組還有成員在屏幕內(nèi)的時候,組的頭部懸浮在屏幕頂部
  2. 當下一個組的頭部滑到屏幕頂部與懸浮頭部挨著的時候信轿,把懸浮頭部
    頂走晃痴,最終懸浮的頭部被替代
pinnedHeaderView.gif

為了更容易理解,需要首先來說說明一下PinnedHeaderListView的大概思路财忽。它是通過將ListView所有的子item分成不同的section倘核,每個section的item的數(shù)目不一樣,每一個section的第一個item稱為header即彪。我們姑且將這兩種不同的類型稱之為section header和section item紧唱。不過值得注意的是,只是從邏輯上做了這樣的劃分隶校,實際上所有的item漏益,無論是否是header都是ListView里普通的一項。
基于上面的說明深胳,首先看一下SectionedBaseAdapter绰疤,它就是一個BaseAdapter,

private static int HEADER_VIEW_TYPE = 0;  
private static int ITEM_VIEW_TYPE = 0;  
  
/**  
 * Holds the calculated values of @{link getPositionInSectionForPosition}  
 */  
private SparseArray<Integer> mSectionPositionCache;  
/**  
 * Holds the calculated values of @{link getSectionForPosition}  
 */  
private SparseArray<Integer> mSectionCache;  
/**  
 * Holds the calculated values of @{link getCountForSection}  
 */  
private SparseArray<Integer> mSectionCountCache;  
  
/**  
 * Caches the item count  
 */  
private int mCount;  
/**  
 * Caches the section count  
 */  
private int mSectionCount;  
  
public SectionedBaseAdapter() {  
    super();  
    mSectionCache = new SparseArray<Integer>();  
    mSectionPositionCache = new SparseArray<Integer>();  
    mSectionCountCache = new SparseArray<Integer>();  
    mCount = -1;  
    mSectionCount = -1;  
}  

1-2行舞终,定義了兩種類型HEADER_VIEW_TYPE和ITEM_VIEW_TYPE轻庆,分別對應(yīng)section header和section item。
7敛劝、11余爆、15行定義了三個SparseArray,它是Android上對HashMap<Integer, Object>的性能更優(yōu)的替代品夸盟。我們可以將他們當做HashMap<Integer, ?Integer>來理解蛾方。他們的作用是用來對section的信息做記錄(緩存)。具體來講,mSectionPositionCache表示第i個位置的item在對應(yīng)的section中是第幾個位置桩砰,mSectionCache表示第i個位置的item是屬于第幾個section拓春,mSectionCountCache表示每個section有幾個item。
20亚隅、24行痘儡,表示總的item數(shù)(包括每個section的header)和section數(shù)。

@Override  
public final int getCount() {  
    if (mCount >= 0) {  
        return mCount;  
    }  
    int count = 0;  
    for (int i = 0; i < internalGetSectionCount(); i++) {  
        count += internalGetCountForSection(i);  
        count++; // for the header view  
    }  
    mCount = count;  
    return count;  
}  
  
@Override  
public final Object getItem(int position) {  
    return getItem(getSectionForPosition(position), getPositionInSectionForPosition(position));  
}  
  
@Override  
public final long getItemId(int position) {  
    return getItemId(getSectionForPosition(position), getPositionInSectionForPosition(position));  
}  
  
@Override  
public final View getView(int position, View convertView, ViewGroup parent) {  
    if (isSectionHeader(position)) {  
        return getSectionHeaderView(getSectionForPosition(position), convertView, parent);  
    }  
    return getItemView(getSectionForPosition(position), getPositionInSectionForPosition(position), convertView, parent);  
}  
  
@Override  
public final int getItemViewType(int position) {  
    if (isSectionHeader(position)) {  
        return getItemViewTypeCount() + getSectionHeaderViewType(getSectionForPosition(position));  
    }  
    return getItemViewType(getSectionForPosition(position), getPositionInSectionForPosition(position));  
}  
  
@Override  
public final int getViewTypeCount() {  
    return getItemViewTypeCount() + getSectionHeaderViewTypeCount();  
}  

這段代碼就是重寫了BaseAdapter的幾個方法枢步,大家對此應(yīng)該很熟悉沉删,所不同的是在方法的內(nèi)部實現(xiàn),對section的item和header做了區(qū)分處理醉途。

public abstract Object getItem(int section, int position);  
  
public abstract long getItemId(int section, int position);  
  
public abstract int getSectionCount();  
  
public abstract int getCountForSection(int section);  
  
public abstract View getItemView(int section, int position, View convertView, ViewGroup parent);  
  
public abstract View getSectionHeaderView(int section, View convertView, ViewGroup parent);  

這幾個方法是需要子類去實現(xiàn)的矾瑰。

public final int getSectionForPosition(int position) {  
    // first try to retrieve values from cache  
    Integer cachedSection = mSectionCache.get(position);  
    if (cachedSection != null) {  
        return cachedSection;  
    }  
    int sectionStart = 0;  
    for (int i = 0; i < internalGetSectionCount(); i++) {  
        int sectionCount = internalGetCountForSection(i);  
        int sectionEnd = sectionStart + sectionCount + 1;  
        if (position >= sectionStart && position < sectionEnd) {  
            mSectionCache.put(position, i);  
            return i;  
        }  
        sectionStart = sectionEnd;  
    }  
    return 0;  
}  
private int internalGetCountForSection(int section) {  
    Integer cachedSectionCount = mSectionCountCache.get(section);  
    if (cachedSectionCount != null) {  
        return cachedSectionCount;  
    }  
    int sectionCount = getCountForSection(section);  
    mSectionCountCache.put(section, sectionCount);  
    return sectionCount;  
}  
  
private int internalGetSectionCount() {  
    if (mSectionCount >= 0) {  
        return mSectionCount;  
    }  
    mSectionCount = getSectionCount();  
    return mSectionCount;  
}  

這三個方法與開頭的三個SparseArray對應(yīng),方法中先分別從這三個Cache中獲取對應(yīng)的值隘擎,如果獲取不到殴穴,就根據(jù)條件進行計算,將計算后的結(jié)果放入Cache中货葬。
getPositionInSectionForPosition(int position)用于獲取指定位置的item在它所在的section是第幾個位置采幌。
internalGetCountForSection(int section)用于獲取指定section中item的數(shù)目。
internalGetSectionCount()用戶獲得section總的數(shù)目震桶。
之前提到有幾個抽象方法需要實現(xiàn)休傍,下面就看一下SectionedBaseAdapter的實現(xiàn)類TestSectionedAdapter。

public class TestSectionedAdapter extends SectionedBaseAdapter {  
  
    @Override  
    public Object getItem(int section, int position) {  
        // TODO Auto-generated method stub  
        return null;  
    }  
  
    @Override  
    public long getItemId(int section, int position) {  
        // TODO Auto-generated method stub  
        return 0;  
    }  
  
    @Override  
    public int getSectionCount() {  
        return 7;  
    }  
  
    @Override  
    public int getCountForSection(int section) {  
        return 15;  
    }  
  
    @Override  
    public View getItemView(int section, int position, View convertView, ViewGroup parent) {  
        LinearLayout layout = null;  
        if (convertView == null) {  
            LayoutInflater inflator = (LayoutInflater) parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);  
            layout = (LinearLayout) inflator.inflate(R.layout.pinned_header_listview_list_item, null);  
        } else {  
            layout = (LinearLayout) convertView;  
        }  
        ((TextView) layout.findViewById(R.id.textItem)).setText("Section " + section + " Item " + position);  
        return layout;  
    }  
  
    @Override  
    public View getSectionHeaderView(int section, View convertView, ViewGroup parent) {  
        LinearLayout layout = null;  
        if (convertView == null) {  
            LayoutInflater inflator = (LayoutInflater) parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);  
            layout = (LinearLayout) inflator.inflate(R.layout.pinned_header_listview_header_item, null);  
        } else {  
            layout = (LinearLayout) convertView;  
        }  
        ((TextView) layout.findViewById(R.id.textItem)).setText("Header for section " + section);  
        return layout;  
    }  
  
}  

由于在這個例子中蹲姐,getItem和getItemId兩個方法沒有實際的作用磨取,所以直接返回0和null了。
15-23行可以看出柴墩,要實現(xiàn)的這個ListView有7個section忙厌,每個section有15個item。
25-49行可以獲得section header和section item的View江咳。
Adapter的代碼我們就分析完了逢净,大致就是將ListView分成section,然后其他的方法都是圍繞著section的管理來做的歼指。

下面來看一下PinnedHeaderListView這個類爹土,它有一個內(nèi)部接口,這個接口就是在上面提到的Adapter中實現(xiàn)的东臀,在這里都會用到着饥,相信通過上面的講解犀农,大家可以看出來每個接口的大概意思惰赋。

public static interface PinnedSectionedHeaderAdapter {  
   public boolean isSectionHeader(int position);  
  
   public int getSectionForPosition(int position);  
  
   public View getSectionHeaderView(int section, View convertView, ViewGroup parent);  
  
   public int getSectionHeaderViewType(int section);  
  
   public int getCount();  
  
}  

這個類里的變量定義和構(gòu)造函數(shù)等內(nèi)容我們不在這里啰嗦了,直接看最重要的一部分代碼,這也是實現(xiàn)這個功能的關(guān)鍵赁濒。

@Override  
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {  
   if (mOnScrollListener != null) {  
       mOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);  
   }  
  
   if (mAdapter == null || mAdapter.getCount() == 0 || !mShouldPin || (firstVisibleItem < getHeaderViewsCount())) {  
       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();  
  
   int section = mAdapter.getSectionForPosition(firstVisibleItem);  
   int viewType = mAdapter.getSectionHeaderViewType(section);  
   mCurrentHeader = getSectionHeaderView(section, mCurrentHeaderViewType != viewType ? null : mCurrentHeader);  
   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) {  
               mHeaderOffset = headerTop - header.getHeight();  
           } else if (headerTop <= 0) {  
               header.setVisibility(INVISIBLE);  
           }  
       }  
   }  
  
   invalidate();  
}  

ListView滾動的時候轨奄,會不斷的回調(diào)這個方法,然后在這個方法里實現(xiàn)懸浮header顯示邏輯的控制拒炎。
7-17行先對ListView添加的Header的情況進行處理挪拟,這里的Header不是我們說的section header,而是我們通過ListView的addHeaderView()添加的击你,文章開始的使用方法介紹中就是添加了兩個Header玉组。這種情況下,剛開始是不會有懸浮效果的丁侄,因為還沒有進入section惯雳。
23行得到了mCurrentHeader,就是要懸浮顯示的View鸿摇。
24行代碼保證mCurrentHeader可以懸浮在ListView頂部的固定位置石景。
29-41行代碼就是用來控制header移動的。因為當下方section的header快要到達頂端時拙吉,會將之前懸浮的header頂出顯示區(qū)域潮孽,然后直到之前header消失,新的header就會懸浮在ListView頂端筷黔。這里的關(guān)鍵就是通過View的位置來計算之前懸浮header的偏移量mHeaderOffset往史,然后通過invalidate觸發(fā)dispatchDraw方法以重繪View。

@Override  
protected void dispatchDraw(Canvas canvas) {  
   super.dispatchDraw(canvas);  
   if (mAdapter == null || !mShouldPin || mCurrentHeader == null)  
       return;  
   int saveCount = canvas.save();  
   canvas.translate(0, mHeaderOffset);  
   canvas.clipRect(0, 0, getWidth(), mCurrentHeader.getMeasuredHeight()); // needed  
   // for  
   // <  
   // HONEYCOMB  
   mCurrentHeader.draw(canvas);  
   canvas.restoreToCount(saveCount);  
}  

從dispatchDraw的實現(xiàn)中我們可以看到佛舱,確實是用到了偏移量mHeaderOffset怠堪。其中,先將canvas在Y軸方向上移動了mHeaderOffset的距離名眉,然后截取畫布粟矿,在截取后的畫布上繪制header。
通過上面一系列的處理损拢,最終實現(xiàn)了我們在開頭看到的ListView的懸浮效果陌粹。總結(jié)一下PinnedHeaderListView的基本思路:將ListView邏輯上分成若干個section福压,每個section有一個header掏秩,當header滑動到頂端時,會在ListView上繪制一個懸浮的View荆姆,View的內(nèi)容就是這個header蒙幻,當下面的header2達到頂部與header相交時,根據(jù)滑動距離將header向上移胆筒,直到header消失邮破,header2會懸浮在頂端诈豌,這樣就實現(xiàn)了我們看到的效果。

版本控制

版本號 更新內(nèi)容 修改人 修改時間
1.0 初次發(fā)布 lucky_tiger 2017/7/13

項目地址

所在文件夾 demo位置
widget.PinnedHeaderListView com.qr.demo.widget.PinnedHeaderListViewActivity
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末抒和,一起剝皮案震驚了整個濱河市矫渔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌摧莽,老刑警劉巖庙洼,帶你破解...
    沈念sama閱讀 217,907評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異镊辕,居然都是意外死亡油够,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評論 3 395
  • 文/潘曉璐 我一進店門征懈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來叠聋,“玉大人,你說我怎么就攤上這事受裹÷挡梗” “怎么了?”我有些...
    開封第一講書人閱讀 164,298評論 0 354
  • 文/不壞的土叔 我叫張陵棉饶,是天一觀的道長厦章。 經(jīng)常有香客問我,道長照藻,這世上最難降的妖魔是什么袜啃? 我笑而不...
    開封第一講書人閱讀 58,586評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮幸缕,結(jié)果婚禮上群发,老公的妹妹穿的比我還像新娘。我一直安慰自己发乔,他們只是感情好熟妓,可當我...
    茶點故事閱讀 67,633評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著栏尚,像睡著了一般起愈。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上译仗,一...
    開封第一講書人閱讀 51,488評論 1 302
  • 那天抬虽,我揣著相機與錄音,去河邊找鬼纵菌。 笑死阐污,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的咱圆。 我是一名探鬼主播笛辟,決...
    沈念sama閱讀 40,275評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼功氨,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了隘膘?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,176評論 0 276
  • 序言:老撾萬榮一對情侶失蹤杠览,失蹤者是張志新(化名)和其女友劉穎弯菊,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體踱阿,經(jīng)...
    沈念sama閱讀 45,619評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡管钳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,819評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了软舌。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片才漆。...
    茶點故事閱讀 39,932評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖佛点,靈堂內(nèi)的尸體忽然破棺而出醇滥,到底是詐尸還是另有隱情,我是刑警寧澤超营,帶...
    沈念sama閱讀 35,655評論 5 346
  • 正文 年R本政府宣布鸳玩,位于F島的核電站,受9級特大地震影響演闭,放射性物質(zhì)發(fā)生泄漏不跟。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,265評論 3 329
  • 文/蒙蒙 一米碰、第九天 我趴在偏房一處隱蔽的房頂上張望窝革。 院中可真熱鬧,春花似錦吕座、人聲如沸虐译。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽菱蔬。三九已至,卻和暖如春史侣,著一層夾襖步出監(jiān)牢的瞬間拴泌,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評論 1 269
  • 我被黑心中介騙來泰國打工惊橱, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蚪腐,地道東北人。 一個月前我還...
    沈念sama閱讀 48,095評論 3 370
  • 正文 我出身青樓税朴,卻偏偏與公主長得像回季,于是被迫代替她去往敵國和親家制。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,884評論 2 354

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,127評論 25 707
  • 好久沒有變身成身經(jīng)百戰(zhàn)的社會人士了,突然的視頻面試居然有點慌張鼻忠,慌張到好多問題第一反應(yīng)答不上來涵但,嗚……我老是這樣的...
    JOY你是誰閱讀 2,085評論 0 0
  • 婀娜梅枝迎飛雪,冰霜花蕊送清風(fēng)帖蔓。 攝影:刁刁
    郭紹武閱讀 417評論 0 0
  • 三窮三富過到老塑娇,十年興敗誰知曉澈侠,誰人背后無人說,誰人背后不說人埋酬!什么是真哨啃,什么是假,喜我者写妥,我惜之棘催,嫌我者,我棄之...
    王路柯閱讀 983評論 0 0
  • 留意到自己最近幾天的情緒有些低落,身體有些不舒服∧嫡可能的原因是受外部比較的壓力影響,也可能最近的事情比較多呼猪,給自己...
    sageness閱讀 191評論 0 0