对于联系人列表,几乎也是比较常见的功能,说简单吧,好像又不是一下就能写完的,今天终于抽空开始完善这个功能点。整个过程比较重要的技术点:
- 通过ContentProvider获取联系人数据
- SideBar导航的绘制以及联动列表
- 粘性头部列表的实现
效果
废话不说,先看效果~
注:其中为了不泄漏好友的联系方式,手机号全被替换了
目前这样的效果,对于列表的功能基本差不多了,对于进一步的功能包括:
- 联系人的添加和删除
- 搜索联系人
- 粘性头部对于GridLayoutManager的适配
但是这两个功能不影响这次联系人列表的实现。
功能
不管在做任何事,我们都要确定我们要做的是什么,那么我们先来看一下这次需要实现的功能包括哪些:
- 获取联系人数据,并按照拼音顺序排列
- SideBar绘制并滑动时联动列表移动到相应位置
- 粘性头部列表
实现方案
- 使用ContentProvider获取数据,其中包含排序
- 自定义View,绘制字母,重写onTouchEvent事件,监听滑动到哪个位置并提供回调
- 列表使用RecyclerView实现,粘性头部使用RecyclerView.ItemDecoration实现
在做实现上边的功能之前,第一个我以前在项目中做过,第二个虽然没有做过但是对于自定义View这个部分看起来还是比较容易的。第三个,虽然以前有了解过ItemDecoration但是没有实际操作过,缺乏经验,但是好在网上已有不少解决方案,其中我参考了这些文章:
RecyclerView探索之通过ItemDecoration实现StickyHeader效果
RecyclerView 悬浮/粘性头部——StickyHeaderDecoration
首先感谢这几位作者的分享。
实现
ContentProvider获取联系人数据
其实对于ContentProvider来说,是系统提供的用于应用之间共享数据使用的方式,获取数据的方式就像执行sql语句和访问服务器接口一样,将目标地址确定,再增加查询条件,正确的访问就能得到想要的数据。
这篇重点不在如何获取数据,所以就直接上代码了(之后也会有文章介绍ContentProvider的详细使用,这部分的内容也比较多)。
1 | public class ContactHelper { |
也就是说通过getContacts的方法就能获取到联系人数据了,并且是按照名字拼音排序。
自定义SideBar
自定义View三部曲,先明确有哪些属性需要自定义,然后重写onMeasure方法,然后重写onDraw方法,这样就把样式基本确定了。当然这里涉及到了事件操作所以还加了一步重写onTouchEvent方法。最后就是直接在布局中引入即可。
属性定义
话不多说哦,直接上代码,简洁明了。
1 | <declare-styleable name="SideBarView"> |
可以有看到,包括了,字母的大小,颜色,以及字母被选中时的颜色。
然后在代码中获取属性。
1 | public SideBarView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { |
方法重写
onMeasure方法
主要就是需要注意在使用wrap_content时的大小。
1 | @Override |
其中在onSizeChanged的时候计算每一个字母的高度,这里也不在细说onMeasure的重写方法,具体可以学习一下扔物线大神的文章:
HenCoder UI 部分 2-2 全新定义 View 的尺寸
onDraw方法
首先这里是把所有字母都显示出来了,在有数据的时候去匹配,只有当这个字母存在是才会存在有效的联动。
1 | @Override |
可以看到其实就是一个文本的绘制,每个字母在每个等分的高度中居中绘制。
onTouchEvent方法
这里其实就是主要监听ACTION_MOVE事件,然后计算y的大小,判断是在字母数组的哪个位置。
1 | @Override |
这里代码比较长,核心逻辑其实就是在chooseLetter方法中,表示是哪个字母在按下和滑动时被选中。
注:同时重写了performClick方法,虽然这里用不到点击方法,但是也实现一下正确的重写方案,因为重写了onTouchEvent方法没有去重写performClick和在其中调用performClick方法的话就会让使用无障碍功能是点击失效。因为这个在实现无障碍功能时遇到过这样的坑,最后使用adb命令解决
这样基本就实现了这个自定义View,其中还提供了滑动到某个字母的回调。
布局使用
1 | <net.arvin.androidstudy.contentprovider.contact.SideBarView |
这个地方没啥好说的。
粘性头部实现
ItemDecoration
上边提到的题篇文章说的挺不错的了,其中有张图特别有用,借用一下:
这四个部分是对界面展示有影响的,背景在最底层,上层布局的会覆盖。其中重要的是在ItemDecoraction中onDraw方法和onDrawOver方法把RecyclerView的itemView夹在中间,也就是说,itemView会覆盖onDraw绘制的内容,onDrawOver会覆盖itemView的内容。还有一个重要的概念就是ItemDecoraction包含了一个偏移量,可以使得itemView不遮挡onDraw绘制的内容,也可以使得onDrawOver不遮挡itemView的内容。
概念逻辑性的东西说完之后,再来看实现。
实现
实现逻辑比不难,细想一下,可以知道其实就是将数据分组,在分组的第一个需要加上绘制的header,然后为了让header悬停的话,屏幕上的第一个itemView也肯定需要绘制header。所以我们定义的偏移量就应该是header的大小,然后就是绘制header,按照这个逻辑去实现,发现header被另一个header替换的时候并不是顶上去的,而是被覆盖了。样子可以去这里看,可能有防盗链,直接引用看不到。
这时候其实只需要再加一个逻辑,就是如果这个item是屏幕的第一个也是这个分组的最后一个时,它的header高度应该在滑动中逐渐减小。核心逻辑就是header的绘制。
1 | @Override |
而绘制的核心就是在于上下左右位置的计算,这个就和自定义ViewGroup中重写layout方法类似。其中核心的判断点都有注释。
这样大体功能基本实现。其中还有一些小技巧哦,在分组和滑动联动时有用到。
其他技巧
分组信息
1 | private void groupInfo() { |
借助HashMap,将联系人姓名拼音的字母和在所有数据中的位置对应起来,然后使用List将联系人数据中出现的字母保存起来,可直接找到当前字母的上一个和下一个字母是什么。其中用到了汉字转拼音的工具类,借用了张涛大神总结的工具类。同时也把SideBarView中的所有字母是否在联系人数据中是否出现设置了。
分组中保存了分组的标题文本,在分组中的位置,以及分组的长度。
1 | @Override |
联动–列表滚动到指定项
有两种方式:参考了这篇文章
没有动画滚动
1 | contactList.scrollToPosition(pos); |
这种方式比较简单。
动画平滑滚动
这种方式复杂一些。
1 | /** |
通过这两步配置,在使用时直接调用smoothMoveToPosition方法,即可平滑的滚动到指定项。
当然还有一个屏幕中的那个文本的显示,也完全是在sidebar联动回调和列表滚动监听中获取当前的要显示的文本作为展示。就不多说了。
总结
对于这个功能的实现,虽然不难,但是包含的知识点还是不少,个人觉得通过这样的方式,对于我的提升是很有帮助的,不在对于每个知识点无从下手,要把知识运用到实际中去才是硬道理。
最后再次附上项目代码,这个项目会包含很多功能点,对于不同的功能点都在不同的包中。