之前在使用iOS时,看到过一种分组的View,每一组都有一个Header,在上下滑动的时候,会有一个悬浮的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,通过数据来控制其显示或隐藏,实现原理如下图:

优点: 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 mDatas;        private static final int TYPE_CATEGORY_ITEM = 0;          private static final int TYPE_ITEM = 1;                  public ListViewAdapter(ArrayList 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 mDatas;        private static final int TYPE_CATEGORY_ITEM = 0;          private static final int TYPE_ITEM = 1;                  public ListViewAdapter(ArrayList 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字母排序和过滤搜索功能,实现汉字转成拼音


最后来一张截图:




更多相关文章

  1. 一句话锁定MySQL数据占用元凶
  2. Android传感器-开发指南
  3. 关于Android(安卓)draw中的画布的说明
  4. Android数据加密DES、3DES、AES
  5. 自定义View 篇一--------《自定义View流程分析》
  6. android 开发 socket发送会有部分乱码,串码,伴随着数据接收不完整
  7. Android中保存图片的两种方式
  8. Android后门GhostCtrl,完美控制设备任意权限并窃取用户数据
  9. 在mac上无法使用Android(安卓)Studio的解决方法

随机推荐

  1. Android、JUnit深入浅出(一)——JUnit初步
  2. Android(安卓)in Mono开发初体验之DataBa
  3. 基于Android的WiFi对讲机项目简介
  4. Android百度地图调用和GPS定位
  5. Android(安卓)内存泄漏调试(转载)
  6. Android的Animation的onAnimationXXX/onA
  7. android RecyclerView布局真的只是那么简
  8. 处女男学Android(十三)---Android(安卓)轻
  9. Android(安卓)DiskLruCache完全解析,硬盘
  10. Android Studio无法真机调试