Android仿微信通讯录打造带悬停头部的分组列表(上)
一 概述
本文是android导航分组列表系列上,因时间和篇幅原因分上下,最终上下合璧,完整版效果如下:
上部残卷效果如下:两个itemdecoration,一个实现悬停头部分组列表功能,一个实现分割线(官方demo)
网上关于实现带悬停分组头部的列表的方法有很多,像我看过有主席的自定义expandlistview实现的,也看过有人用一个额外的父布局里面套 recyclerview/listview+一个头部view(位置固定在父布局上方)实现的。
对于以上解决方案,有以下几点个人觉得不好的地方:
1. 现在recyclerview是主流
2. 在recyclerview外套一个父布局总归是增加布局层级,容易overdraw,显得不够优雅。
3. item布局实现带这种分类头部的方法有两种,一种是把分类头部当做一种itemviewtype(麻烦),另一种是每个item布局都包含了分类头部的布局,代码里根据postion等信息动态visible,gone头部(布局冗余,item效率降低)。
况且google为我们提供了itemdecoration,它本身就是用来修饰recyclerview里的item的,它的getitemoffsets() ondraw()方法用于为item分类头部留出空间和绘制(解决缺点3),它的ondrawover()方法用于绘制悬停的头部view(解决缺点2)。
而且更重要的是,itemdecoration出来这么久了,你还不用它?
本文就利用itemdecoration 打造 分组列表,并配有悬停头部功能。
亮点预览:添加多个itemdecoration、它们的执行顺序、itemdecoration方法执行顺序、itemdecoration和recyclerview的绘制顺序
二 使用itemdecoration
用法:为recyclerviewpool添加一个或多个itemdecoration
//如果add多个,那么按照先后顺序,依次渲染。 mrv.additemdecoration(mdecoration = new titleitemdecoration(this, mdatas)); mrv.additemdecoration(new titleitemdecoration2(this,mdatas)); mrv.additemdecoration(new divideritemdecoration(mainactivity.this,divideritemdecoration.vertical_list));
为recyclerview添加itemdecoration只要这么一句additemdecoration(),
它有两个同名重载方法:
additemdecoration(itemdecoration decor) 常用,(按照add顺序,依次渲染itemdecoration)
additemdecoration(itemdecoration decor, int index) add一个itemdecoration,并为它指定顺序
上来就高能,别的讲解recyclerview的文章一般都是对itemdecoration一笔带过,用的demo一般也都是官方的divideritemdecoration类,更别提还添加多个itemdecoration了。其实我也是昨天写demo的时候才发现这个方法,点进去查看了一下源码:
public void additemdecoration(itemdecoration decor) { additemdecoration(decor, -1); } public void additemdecoration(itemdecoration decor, int index) { if (mlayout != null) { mlayout.assertnotinlayoutorscroll("cannot add item decoration during a scroll or" + " layout"); } if (mitemdecorations.isempty()) { setwillnotdraw(false); } if (index < 0) { mitemdecorations.add(decor); } else { mitemdecorations.add(index, decor); } markitemdecorinsetsdirty(); requestlayout(); }
老套路:我们最常用的单参数方法 内部调用了双参数方法,并把index 传入-1。
我们add的itemdecoration 都存储在recyclerview类的mitemdecorations变量里,
这个变量就是一个arraylist,定义如下
private final arraylist<itemdecoration> mitemdecorations = new arraylist<>();
三 itemdecoration方法介绍和编写
常用(全部)方法:
按照在recyclerview中它们被调用的顺序排列:
1. public void getitemoffsets(rect outrect, view view, recyclerview parent, recyclerview.state state)
2. public void ondraw(canvas c, recyclerview parent, recyclerview.state state)
3. public void ondrawover(canvas c, recyclerview parent, recyclerview.state state)
这个三个方法也是继承一个itemdecoration必须实现的三个方法。(其实itemdecoration里除了@deprecated 的方法 也就它们三了,)
方法一的编写
public void getitemoffsets(rect outrect, view view, recyclerview parent, recyclerview.state state):
我们需要利用 parent和state变量,来获取需要的辅助信息,例如postion, 最终调用outrect.set(int left, int top, int right, int bottom)方法,设置四个方向上 需要为itemview设置padding的值。
下图我觉得很经典:摘自(https://blog.piasy.com/2016/03/26/insight-android-recyclerview-itemdecoration/?utm_source=tuicool&utm_medium=referral)向作者表示感谢。如作者不许我转图,烦请联系我删除
本文的 实体bean如下编写:
/** * created by zhangxutong . * date: 16/08/28 */ public class citybean { private string tag;//所属的分类(城市的汉语拼音首字母) private string city; public citybean(string tag, string city) { this.tag = tag; this.city = city; } public string gettag() { return tag; } public void settag(string tag) { this.tag = tag; } public string getcity() { return city; } public void setcity(string city) { this.city = city; } }
getitemoffsets方法 如下:
通过parent获取postion信息,通过postion拿到数据里的每个bean里的分类,因为数据集已经有序,如果与前一个分类不一样,说明是一个新的分类,则需要绘制头部outrect.set(0, mtitleheight, 0, 0);,否则不需要outrect.set(0, 0, 0, 0);。
@override public void getitemoffsets(rect outrect, view view, recyclerview parent, recyclerview.state state) { super.getitemoffsets(outrect, view, parent, state); int position = ((recyclerview.layoutparams) view.getlayoutparams()).getviewlayoutposition(); //我记得rv的item position在重置时可能为-1.保险点判断一下吧 if (position > -1) { if (position == 0) {//等于0肯定要有title的 outrect.set(0, mtitleheight, 0, 0); } else {//其他的通过判断 if (null != mdatas.get(position).gettag() && !mdatas.get(position).gettag().equals(mdatas.get(position - 1).gettag())) { outrect.set(0, mtitleheight, 0, 0);//不为空 且跟前一个tag不一样了,说明是新的分类,也要title } else { outrect.set(0, 0, 0, 0); } } } }
--------------------------------------------------------------------------------
方法二的编写
public void ondraw(canvas c, recyclerview parent, recyclerview.state state)
我们需要利用 parent和state变量,来获取需要的辅助信息,例如绘制的上下左右,childcount, childview等。。最终利用c调用canvas的方法来绘制出我们想要的ui。会自定义view就会写本方法~
ondraw绘制出的内容是在itemview下层,虽然它可以绘制超出getitemoffsets()里的rect区域,但是超出区域最终不会显示,但被itemview覆盖的区域会产生overdraw。
本文如下编写:通过parent获取绘制ui的 left和right以及childcount, 遍历childview,根据childview的postion,和方法一中的判断方法一样,来决定是否绘制分类title区域:
分类绘制title的方法就是自定义view的套路,根据确定的上下左右范围先drawrect绘制一个背景,然后drawtext绘制文字。
(不会自定义view的可参考郭神 洋神 文章:
)。
@override public void ondraw(canvas c, recyclerview parent, recyclerview.state state) { super.ondraw(c, parent, state); final int left = parent.getpaddingleft(); final int right = parent.getwidth() - parent.getpaddingright(); final int childcount = parent.getchildcount(); for (int i = 0; i < childcount; i++) { final view child = parent.getchildat(i); final recyclerview.layoutparams params = (recyclerview.layoutparams) child .getlayoutparams(); int position = params.getviewlayoutposition(); //我记得rv的item position在重置时可能为-1.保险点判断一下吧 if (position > -1) { if (position == 0) {//等于0肯定要有title的 drawtitlearea(c, left, right, child, params, position); } else {//其他的通过判断 if (null != mdatas.get(position).gettag() && !mdatas.get(position).gettag().equals(mdatas.get(position - 1).gettag())) { //不为空 且跟前一个tag不一样了,说明是新的分类,也要title drawtitlearea(c, left, right, child, params, position); } else { //none } } } } } /** * 绘制title区域背景和文字的方法 * * @param c * @param left * @param right * @param child * @param params * @param position */ private void drawtitlearea(canvas c, int left, int right, view child, recyclerview.layoutparams params, int position) {//最先调用,绘制在最下层 mpaint.setcolor(color_title_bg); c.drawrect(left, child.gettop() - params.topmargin - mtitleheight, right, child.gettop() - params.topmargin, mpaint); mpaint.setcolor(color_title_font); mpaint.gettextbounds(mdatas.get(position).gettag(), 0, mdatas.get(position).gettag().length(), mbounds); c.drawtext(mdatas.get(position).gettag(), child.getpaddingleft(), child.gettop() - params.topmargin - (mtitleheight / 2 - mbounds.height() / 2), mpaint); }
写完 12 方法,就已经完成了分类列表title的绘制,方法3实现顶部悬停title效果:go
--------------------------------------------------------------------------------
方法三的编写
public void ondrawover(canvas c, recyclerview parent, recyclerview.state state):
和 ondraw()方法类似, 我们需要利用 parent和state变量,来获取需要的辅助信息,例如绘制的上下左右,position, childview等。。最终利用c调用canvas的方法来绘制出我们想要的ui。同样是会自定义view就会写本方法~
ondrawover绘制出的内容是在recyclerview的最上层,会遮挡住itemview,so天生自带悬停效果,用来绘制悬停view再好不过。
本文如下编写:首先通过parent获取layoutmanager(由于悬停分组列表的特殊性,写死了是linearlayoutmanger),然后获取当前第一个可见itemview以及postion,以及它所属的分类title(tag),然后绘制悬停view的背景和文字(tag),可参考方法2里的书写,大同小异。
@override public void ondrawover(canvas c, recyclerview parent, recyclerview.state state) {//最后调用 绘制在最上层 int pos = ((linearlayoutmanager)(parent.getlayoutmanager())).findfirstvisibleitemposition(); string tag = mdatas.get(pos).gettag(); view child = parent.getchildat(pos); mpaint.setcolor(color_title_bg); c.drawrect(parent.getpaddingleft(), parent.getpaddingtop(), parent.getright() - parent.getpaddingright(), parent.getpaddingtop() + mtitleheight, mpaint); mpaint.setcolor(color_title_font); mpaint.gettextbounds(tag, 0, tag.length(), mbounds); c.drawtext(tag, child.getpaddingleft(), parent.getpaddingtop() + mtitleheight - (mtitleheight / 2 - mbounds.height() / 2), mpaint); }
至此,我们的 带悬停头部的分组列表的itemdecoration就编写完毕了,完整代码如下:
四 分类title itemdecoration完整代码:
/** * 有分类title的 itemdecoration * created by zhangxutong . * date: 16/08/28 */ public class titleitemdecoration extends recyclerview.itemdecoration { private list<citybean> mdatas; private paint mpaint; private rect mbounds;//用于存放测量文字rect private int mtitleheight;//title的高 private static int color_title_bg = color.parsecolor("#ffdfdfdf"); private static int color_title_font = color.parsecolor("#ff000000"); private static int mtitlefontsize;//title字体大小 public titleitemdecoration(context context, list<citybean> datas) { super(); mdatas = datas; mpaint = new paint(); mbounds = new rect(); mtitleheight = (int) typedvalue.applydimension(typedvalue.complex_unit_dip, 30, context.getresources().getdisplaymetrics()); mtitlefontsize = (int) typedvalue.applydimension(typedvalue.complex_unit_sp, 16, context.getresources().getdisplaymetrics()); mpaint.settextsize(mtitlefontsize); mpaint.setantialias(true); } @override public void ondraw(canvas c, recyclerview parent, recyclerview.state state) { super.ondraw(c, parent, state); final int left = parent.getpaddingleft(); final int right = parent.getwidth() - parent.getpaddingright(); final int childcount = parent.getchildcount(); for (int i = 0; i < childcount; i++) { final view child = parent.getchildat(i); final recyclerview.layoutparams params = (recyclerview.layoutparams) child .getlayoutparams(); int position = params.getviewlayoutposition(); //我记得rv的item position在重置时可能为-1.保险点判断一下吧 if (position > -1) { if (position == 0) {//等于0肯定要有title的 drawtitlearea(c, left, right, child, params, position); } else {//其他的通过判断 if (null != mdatas.get(position).gettag() && !mdatas.get(position).gettag().equals(mdatas.get(position - 1).gettag())) { //不为空 且跟前一个tag不一样了,说明是新的分类,也要title drawtitlearea(c, left, right, child, params, position); } else { //none } } } } } /** * 绘制title区域背景和文字的方法 * * @param c * @param left * @param right * @param child * @param params * @param position */ private void drawtitlearea(canvas c, int left, int right, view child, recyclerview.layoutparams params, int position) {//最先调用,绘制在最下层 mpaint.setcolor(color_title_bg); c.drawrect(left, child.gettop() - params.topmargin - mtitleheight, right, child.gettop() - params.topmargin, mpaint); mpaint.setcolor(color_title_font); /* paint.fontmetricsint fontmetrics = mpaint.getfontmetricsint(); int baseline = (getmeasuredheight() - fontmetrics.bottom + fontmetrics.top) / 2 - fontmetrics.top;*/ mpaint.gettextbounds(mdatas.get(position).gettag(), 0, mdatas.get(position).gettag().length(), mbounds); c.drawtext(mdatas.get(position).gettag(), child.getpaddingleft(), child.gettop() - params.topmargin - (mtitleheight / 2 - mbounds.height() / 2), mpaint); } @override public void ondrawover(canvas c, recyclerview parent, recyclerview.state state) {//最后调用 绘制在最上层 int pos = ((linearlayoutmanager)(parent.getlayoutmanager())).findfirstvisibleitemposition(); string tag = mdatas.get(pos).gettag(); view child = parent.getchildat(pos); mpaint.setcolor(color_title_bg); c.drawrect(parent.getpaddingleft(), parent.getpaddingtop(), parent.getright() - parent.getpaddingright(), parent.getpaddingtop() + mtitleheight, mpaint); mpaint.setcolor(color_title_font); mpaint.gettextbounds(tag, 0, tag.length(), mbounds); c.drawtext(tag, child.getpaddingleft(), parent.getpaddingtop() + mtitleheight - (mtitleheight / 2 - mbounds.height() / 2), mpaint); } @override public void getitemoffsets(rect outrect, view view, recyclerview parent, recyclerview.state state) { super.getitemoffsets(outrect, view, parent, state); int position = ((recyclerview.layoutparams) view.getlayoutparams()).getviewlayoutposition(); //我记得rv的item position在重置时可能为-1.保险点判断一下吧 if (position > -1) { if (position == 0) {//等于0肯定要有title的 outrect.set(0, mtitleheight, 0, 0); } else {//其他的通过判断 if (null != mdatas.get(position).gettag() && !mdatas.get(position).gettag().equals(mdatas.get(position - 1).gettag())) { outrect.set(0, mtitleheight, 0, 0);//不为空 且跟前一个tag不一样了,说明是新的分类,也要title } else { outrect.set(0, 0, 0, 0); } } } } } /** * 有分类title的 itemdecoration * created by zhangxutong . * date: 16/08/28 */ public class titleitemdecoration extends recyclerview.itemdecoration { private list<citybean> mdatas; private paint mpaint; private rect mbounds;//用于存放测量文字rect private int mtitleheight;//title的高 private static int color_title_bg = color.parsecolor("#ffdfdfdf"); private static int color_title_font = color.parsecolor("#ff000000"); private static int mtitlefontsize;//title字体大小 public titleitemdecoration(context context, list<citybean> datas) { super(); mdatas = datas; mpaint = new paint(); mbounds = new rect(); mtitleheight = (int) typedvalue.applydimension(typedvalue.complex_unit_dip, 30, context.getresources().getdisplaymetrics()); mtitlefontsize = (int) typedvalue.applydimension(typedvalue.complex_unit_sp, 16, context.getresources().getdisplaymetrics()); mpaint.settextsize(mtitlefontsize); mpaint.setantialias(true); } @override public void ondraw(canvas c, recyclerview parent, recyclerview.state state) { super.ondraw(c, parent, state); final int left = parent.getpaddingleft(); final int right = parent.getwidth() - parent.getpaddingright(); final int childcount = parent.getchildcount(); for (int i = 0; i < childcount; i++) { final view child = parent.getchildat(i); final recyclerview.layoutparams params = (recyclerview.layoutparams) child .getlayoutparams(); int position = params.getviewlayoutposition(); //我记得rv的item position在重置时可能为-1.保险点判断一下吧 if (position > -1) { if (position == 0) {//等于0肯定要有title的 drawtitlearea(c, left, right, child, params, position); } else {//其他的通过判断 if (null != mdatas.get(position).gettag() && !mdatas.get(position).gettag().equals(mdatas.get(position - 1).gettag())) { //不为空 且跟前一个tag不一样了,说明是新的分类,也要title drawtitlearea(c, left, right, child, params, position); } else { //none } } } } } /** * 绘制title区域背景和文字的方法 * * @param c * @param left * @param right * @param child * @param params * @param position */ private void drawtitlearea(canvas c, int left, int right, view child, recyclerview.layoutparams params, int position) {//最先调用,绘制在最下层 mpaint.setcolor(color_title_bg); c.drawrect(left, child.gettop() - params.topmargin - mtitleheight, right, child.gettop() - params.topmargin, mpaint); mpaint.setcolor(color_title_font); /* paint.fontmetricsint fontmetrics = mpaint.getfontmetricsint(); int baseline = (getmeasuredheight() - fontmetrics.bottom + fontmetrics.top) / 2 - fontmetrics.top;*/ mpaint.gettextbounds(mdatas.get(position).gettag(), 0, mdatas.get(position).gettag().length(), mbounds); c.drawtext(mdatas.get(position).gettag(), child.getpaddingleft(), child.gettop() - params.topmargin - (mtitleheight / 2 - mbounds.height() / 2), mpaint); } @override public void ondrawover(canvas c, recyclerview parent, recyclerview.state state) {//最后调用 绘制在最上层 int pos = ((linearlayoutmanager)(parent.getlayoutmanager())).findfirstvisibleitemposition(); string tag = mdatas.get(pos).gettag(); view child = parent.getchildat(pos); mpaint.setcolor(color_title_bg); c.drawrect(parent.getpaddingleft(), parent.getpaddingtop(), parent.getright() - parent.getpaddingright(), parent.getpaddingtop() + mtitleheight, mpaint); mpaint.setcolor(color_title_font); mpaint.gettextbounds(tag, 0, tag.length(), mbounds); c.drawtext(tag, child.getpaddingleft(), parent.getpaddingtop() + mtitleheight - (mtitleheight / 2 - mbounds.height() / 2), mpaint); } @override public void getitemoffsets(rect outrect, view view, recyclerview parent, recyclerview.state state) { super.getitemoffsets(outrect, view, parent, state); int position = ((recyclerview.layoutparams) view.getlayoutparams()).getviewlayoutposition(); //我记得rv的item position在重置时可能为-1.保险点判断一下吧 if (position > -1) { if (position == 0) {//等于0肯定要有title的 outrect.set(0, mtitleheight, 0, 0); } else {//其他的通过判断 if (null != mdatas.get(position).gettag() && !mdatas.get(position).gettag().equals(mdatas.get(position - 1).gettag())) { outrect.set(0, mtitleheight, 0, 0);//不为空 且跟前一个tag不一样了,说明是新的分类,也要title } else { outrect.set(0, 0, 0, 0); } } } } }
五 一些itemdecoration的相关补充姿势
1. 多个itemdecoration,以及它们的绘制顺序。
就像第二节中的用法提到的,可以为一个recyclerview添加多个itemdecoration,那么多个itemdecoration的绘制顺序是什么呢:我们看看源码吧:
第二节中提到,多个itemdecoration最终是存储在recyclerview里的mitemdecorations(arraylist)变量中,那我们就去recyclerview的 源码里搜一搜,看看哪些地方用到了mitemdecorations。
发现在draw()和ondraw()方法里:按照在mitemdecorations里的postion顺序,依次调用了每个itemdecoration的ondrawover和ondraw方法。所以后添加的itemdecoration,如果和前面的itemdecoration的绘制区域有重合的地方,会遮盖住前面的itemdecoration(overdraw)。
@override public void draw(canvas c) { super.draw(c); final int count = mitemdecorations.size(); for (int i = 0; i < count; i++) { mitemdecorations.get(i).ondrawover(c, this, mstate); } @override public void ondraw(canvas c) { super.ondraw(c); final int count = mitemdecorations.size(); for (int i = 0; i < count; i++) { mitemdecorations.get(i).ondraw(c, this, mstate); } }
2. itemdecoration和recyclerview的item的绘制顺序。
在介绍itemdecoration的三个方法时,我们提到过结论:
itemdecoration的ondraw最先调用,绘制在最底层, 其上再绘制itemview 中间层, 再上调用itemdecoration的ondrawover,绘制在最上层。
理由:
由上面代码可见, recyclerview的draw()方法中,在super.draw(c)方法调用完后,才调用mitemdecorations.get(i).ondrawover(c, this, mstate); 而super.draw(c)方法就是直接调用view的public void draw(canvas canvas) 方法,如下所示:
其中又先调用了view(recyclerview)的ondraw()方法,
在recyclerview的ondraw()方法中,会调用mitemdecorations.get(i).ondraw(c, this, mstate);
所以ondraw最先调用,绘制在最底层
后调用了view(viewgroup)的dispatchdraw(canvas)方法;
在viewgroup的dispatchdraw(canvas)方法里,会执行 drawchild(canvas canvas, view child, long drawingtime)方法,绘制每个itemview。
所以itemview绘制在中间层
最后super.draw(c)走完,调用mitemdecorations.get(i).ondrawover(c, this, mstate);
所以再上调用itemdecoration的ondrawover,绘制在最上层。 (从方法名字也可以看出哈)
view的draw()方法如下,
/** * this method is called by viewgroup.drawchild() to have each child view draw itself. * * this is where the view specializes rendering behavior based on layer type, * and hardware acceleration. */ boolean draw(canvas canvas, viewgroup parent, long drawingtime) { ............省略 // step 3, draw the content if (!dirtyopaque) ondraw(canvas); // step 4, draw the children dispatchdraw(canvas);
六 完整代码地址
csdn代码上传中
欢迎光临我的github下载上下合集demo:喜欢的随手点个star 哈~
https://github.com/mcxtzhang/demos/tree/master/itemdecorationdemo
master分支为上部残篇,sidebar分支为上下合璧完整篇。
七 总结
本文是我第一次用markdown编写博客,感觉一个字爽。
recyclerview相关的各个类,个个是宝,每一次探索都觉得如获至宝, 感觉利用itemdecoration可以干很多事,可惜itemdecoration貌似不能接受到用户的点击事件~要不我右侧导航栏都想用itemdecoration实现了。
关于可以add多个itemdecoration这一点,想了一下,觉得很精妙,这是一种很好的设计思想,多个itemdecoration各司其职,如本文,采用官方itemdecoration作分割线,自己又写一个itemdecoration作分类title和分类title相关的悬停title。用时根据需要,选择任意数量的“装饰品”itemdecoration,来丰富你的recyclerview。可能我的low常规思想还是一个xxx类,使用时如果扩充功能,需要extends and code~但这样不同的功能就太耦合了,不利于复用。毕竟 “组合大于继承”。
这一周亚历山大,工作上的事很多,下篇原本打算明天写的,可能要挪到周末了。
心急的朋友可以去我的github上 sidebar分支看,就是在本文的基础上,组合一个侧边栏自定义view,然后利用tinypinyin(https://github.com/promeg/tinypinyin),取数据源的拼音,然后利用拼音顺序排序数据源,set给adapter,set给侧边栏,监听侧边栏的item切换,在回调方法里,调用recyclerview的scrolltopositionwithoffset(int position, int offset) 方法,滑动recyclerview到指定位置~。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。