码迷,mamicode.com
首页 > 移动开发 > 详细

Android仿联系人列表分组悬浮列表,PinnedHeaderListView源码解析

时间:2016-04-18 17:23:04      阅读:383      评论:0      收藏:0      [点我收藏+]

标签:

github地址:https://github.com/JimiSmith/PinnedHeaderListView
关于实现类似联系人列表,组的头部总是悬浮在listview最顶部的效果,github上面有两个比较好的实现,分别是pinnedSectionListview和pinnedHeaderListView,之所以选择后者进行源码解析,是因为后者的源码比较简单,便于我们理解实现的精髓所在。
如果你想直接实现Android仿联系人列表分组悬浮列表,
自定义PinnedHeaderListView,看这里 http://blog.csdn.net/u010335298/article/details/51150346


代码结构

翻开源码,我们一共可以找到四个有用的类,分别是:
1. PinnedHeaderListView: 实现组的头部总是悬浮在顶部的listview
2. SectionedBaseAdapter: 封装的adapter的抽象类
3. PinnedHeaderListViewMainActivity: 具体使用的activity
4. TestSectionedAdapter: 实现了抽象类SectionedBaseAdapter的adapter


SectionedBaseAdapter

首先,我们来看抽象类SectionedBaseAdapter的实现
技术分享


接口PinnedHeaderListView.PinnedSectionedHeaderAdapter

public abstract class SectionedBaseAdapter extends BaseAdapter implements PinnedHeaderListView.PinnedSectionedHeaderAdapter 

可以看到,SectionedBaseAdapter继承BaseAdapter,
同时实现了PinnedHeaderListView.PinnedSectionedHeaderAdapter这个接口
我们来看PinnedHeaderListView.PinnedSectionedHeaderAdapter的定义:


    public static interface PinnedSectionedHeaderAdapter {
        public boolean isSectionHeader(int position); //是否是组的头部

        public int getSectionForPosition(int position); //根据位置判断对应的组号

        public View getSectionHeaderView(int section, View convertView, ViewGroup parent); // 得到组的头部view

        public int getSectionHeaderViewType(int section); //

        public int getCount();

    }

看一下SectionedBaseAdapter的实现:

 /*********************************************************************************************************
     *
     *
     * 以下 , 实现了PinnedSectionedHeaderAdapter接口
     *
     *
     * *********************************************************************************************************/
    /**
     * 是否是组的头部
     * @param position
     * @return
     */
    public final boolean isSectionHeader(int position) {
        int sectionStart = 0;
        for (int i = 0; i < internalGetSectionCount(); i++) {
            if (position == sectionStart) {
                return true;
            } else if (position < sectionStart) {
                return false;
            }
            sectionStart += internalGetCountForSection(i) + 1;
        }
        return false;
    }

    /**
     * 根据位置得到对应的组号
     * @param position
     * @return
     */
    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;
    }

    /**
     *
     * @param section
     * @param convertView
     * @param parent
     * @return
     */
    public abstract View getSectionHeaderView(int section, View convertView, ViewGroup parent);

    /**
     *
     * @param section
     * @return
     */
    public int getSectionHeaderViewType(int section) {
        return HEADER_VIEW_TYPE;
    }

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

    /*********************************************************************************************************
     * 以上 , 实现了PinnedSectionedHeaderAdapter接口
     *********************************************************************************************************/

可以看到,具体的getSectionHeaderView是要在我们自己的adapter中实现的。


继承的BaseAdapter部分的实现

getView方法

/**
     * 根据position是不是sectionHeader,来判断是调用返回getSectionHeaderView,还是调用返回getItemView
     * @param position
     * @param convertView
     * @param parent
     * @return
     */
    @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);
    }

可以看到,getView跟据是否是组的头部,分别调用了getSectionHeaderView和getItemView,
getSectionHeaderView和getItemView都是抽象方法,都需要我们在自己定义的adapter中去实现。


getCount()

@Override
    public final int getCount() {
        if (mCount >= 0) {
            return mCount;
        }
        int count = 0;
        for (int i = 0; i < internalGetSectionCount(); i++) {
            count += internalGetCountForSection(i);//添加组?元素的个数
            count++; // 添加组头部
        }
        mCount = count;
        return count;
    }

可以看出,count包括了所有的组内元素的个数和所有的组头部个数


getPositionInSectionForPosition

 /*********************************************************************************************************
     * 以上 , 实现了PinnedSectionedHeaderAdapter接口
     *********************************************************************************************************/

    /**
     * 得到在组中的位置
     * @param position
     * @return
     */
    public int getPositionInSectionForPosition(int position) {
        // first try to retrieve values from cache
        Integer cachedPosition = mSectionPositionCache.get(position);
        if (cachedPosition != null) {
            return cachedPosition;
        }
        int sectionStart = 0;
        for (int i = 0; i < internalGetSectionCount(); i++) {
            int sectionCount = internalGetCountForSection(i);
            int sectionEnd = sectionStart + sectionCount + 1;
            if (position >= sectionStart && position < sectionEnd) {
                int positionInSection = position - sectionStart - 1;
                mSectionPositionCache.put(position, positionInSection);
                return positionInSection;
            }
            sectionStart = sectionEnd;
        }
        return 0;
    }

把从cache中得到的忽略,从for循环开始看
循环每个组内,可以看到,if (position >= sectionStart && position < sectionEnd),即position在组内的话,得到在组中的位置,返回在组中的位置


getSectionForPosition

/**
     * 根据位置得到对应的组号
     * @param position
     * @return
     */
    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;
    }

从for循环开始看,if (position >= sectionStart && position < sectionEnd),即position在组内,返回组号。


isSectionHeader

/**
     * 是否是组的头部
     * @param position
     * @return
     */
    public final boolean isSectionHeader(int position) {
        int sectionStart = 0;
        for (int i = 0; i < internalGetSectionCount(); i++) {
            if (position == sectionStart) {
                return true;
            } else if (position < sectionStart) {
                return false;
            }
            sectionStart += internalGetCountForSection(i) + 1;
        }
        return false;
    }

也是遍历所有的组,如果position == sectionStart,也就是是组的头部,返回true.


PinnedHeaderListView的源码

public class PinnedHeaderListView extends ListView implements OnScrollListener , AdapterView.OnItemClickListener{

PinnedHeaderListView继承自ListView,实现了OnScrollListener和OnItemClickListener,
在构造函数中setOnScrollListener(this)和setOnItemClickListener(this);

public PinnedHeaderListView(Context context) {
        super(context);
        super.setOnScrollListener(this);
        super.setOnItemClickListener(this);
    }

我们来看PinnedHeaderListView的代码结构:
技术分享
红框标出的都是比较重要的方法,我们会进行一一讲解

首选,接口PinnedSectionHeaderAdapter我们已经讲过了
我们从onScroll方法开始看


OnScroll方法

 @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        if (mOnScrollListener != null) {
            mOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
        }

        headerCount = getHeaderViewsCount();
        if (mAdapter == null || mAdapter.getCount() == 0 || !mShouldPin || (firstVisibleItem < headerCount)) {
            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();//去掉header view的影响

        int section = mAdapter.getSectionForPosition(firstVisibleItem); //得到组号
        int viewType = mAdapter.getSectionHeaderViewType(section);
        mCurrentHeader = getSectionHeaderView(section, mCurrentHeaderViewType != viewType ? null : mCurrentHeader);
        //layout header,使它在最顶端
        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) { // 下一个组的头部快滑动到顶部,距离顶部的距离小于现在在顶部悬浮的head的高度了
                    mHeaderOffset = headerTop - header.getHeight(); //MheaderOffset是小于0的
                } else if (headerTop <= 0) { //下一个组的头部滑动到了顶部了
                    header.setVisibility(INVISIBLE);
                }
            }
        }

        invalidate();
    }

我们一行一行的来看,

if (mOnScrollListener != null) {
            mOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
        }

这里是对onScrollListener的set,因为我们在构造函数中setOnScrollListener(this),这句代码保证了用户也可以设置自己的onScrollListener

headerCount = getHeaderViewsCount();

得到header的个数

 if (mAdapter == null || mAdapter.getCount() == 0 || !mShouldPin || (firstVisibleItem < headerCount)) {
            mCurrentHeader = null;
            mHeaderOffset = 0.0f;
            for (int i = firstVisibleItem; i < firstVisibleItem + visibleItemCount; i++) {
                View header = getChildAt(i);
                if (header != null) {
                    header.setVisibility(VISIBLE);
                }
            }
            return;
        }

如果adapter为空,或者adapter的count为0,或者我们设置了不顶部悬浮组头部等这些条件的话,就return,不再继续操作

 firstVisibleItem -= getHeaderViewsCount();//去掉header view的影响
 int section = mAdapter.getSectionForPosition(firstVisibleItem); //得到组号
 int viewType = mAdapter.getSectionHeaderViewType(section);
 mCurrentHeader = getSectionHeaderView(section, mCurrentHeaderViewType != viewType ? null : mCurrentHeader);

可以看出,通过getSectionForPosition方法得到了组号,然后根据getSectionHeaderView方法得到我们应该悬浮的组的header view

 //layout header,使它在最顶端
 ensurePinnedHeaderLayout(mCurrentHeader);
 mCurrentHeaderViewType = viewType;

ensurePinnedHeaderLayout,顾名思义,确保pinned header 执行layout,而layout是为了保证pinned header的相对父布局的位置,我们看ensurePinnedHeaderLayout方法的实现

/**
     * layout header,使它在最顶端
     * @param header 组对应的头部view
     */
    private void ensurePinnedHeaderLayout(View header) {
        if (header.isLayoutRequested()) {
            int widthSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), mWidthMode);

            int heightSpec;
            ViewGroup.LayoutParams layoutParams = header.getLayoutParams();
            if (layoutParams != null && layoutParams.height > 0) {
                heightSpec = MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY);
            } else {
                heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
            }
            header.measure(widthSpec, heightSpec);
            header.layout(0, 0, header.getMeasuredWidth(), header.getMeasuredHeight());
        }
    }

可以看出,对header执行了measure和layout,layout时left=0,top=0,也就是让header一直在顶部。
我们继续看scroll函数

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) { 
          // 下一个组的头部快滑动到顶部,距离顶部的距离小于现在在顶部悬浮的head的高度了
              mHeaderOffset = headerTop - header.getHeight(); //MheaderOffset是小于0的
          } else if (headerTop <= 0) { //下一个组的头部滑动到了顶部了
              header.setVisibility(INVISIBLE);
          }
     }
}

invalidate();

使mHeaderOffset 置零
遍历所有可见的item,找到是sectionHeader的第i个item,得到header
看这一句话,if (pinnedHeaderHeight >= headerTop && headerTop > 0),意思是说,如果可见的元素中,第一个是SectionHeader的view距离顶部的距离小于现在悬浮在顶部的组的头部的高度,进行以下操作

mHeaderOffset = headerTop - header.getHeight(); //MheaderOffset是小于0的

给mHeaderOffset赋值。
我们来看mHeaderOffset在哪里用到的。是在disPatchDraw中用到了
dispatchDraw

  @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        if (mAdapter == null || !mShouldPin || mCurrentHeader == null )
            return;
        int saveCount = canvas.save();
        //沿y轴向下移动mHeaderOffset距离,把画布移动到(0,mHeaderOffset)
        //注意,此处mHeaderOffset是<=0的,所以等于说是把画布往上移动了一段距离
        canvas.translate(0, mHeaderOffset);
        canvas.clipRect(0, 0, getWidth(), mCurrentHeader.getMeasuredHeight()); // needed
        // for
        // <
        // HONEYCOMB
        mCurrentHeader.draw(canvas);
        canvas.restoreToCount(saveCount);
    }

可以看出mHeaderOffset小于0的时候,正悬浮在顶部的view向上移动了mHeaderOffset距离。

到此为止,onScroll函数执行完毕了。


onItemClick方法

源码的onItemClick是有一些问题的,我在源码的基础上进行了修改。我们来看
先定义接口OnItemClickListener

 public interface OnItemClickListener {

        void onSectionItemClick(AdapterView<?> adapterView, View view, int section, int position, long id);

        void onSectionClick(AdapterView<?> adapterView, View view, int section, long id);

        void onHeaderClick(AdapterView<?> adapterView, View view, int position, long id);

        void onFooterClick(AdapterView<?> adapterView, View view, int position, long id);

    }

onSectionItemClick: 组的item被点击的点击回调
onSectionClick: 组的头部被点击的点击回调
onHeaderClick: list view的头部view被点击的点击回调
onFooterClick: list view的footer被点击的点击回调

onItemClick方法

 @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    //header view
        if(position < headerCount){
            if(mOnItemClickListener  !=null){
                mOnItemClickListener.onHeaderClick(parent, view, position, id);
            }
            return;
        }
        //footer view
        if(mAdapter!= null && position >= headerCount + mAdapter.getCount()){
            if(mOnItemClickListener  !=null){
                mOnItemClickListener.onFooterClick(parent, view, position - headerCount - mAdapter.getCount(), id);
            }
            return;
        }
        //section header or section item
        position = position - headerCount;
        SectionedBaseAdapter adapter;
        if (parent.getAdapter().getClass().equals(HeaderViewListAdapter.class)) {
            HeaderViewListAdapter wrapperAdapter = (HeaderViewListAdapter) parent.getAdapter();
            adapter = (SectionedBaseAdapter) wrapperAdapter.getWrappedAdapter();
        } else {
            adapter = (SectionedBaseAdapter) parent.getAdapter();
        }
        int section = adapter.getSectionForPosition(position);
        int p = adapter.getPositionInSectionForPosition(position);

        if (p == -1) {//click section header
            if( mOnItemClickListener != null){
                mOnItemClickListener.onSectionClick(parent, view, section, id);
            }
        } else {//click section item
            if( mOnItemClickListener != null){
                mOnItemClickListener.onSectionItemClick(parent, view, section, p, id);
            }

        }
    }

Android仿联系人列表分组悬浮列表,PinnedHeaderListView源码解析

标签:

原文地址:http://blog.csdn.net/u010335298/article/details/51178179

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!