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

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

        }
    }
时间: 2024-10-07 07:04:21

Android仿联系人列表分组悬浮列表,PinnedHeaderListView源码解析的相关文章

Android事件总线(二)EventBus3.0源码解析

相关文章 Android事件总线(一)EventBus3.0用法全解析 前言 上一篇我们讲到了EventBus3.0的用法,这一篇我们来讲一下EventBus3.0的源码以及它的利与弊. 1.构造函数 当我们要调用EventBus的功能时,比如注册或者发送事件,总会调用EventBus.getDefault()来获取EventBus实例: public static EventBus getDefault() { if (defaultInstance == null) { synchroniz

[Android]Toolbar使用详解(三)——源码解析

更多关于Toolbar的使用请移步Toolbar使用详解系列 从Toolbar的使用一步步解析Toolbar源码 大体架构 API 0.设置导航图标 mToolbar.setNavigationIcon(R.drawable.ic_actionbar_flow); 源码如下 public void setNavigationIcon(int resId) { this.setNavigationIcon(this.mTintManager.getDrawable(resId)); } setNa

Android仿联系人列表分组悬浮列表实现,自己定义PinnedHeaderListView实现

效果 (关于gif怎么生成的.我先录手机的屏幕得到mp4文件.然后用这个网址:https://cloudconvert.com/mp4-to-gif 进行的mp4转换gif,使用的时候须要又一次选择gif的大小,不然生成的gif图片太大了) 效果包含下面几个方面 1. 当组的头部从屏幕顶部消失.并且组还有成员在屏幕内的时候.组的头部悬浮在屏幕顶部,并且为红色背景(我设置的组的头部是黄色背景) 2. 当下一个组的头部滑到屏幕顶部与红色的悬浮头部挨着的时候.把红色的头部顶走(红色头部随下一个组的头部

活动列表+星星评分功能(源码下载)

这是一个Demo,内容主要是模仿一些网站的数据列表,实现评分功能.放缩功能. 下面是小Demo: 工具 一个星星评分插件+ jQuery1.8.2 思路 思路很简单,通过整张表格是由后台生成(StringBuilder)然后发送给前台页面填充出来的. 当点击点评按钮后,会调用jQuery的slideToggle()方法,执行动画. 关于Ajax部分 本Demo一共两处使用Ajax,第一处是加载页面的时候,生成页面table,第二处是点击提交的时候,将数据提交给服务器. 代码特别简单,就不贴出来了

Android EventBus3.0使用及源码解析

叨了个叨 最近因为换工作的一些琐事搞的我一个头两个大,也没怎么去学新东西,实在是有些愧疚.新项目用到了EventBus3.0,原来只是听说EventBus的鼎鼎大名,一直没仔细研究过.趁着周末有些时间,研究下代码,也算没有虚度光阴. EventBus GitHub : https://github.com/greenrobot/EventBus EventBus3.0简介 EventBus是greenrobot出品的一个用于Android中事件发布/订阅的库.以前传递对象可能通过接口.广播.文件

Android 热修复Nuwa的原理及Gradle插件源码解析

现在,热修复的具体实现方案开源的也有很多,原理也大同小异,本篇文章以Nuwa为例,深入剖析. Nuwa的github地址 https://github.com/jasonross/Nuwa 以及用于hotpatch生成的gradle插件地址 https://github.com/jasonross/NuwaGradle 而Nuwa的具体实现是根据QQ空间的热修复方案来实现的.安卓App热补丁动态修复技术介绍.在阅读本篇文章之前,请先阅读该文章. 从QQ空间终端开发团队的文章中可以总结出要进行热更

【原】Android热更新开源项目Tinker源码解析系列之二:资源文件热更新

上一篇文章介绍了Dex文件的热更新流程,本文将会分析Tinker中对资源文件的热更新流程. 同Dex,资源文件的热更新同样包括三个部分:资源补丁生成,资源补丁合成及资源补丁加载. 本系列将从以下三个方面对Tinker进行源码解析: Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Android热更新开源项目Tinker源码解析系列之二:资源热更新 Android热更新开源项目Tinker源码解析系类之三:so热更新 转载请标明本文来源:http://www.cnblogs

源码解析Android中View的layout布局过程

Android中的Veiw从内存中到呈现在UI界面上需要依次经历三个阶段:量算 -> 布局 -> 绘图,关于View的量算.布局.绘图的总体机制可参见博文 < Android中View的布局及绘图机制>.量算是布局的基础,如果想了解量算的细节,可参见博文<源码解析Android中View的measure量算过程>.本文将从源码角度解析View的布局layout过程,本文会详细介绍View布局过程中的关键方法,并对源码加上了注释以进行说明. 对View进行布局的目的是计算

Android网络编程(十一)源码解析Retrofit

相关文章 Android网络编程(一)HTTP协议原理 Android网络编程(二)HttpClient与HttpURLConnection Android网络编程(三)Volley用法全解析 Android网络编程(四)从源码解析volley Android网络编程(五)OkHttp2.x用法全解析 Android网络编程(六)OkHttp3用法全解析 Android网络编程(七)源码解析OkHttp前篇[请求网络] Android网络编程(八)源码解析OkHttp后篇[复用连接池] Andr