天天看點

Anroid ListView分組和懸浮Header實作

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

Anroid ListView分組和懸浮Header實作

上圖中标紅的1,2,3,4四張圖中,當向上滑動時,仔細觀察灰色條的Header變化,當第二組向上滑動時,會把第一組的懸浮Header擠上去。

這種效果在Android是沒有的,iOS的SDK就自帶這種效果。這篇文章就介紹如何在Android實作這種效果。

1、懸浮Header的實作

其實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;
                break;
            }

            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);
                }
                mHeaderViewVisible = true;
                break;
            }
        }
    }
           

1.4、onLayout和onMeasure 在這兩個方法中,控制Header View的位置及大小

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (mHeaderView != null) {
            measureChild(mHeaderView, widthMeasureSpec, heightMeasureSpec);
            mHeaderViewWidth = mHeaderView.getMeasuredWidth();
            mHeaderViewHeight = mHeaderView.getMeasuredHeight();
        }
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        if (mHeaderView != null) {
            mHeaderView.layout(0, 0, mHeaderViewWidth, mHeaderViewHeight);
            configureHeaderView(getFirstVisiblePosition());
        }
    }
           

好了,到這裡,懸浮Header View就完了,各位可能看不到完整的代碼,隻要明白這幾個核心的方法,自己寫出來,也差不多了。

2、ListView Section實作

有兩種方法實作ListView Section效果,請參考http://cyrilmottier.com/2011/07/05/listview-tips-tricks-2-section-your-listview/ 方法一: 每一個ItemView中包含Header,通過資料來控制其顯示或隐藏,實作原理如下圖:

Anroid 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,得到指定位置的資料變得複雜一些

到這裡,我的實作方式是選擇第二種方案,盡管它的實作方式要複雜一些,但優點比較明顯。

3、Adapter的實作

這裡主要就是說一下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;
        }
        
        @Override
        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;
        }
        
        @Override
        public int getCount() {
            return mDatas.size();
        }
        
        @Override
        public int getItemViewType(int position) {
            // 異常情況處理  
            if (null == mDatas || position <  0|| position > getCount()) {
                return TYPE_ITEM;
            } 
            
            Contact item = mDatas.get(position);
            if (item.isSection) {
                return TYPE_CATEGORY_ITEM;
            }
            
            return TYPE_ITEM;
        }

        @Override
        public int getViewTypeCount() {
            return 2;
        }

        @Override
        public Object getItem(int position) {
            return (position >= 0 && position < mDatas.size()) ? mDatas.get(position) : 0;
        }

        @Override
        public long getItemId(int position) {
            return 0;
        }

        @Override
        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:
                if (null == convertView) {
                    convertView = getHeaderView();
                }
                itemView = (TextView) convertView;
                itemView.setText(data.toString());
                break;
            }
            
            return convertView;
        }

        @Override
        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;
        }

        @Override
        public void configurePinnedHeader(View header, int position, int alpha) {
            Contact item = (Contact) getItem(position);
            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();
            } else {
                this.sortLetter = "#";
            }
        }
    }
    
    @Override
    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;
    
    @Override
    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() {
            return false;
        }
        
        @Override
        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;
        }
        
        @Override
        public int getCount() {
            return mDatas.size();
        }
        
        @Override
        public int getItemViewType(int position) {
            // 異常情況處理  
            if (null == mDatas || position <  0|| position > getCount()) {
                return TYPE_ITEM;
            } 
            
            Contact item = mDatas.get(position);
            if (item.isSection) {
                return TYPE_CATEGORY_ITEM;
            }
            
            return TYPE_ITEM;
        }

        @Override
        public int getViewTypeCount() {
            return 2;
        }

        @Override
        public Object getItem(int position) {
            return (position >= 0 && position < mDatas.size()) ? mDatas.get(position) : 0;
        }

        @Override
        public long getItemId(int position) {
            return 0;
        }

        @Override
        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:
                if (null == convertView) {
                    convertView = getHeaderView();
                }
                itemView = (TextView) convertView;
                itemView.setText(data.toString());
                break;
            }
            
            return convertView;
        }

        @Override
        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;
        }

        @Override
        public void configurePinnedHeader(View header, int position, int alpha) {
            Contact item = (Contact) getItem(position);
            if (null != item) {
                if (header instanceof TextView) {
                    ((TextView) header).setText(item.sectionStr);
                }
            }
        }
    }
}
           

關于資料加載,分組的邏輯這裡就不列出了,資料分組請參考: Android 實作ListView的A-Z字母排序和過濾搜尋功能,實作漢字轉成拼音

最後來一張截圖:

Anroid ListView分組和懸浮Header實作

繼續閱讀