之前在使用ios時,看到過一種分組的view,每一組都有一個header,在上下滑動的時候,會有一個懸浮的header,這種體驗覺得很不錯,請看下圖:
上圖中标紅的1,2,3,4四張圖中,當向上滑動時,仔細觀察灰色條的header變化,當第二組向上滑動時,會把第一組的懸浮header擠上去。
這種效果在android是沒有的,ios的sdk就自帶這種效果。這篇文章就介紹如何在android實作這種效果。
其實android自帶的聯系人的app中就有這樣的效果,我也是把他的類直接拿過來的,實作了pinnedheaderlistview這麼一個類,擴充于listview,核心原理就是在listview的最頂部繪制一個調用者設定的header view,在滑動的時候,根據一些狀态來決定是否向上或向下移動header view(其實就是調用其layout方法,理論上在繪制那裡作一些平移也是可以的)。下面說一下具體的實作:
1.1、pinnedheaderadapter接口
這個接口需要listview的adapter來實作,它定義了兩個方法,一個是讓adapter告訴listview目前指定的position的資料的狀态,比如指定position的資料可能是組的header;另一個方法就是設定header view,比如設定header view的文本,圖檔等,這個方法是由調用者去實作的。
/**
* adapter interface. the list adapter must implement this interface.
*/
public interface pinnedheaderadapter {
/**
* pinned header state: don't show the header.
*/
public static final int pinned_header_gone = 0;
* pinned header state: show the header at the top of the list.
public static final int pinned_header_visible = 1;
* pinned header state: show the header. if the header extends beyond
* the bottom of the first shown element, push it up and clip.
public static final int pinned_header_pushed_up = 2;
* computes the desired state of the pinned header for the given
* position of the first visible list item. allowed return values are
* {@link #pinned_header_gone}, {@link #pinned_header_visible} or
* {@link #pinned_header_pushed_up}.
int getpinnedheaderstate(int position);
* configures the pinned header view to match the first visible list item.
*
* @param header pinned header view.
* @param position position of the first visible list item.
* @param alpha fading of the header view, between 0 and 255.
void configurepinnedheader(view header, int position, int alpha);
}
1.2、如何繪制header view
這是在dispatchdraw方法中繪制的:
@override
protected void dispatchdraw(canvas canvas) {
super.dispatchdraw(canvas);
if (mheaderviewvisible) {
drawchild(canvas, mheaderview, getdrawingtime());
}
1.3、配置header view
核心就是根據不同的狀态值來控制header view的狀态,比如pinned_header_gone(隐藏)的情況,可能需要設定一個flag标記,不繪制header view,那麼就達到隐藏的效果。當pinned_header_pushed_up狀态時,可能需要根據不同的位移來計算header view的移動位移。下面是具體的實作:
public void configureheaderview(int position) {
if (mheaderview == null || null == madapter) {
return;
int state = madapter.getpinnedheaderstate(position);
switch (state) {
case pinnedheaderadapter.pinned_header_gone: {
mheaderviewvisible = false;
break;
}
case pinnedheaderadapter.pinned_header_visible: {
madapter.configurepinnedheader(mheaderview, position, max_alpha);
if (mheaderview.gettop() != 0) {
mheaderview.layout(0, 0, mheaderviewwidth, mheaderviewheight);
}
mheaderviewvisible = true;
case pinnedheaderadapter.pinned_header_pushed_up: {
view firstview = getchildat(0);
int bottom = firstview.getbottom();
int itemheight = firstview.getheight();
int headerheight = mheaderview.getheight();
int y;
int alpha;
if (bottom < headerheight) {
y = (bottom - headerheight);
alpha = max_alpha * (headerheight + y) / headerheight;
} else {
y = 0;
alpha = max_alpha;
madapter.configurepinnedheader(mheaderview, position, alpha);
if (mheaderview.gettop() != y) {
mheaderview.layout(0, y, mheaderviewwidth, mheaderviewheight + y);
1.4、onlayout和onmeasure
在這兩個方法中,控制header view的位置及大小
protected void onmeasure(int widthmeasurespec, int heightmeasurespec) {
super.onmeasure(widthmeasurespec, heightmeasurespec);
if (mheaderview != null) {
measurechild(mheaderview, widthmeasurespec, heightmeasurespec);
mheaderviewwidth = mheaderview.getmeasuredwidth();
mheaderviewheight = mheaderview.getmeasuredheight();
protected void onlayout(boolean changed, int left, int top, int right, int bottom) {
super.onlayout(changed, left, top, right, bottom);
mheaderview.layout(0, 0, mheaderviewwidth, mheaderviewheight);
configureheaderview(getfirstvisibleposition());
好了,到這裡,懸浮header view就完了,各位可能看不到完整的代碼,隻要明白這幾個核心的方法,自己寫出來,也差不多了。
有兩種方法實作listview section效果,請參考http://cyrilmottier.com/2011/07/05/listview-tips-tricks-2-section-your-listview/
方法一:
每一個itemview中包含header,通過資料來控制其顯示或隐藏,實作原理如下圖:
優點:
1,實作簡單,在adapter.getview的實作中,隻需要根據資料來判斷是否是header,不是的話,隐藏item view中的header部分,否則顯示。
2,adapter.getitem(int n)始終傳回的資料是在資料清單中對應的第n個資料,這樣容易了解。
3,控制header的點選事件更加容易
缺點:
1、使用更多的記憶體,第一個item view中都包含一個header view,這樣會費更多的記憶體,多數時候都可能header都是隐藏的。
方法二:
使用不同類型的view:重寫getitemviewtype(int)和getviewtypecount()方法。
1,允許多個不同類型的item
2,了解更加簡單
1,實作比較複雜
2,得到指定位置的資料變得複雜一些
到這裡,我的實作方式是選擇第二種方案,盡管它的實作方式要複雜一些,但優點比較明顯。
這裡主要就是說一下getpinnedheaderstate和configurepinnedheader這兩個方法的實作
private class listviewadapter extends baseadapter implements pinnedheaderadapter {
private arraylist<contact> mdatas;
private static final int type_category_item = 0;
private static final int type_item = 1;
public listviewadapter(arraylist<contact> datas) {
mdatas = datas;
@override
public boolean areallitemsenabled() {
return false;
public boolean isenabled(int position) {
// 異常情況處理
if (null == mdatas || position < 0|| position > getcount()) {
return true;
}
contact item = mdatas.get(position);
if (item.issection) {
return false;
return true;
public int getcount() {
return mdatas.size();
public int getitemviewtype(int position) {
return type_item;
return type_category_item;
return type_item;
public int getviewtypecount() {
return 2;
public object getitem(int position) {
return (position >= 0 && position < mdatas.size()) ? mdatas.get(position) : 0;
public long getitemid(int position) {
return 0;
public view getview(int position, view convertview, viewgroup parent) {
int itemviewtype = getitemviewtype(position);
contact data = (contact) getitem(position);
textview itemview;
switch (itemviewtype) {
case type_item:
if (null == convertview) {
itemview = new textview(sectionlistview.this);
itemview.setlayoutparams(new abslistview.layoutparams(viewgroup.layoutparams.match_parent,
mitemheight));
itemview.settextsize(16);
itemview.setpadding(10, 0, 0, 0);
itemview.setgravity(gravity.center_vertical);
//itemview.setbackgroundcolor(color.argb(255, 20, 20, 20));
convertview = itemview;
itemview = (textview) convertview;
itemview.settext(data.tostring());
case type_category_item:
convertview = getheaderview();
return convertview;
public int getpinnedheaderstate(int position) {
if (position < 0) {
return pinned_header_gone;
contact item = (contact) getitem(position);
contact itemnext = (contact) getitem(position + 1);
boolean issection = item.issection;
boolean isnextsection = (null != itemnext) ? itemnext.issection : false;
if (!issection && isnextsection) {
return pinned_header_pushed_up;
return pinned_header_visible;
public void configurepinnedheader(view header, int position, int alpha) {
if (null != item) {
if (header instanceof textview) {
((textview) header).settext(item.sectionstr);
在getpinnedheaderstate方法中,如果第一個item不是section,第二個item是section的話,就傳回狀态pinned_header_pushed_up,否則傳回pinned_header_visible。
在configurepinnedheader方法中,就是将item的section字元串設定到header view上面去。
【重要說明】
adapter中的資料裡面已經包含了section(header)的資料,資料結構中有一個方法來辨別它是否是section。那麼,在點選事件就要注意了,通過position可能傳回的是section資料結構。
資料結構contact的定義如下:
public class contact {
int id;
string name;
string pinyin;
string sortletter = "#";
string sectionstr;
string phonenumber;
boolean issection;
static characterparser sparser = characterparser.getinstance();
contact() {
contact(int id, string name) {
this.id = id;
this.name = name;
this.pinyin = sparser.getspelling(name);
if (!textutils.isempty(pinyin)) {
string sortstring = this.pinyin.substring(0, 1).touppercase();
if (sortstring.matches("[a-z]")) {
this.sortletter = sortstring.touppercase();
this.sortletter = "#";
public string tostring() {
if (issection) {
return name;
} else {
//return name + " (" + sortletter + ", " + pinyin + ")";
return name + " (" + phonenumber + ")";
}
完整的代碼
package com.lee.sdk.test.section;
import java.util.arraylist;
import android.graphics.color;
import android.os.bundle;
import android.view.gravity;
import android.view.view;
import android.view.viewgroup;
import android.widget.abslistview;
import android.widget.adapterview;
import android.widget.adapterview.onitemclicklistener;
import android.widget.baseadapter;
import android.widget.textview;
import android.widget.toast;
import com.lee.sdk.test.gabaseactivity;
import com.lee.sdk.test.r;
import com.lee.sdk.widget.pinnedheaderlistview;
import com.lee.sdk.widget.pinnedheaderlistview.pinnedheaderadapter;
public class sectionlistview extends gabaseactivity {
private int mitemheight = 55;
private int msecheight = 25;
protected void oncreate(bundle savedinstancestate) {
super.oncreate(savedinstancestate);
setcontentview(r.layout.activity_main);
float density = getresources().getdisplaymetrics().density;
mitemheight = (int) (density * mitemheight);
msecheight = (int) (density * msecheight);
pinnedheaderlistview mlistview = new pinnedheaderlistview(this);
mlistview.setadapter(new listviewadapter(contactloader.getinstance().getcontacts(this)));
mlistview.setpinnedheaderview(getheaderview());
mlistview.setbackgroundcolor(color.argb(255, 20, 20, 20));
mlistview.setonitemclicklistener(new onitemclicklistener() {
@override
public void onitemclick(adapterview<?> parent, view view, int position, long id) {
listviewadapter adapter = ((listviewadapter) parent.getadapter());
contact data = (contact) adapter.getitem(position);
toast.maketext(sectionlistview.this, data.tostring(), toast.length_short).show();
});
setcontentview(mlistview);
private view getheaderview() {
textview itemview = new textview(sectionlistview.this);
itemview.setlayoutparams(new abslistview.layoutparams(viewgroup.layoutparams.match_parent,
msecheight));
itemview.setgravity(gravity.center_vertical);
itemview.setbackgroundcolor(color.white);
itemview.settextsize(20);
itemview.settextcolor(color.gray);
itemview.setbackgroundresource(r.drawable.section_listview_header_bg);
itemview.setpadding(10, 0, 0, itemview.getpaddingbottom());
return itemview;
private class listviewadapter extends baseadapter implements pinnedheaderadapter {
private arraylist<contact> mdatas;
private static final int type_category_item = 0;
private static final int type_item = 1;
public listviewadapter(arraylist<contact> datas) {
mdatas = datas;
@override
public boolean areallitemsenabled() {
public boolean isenabled(int position) {
// 異常情況處理
if (null == mdatas || position < 0|| position > getcount()) {
return true;
}
contact item = mdatas.get(position);
if (item.issection) {
return false;
public int getcount() {
return mdatas.size();
public int getitemviewtype(int position) {
return type_item;
return type_category_item;
public int getviewtypecount() {
return 2;
public object getitem(int position) {
return (position >= 0 && position < mdatas.size()) ? mdatas.get(position) : 0;
public long getitemid(int position) {
return 0;
public view getview(int position, view convertview, viewgroup parent) {
int itemviewtype = getitemviewtype(position);
contact data = (contact) getitem(position);
textview itemview;
switch (itemviewtype) {
case type_item:
if (null == convertview) {
itemview = new textview(sectionlistview.this);
itemview.setlayoutparams(new abslistview.layoutparams(viewgroup.layoutparams.match_parent,
mitemheight));
itemview.settextsize(16);
itemview.setpadding(10, 0, 0, 0);
itemview.setgravity(gravity.center_vertical);
//itemview.setbackgroundcolor(color.argb(255, 20, 20, 20));
convertview = itemview;
}
itemview = (textview) convertview;
itemview.settext(data.tostring());
break;
case type_category_item:
convertview = getheaderview();
return convertview;
public int getpinnedheaderstate(int position) {
if (position < 0) {
return pinned_header_gone;
contact item = (contact) getitem(position);
contact itemnext = (contact) getitem(position + 1);
boolean issection = item.issection;
boolean isnextsection = (null != itemnext) ? itemnext.issection : false;
if (!issection && isnextsection) {
return pinned_header_pushed_up;
return pinned_header_visible;
public void configurepinnedheader(view header, int position, int alpha) {
if (null != item) {
if (header instanceof textview) {
((textview) header).settext(item.sectionstr);
關于資料加載,分組的邏輯這裡就不列出了,資料分組請參考:
<a target="_blank" href="http://blog.csdn.net/xiaanming/article/details/12684155">android 實作listview的a-z字母排序和過濾搜尋功能,實作漢字轉成拼音</a>
最後來一張截圖: