天天看點

android自定義listview實作header懸浮框效果

之前在使用ios時,看到過一種分組的view,每一組都有一個header,在上下滑動的時候,會有一個懸浮的header,這種體驗覺得很不錯,請看下圖:

android自定義listview實作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的文本,圖檔等,這個方法是由調用者去實作的。

android自定義listview實作header懸浮框效果

/** 

 * 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方法中繪制的:

android自定義listview實作header懸浮框效果

@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的移動位移。下面是具體的實作:

android自定義listview實作header懸浮框效果

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的位置及大小

android自定義listview實作header懸浮框效果

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,通過資料來控制其顯示或隐藏,實作原理如下圖:

android自定義listview實作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這兩個方法的實作

android自定義listview實作header懸浮框效果

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的定義如下:

android自定義listview實作header懸浮框效果

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 + ")";  

}    

完整的代碼

android自定義listview實作header懸浮框效果

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>

最後來一張截圖:

android自定義listview實作header懸浮框效果

繼續閱讀