Android ItemDecoration 实现分组索引列表的示例代码
本文介绍了android itemdecoration 实现分组索引列表的示例代码,分享给大家。具体如下:
先来看看效果:
我们要实现的效果主要涉及三个部分:
- 分组 groupheader
- 分割线
- sidebar
前两个部分涉及到一个itemdecoration类,也是我们接下来的重点,该类是recyclerview的一个抽象静态内部类,主要作用就是给recyclerview的itemview绘制额外的装饰效果,例如给recyclerview添加分割线。
使用itemdecoration时需要继承该类,根据需求可以重写如下三个方法,其它的方法已经deprecated了:
public class groupheaderitemdecoration extends recyclerview.itemdecoration { @override public void getitemoffsets(rect outrect, view view, recyclerview parent, recyclerview.state state) { super.getitemoffsets(outrect, view, parent, state); } @override public void ondraw(canvas c, recyclerview parent, recyclerview.state state) { super.ondraw(c, parent, state); } @override public void ondrawover(canvas c, recyclerview parent, recyclerview.state state) { super.ondrawover(c, parent, state); } }
然后将其添加到recyclerview中:
recyclerview.additemdecoration(new groupheaderitemdecoration())
了解这个三个方法的作用,这样才能更好的实现我们想要的功能:
1、getitemoffsets()
给指定的itemview设置偏移量,具体怎么设置呢,咱们看图说话:
图中左边的是原始recyclerview列表,右边是设置了itemview偏移量的列表,其实相当于在itemview外部添加了一个矩形区域
其中left、top、right、bottom就是itemview在四个方向的偏移量,对应的设置代码如下:
outrect.set(left, top, right, bottom)
在我们的分组索引列表中,只需要对itemview设置顶部的偏移量,其它三个偏移量为0即可。这样就可以在itemview顶部预留出一定高度的区域,如下图:
2、ondraw()
在getitemoffsets()方法中,我们设置了偏移量,进而得到了对应的偏移区域,接下来在ondraw()中就可以给itemview绘制装饰效果了,所以我们在该方法中将分组索引列表中的groupheader的内容绘制在itemview顶部偏移区域里。也就是绘制前边 gif 图里的a、b、c... groupheader,虽然看起来像一个个独立的itemview,但并不是的哦!
注意该绘制操作会在itemview的ondraw()前完成的!
3、ondrawover()
该方法同样也是用来绘制的,但是它在itemdecoration的ondraw()方法和itemview的ondraw()完成后才执行。所以其绘制的内容会遮挡在recyclerview上,因此我们可以在该方法中绘制分组索引列表中悬浮的groupheader,也就是在列表顶部随着列表滚动切换的groupheader。
一、分组groupheader
三个方法的作用已经解释完了,接下来就是代码实现我们的效果了:
首先保证recyclerview的数据源已经按照某种规律进行了分组排序,具体什么规律你说了算,我们例子中按照数据源中指定字段的值的首字母升序排列,也就是常见通讯录的排序方式。然后在每个data中保存需要在groupheader上显示的内容,可以使用tag字段,我们这里保存的是对应的首字母。这里没必要将整个数据源设置到itemdecoration里边,所以我们只需要提取排序后数据源的tag保存到列表中,然后设置到itemdecoration里边,后边的操作就依赖设置的数据源了,根据tag的异同来决定是否绘制groupheader等。
上边已经分析了,groupheader只在列表中每组数据对应的第一个itemview顶部显示,只需要对itemview设置顶部的偏移量即可:
public class groupheaderitemdecoration extends recyclerview.itemdecoration { @override public void getitemoffsets(rect outrect, view view, recyclerview parent, recyclerview.state state) { super.getitemoffsets(outrect, view, parent, state); recyclerview.layoutmanager manager = parent.getlayoutmanager(); //只处理线性垂直类型的列表 if ((manager instanceof linearlayoutmanager) && linearlayoutmanager.vertical != ((linearlayoutmanager) manager).getorientation()) { return; } int position = parent.getchildadapterposition(view); //itemview的position==0 或者 当前itemview的data的tag和上一个itemview的不相等,则为当前itemview设置top 偏移量 if (!utils.listisempty(tags) && (position == 0 || !tags.get(position).equals(tags.get(position - 1)))) { outrect.set(0, groupheaderheight, 0, 0); } } @override public void ondraw(canvas c, recyclerview parent, recyclerview.state state) { super.ondraw(c, parent, state); } @override public void ondrawover(canvas c, recyclerview parent, recyclerview.state state) { super.ondrawover(c, parent, state); } }
其中tags就是我们设置到itemdecoration的数据源,是一个string集合。groupheaderheight就是itemview的顶部偏移量。
之后就是在itemview的顶部偏移区域绘制groupheader了:
public class groupheaderitemdecoration extends recyclerview.itemdecoration { @override public void getitemoffsets(rect outrect, view view, recyclerview parent, recyclerview.state state) { super.getitemoffsets(outrect, view, parent, state); } @override public void ondraw(canvas c, recyclerview parent, recyclerview.state state) { super.ondraw(c, parent, state); for (int i = 0; i < parent.getchildcount(); i++) { view view = parent.getchildat(i); int position = parent.getchildadapterposition(view); string tag = tags.get(position); //和getitemoffsets()里的条件判断类似,开始绘制分组的groupheader if (!utils.listisempty(tags) && (position == 0 || !tag.equals(tags.get(position - 1)))) { drawgroupheader(c, parent, view, tag); } } } @override public void ondrawover(canvas c, recyclerview parent, recyclerview.state state) { super.ondrawover(c, parent, state); } private void drawgroupheader(canvas c, recyclerview parent, view view, string tag) { recyclerview.layoutparams params = (recyclerview.layoutparams) view.getlayoutparams(); int left = parent.getpaddingleft(); int right = parent.getwidth() - parent.getpaddingright(); int bottom = view.gettop() - params.topmargin; int top = bottom - groupheaderheight; c.drawrect(left, top, right, bottom, mpaint); int x = left + groupheaderleftpadding; int y = top + (groupheaderheight + utils.gettextheight(mtextpaint, tag)) / 2; c.drawtext(tag, x, y, mtextpaint); } }
绘制groupheader就是canvasc操作,先绘制一个矩形框,再绘制相应的文字,当然绘制图片也是没问题的,其中groupheaderleftpadding是个可配置字段,代表绘制的文字或图片到列表左边沿的距离,也可以理解为groupheader的左padding。
最后就是悬浮在顶部的groupheader绘制了:
public class groupheaderitemdecoration extends recyclerview.itemdecoration { @override public void getitemoffsets(rect outrect, view view, recyclerview parent, recyclerview.state state) { super.getitemoffsets(outrect, view, parent, state); } @override public void ondraw(canvas c, recyclerview parent, recyclerview.state state) { super.ondraw(c, parent, state); } @override public void ondrawover(canvas c, recyclerview parent, recyclerview.state state) { super.ondrawover(c, parent, state); if (!show) { return; } //列表第一个可见的itemview位置 int position = ((linearlayoutmanager) (parent.getlayoutmanager())).findfirstvisibleitemposition(); string tag = tags.get(position); view view = parent.findviewholderforadapterposition(position).itemview; //当前itemview的data的tag和下一个itemview的不相等,则代表将要重新绘制悬停的groupheader boolean flag = false; if (!utils.listisempty(tags) && (position + 1) < tags.size() && !tag.equals(tags.get(position + 1))) { //如果第一个可见itemview的底部坐标小于groupheaderheight,则执行canvas向上位移操作 if (view.getbottom() <= groupheaderheight) { c.save(); flag = true; c.translate(0, view.getheight() + view.gettop() - groupheaderheight); } } drawsuspensiongroupheader(c, parent, tag); if (flag) { c.restore(); } } private void drawsuspensiongroupheader(canvas c, recyclerview parent, string tag) { int left = parent.getpaddingleft(); int right = parent.getwidth() - parent.getpaddingright(); int bottom = groupheaderheight; int top = 0; c.drawrect(left, top, right, bottom, mpaint); int x = left + groupheaderleftpadding; int y = top + (groupheaderheight + utils.gettextheight(mtextpaint, tag)) / 2; c.drawtext(tag, x, y, mtextpaint); } }
绘制操作和ondraw中的类似,gif 中有一个悬浮groupheader上移的动画,就是通过canvas位移来实现的,注意在canvas位移的前后进行save()和restore()操作。
我们给groupheaderitemdecoration提供了设置groupheader左padding、高度、背景色、文字颜色、尺寸、以及是否显示顶部悬浮groupheader的方法,方便使用。
关于绘制操作需要注意的是,groupheader所在的偏移区域和itemview是相互独立的,不要把groupheader当做itemview的一部分哦。到这里groupheader的功能就实现了,只需要将groupheaderitemdecoration添加到recyclerview即可。
至于如何通过layout或者view来实现groupheader,做过一些尝试,效果都不理想,期待大家的好想法哦!
这里先用一个接口,对外提供自定义绘制groupheader的方法:
public interface ondrawitemdecorationlistener { /** * 绘制groupheader * @param c * @param paint 绘制groupheader区域的paint * @param textpaint 绘制文字的paint * @param params 共四个值left、top、right、bottom 代表groupheader所在区域的四个坐标值 * @param position 原始数据源中的position */ void ondrawgroupheader(canvas c, paint paint, textpaint textpaint, int[] params, int position); /** * 绘制悬浮在列表顶部的groupheader */ void ondrawsuspensiongroupheader(canvas c, paint paint, textpaint textpaint, int[] params, int position); }
二、分割线
现在recyclerview还差一个分割线,当前最笨的办法可以在itemview的布局文件中设置,既然系统都提供了itemdecoration,那用它来优雅的实现为何不可呢,我们只需要给列表中每组数据除了最后一项数据对应的itemview之外的添加分割线即可,也就是不给每组数据对应的最后一个itemview添加分割线。很简单,直接上核心代码:
public class divideitemdecoration extends recyclerview.itemdecoration { @override public void getitemoffsets(rect outrect, view view, recyclerview parent, recyclerview.state state) { super.getitemoffsets(outrect, view, parent, state); recyclerview.layoutmanager manager = parent.getlayoutmanager(); //只处理线性垂直类型的列表 if ((manager instanceof linearlayoutmanager) && linearlayoutmanager.vertical != ((linearlayoutmanager) manager).getorientation()) { return; } int position = parent.getchildadapterposition(view); if (!utils.listisempty(tags) && (position + 1) < tags.size() && tags.get(position).equals(tags.get(position + 1))) { //当前itemview的data的tag和下一个itemview的不相等,则为当前itemview设置bottom 偏移量 outrect.set(0, 0, 0, divideheight); } } @override public void ondraw(canvas c, recyclerview parent, recyclerview.state state) { super.ondraw(c, parent, state); for (int i = 0; i < parent.getchildcount(); i++) { view view = parent.getchildat(i); int position = parent.getchildadapterposition(view); //和getitemoffsets()里的条件判断类似 if (!utils.listisempty(tags) && (position + 1) < tags.size() && tags.get(position).equals(tags.get(position + 1))) { drawdivide(c, parent, view); } } } @override public void ondrawover(canvas c, recyclerview parent, recyclerview.state state) { super.ondrawover(c, parent, state); } private void drawdivide(canvas c, recyclerview parent, view view) { recyclerview.layoutparams params = (recyclerview.layoutparams) view.getlayoutparams(); int left = parent.getpaddingleft(); int right = parent.getwidth(); int top = view.getbottom() + params.bottommargin; int bottom = top + divideheight; c.drawrect(left, top, right, bottom, mpaint); } }
三、sidebar
sidebar就是 gif 图右边的垂直字符条,是一个自定义view。手指触摸选中一个字符,则列表会滚动到对应的分组头部位置。实现起来也蛮简单的,核心代码如下:
public class sidebar extends view { @override protected void onmeasure(int widthmeasurespec, int heightmeasurespec) { super.onmeasure(widthmeasurespec, heightmeasurespec); int widthsize = measurespec.getsize(widthmeasurespec); int widthmode = measurespec.getmode(widthmeasurespec); int heightsize = measurespec.getsize(heightmeasurespec); int heightmode = measurespec.getmode(heightmeasurespec); //重新计算sidebar宽高 if (heightmode == measurespec.at_most || widthmode == measurespec.at_most) { getmaxtextsize(); if (heightmode == measurespec.at_most) { heightsize = (maxheight + 15) * indexarray.length; } if (widthmode == measurespec.at_most) { widthsize = maxwidth + 10; } } setmeasureddimension(widthsize, heightsize); } @override protected void ondraw(canvas canvas) { for (int i = 0; i < indexarray.length; i++) { string index = indexarray[i]; float x = (mwidth - mtextpaint.measuretext(index)) / 2; float y = mmargintop + mheight * i + (mheight + utils.gettextheight(mtextpaint, index)) / 2; //绘制字符 canvas.drawtext(index, x, y, mtextpaint); } } @override public boolean ontouchevent(motionevent event) { switch (event.getaction()) { case motionevent.action_down: case motionevent.action_move: // 选中字符的下标 int pos = (int) ((event.gety() - mmargintop) / mheight); if (pos >= 0 && pos < indexarray.length) { setbackgroundcolor(touch_color); if (onsidebartouchlistener != null) { for (int i = 0; i < tags.size(); i++) { if (indexarray[pos].equals(tags.get(i))) { onsidebartouchlistener.ontouch(indexarray[pos], i); break; } else { onsidebartouchlistener.ontouch(indexarray[pos], -1); } } } } break; case motionevent.action_up: case motionevent.action_cancel: setbackgroundcolor(untouch_color); if (onsidebartouchlistener != null) { onsidebartouchlistener.ontouchend(); } break; } return true; } }
在onmeasure()方法里,如果sidebar的宽、高测量模式为measurespec.at_most则重新计算sidebar的宽、高。ondraw()方法则是遍历索引数组,并绘制字符索引。在ontouchevent()方法里,我们根据手指在sidebar上触摸坐标点的y值,计算出触摸的相应字符,以便在onsidebartouchlistener接口进行后续操作,例如列表的跟随滚动等等。
四、实例
前边已经完成了三大核心功能,最后来愉快的使用下吧:
public class mainactivity extends appcompatactivity { @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_main); recyclerview recyclerview = (recyclerview) findviewbyid(r.id.list); sidebar sidebar = (sidebar) findviewbyid(r.id.side_bar); final textview tip = (textview) findviewbyid(r.id.tip); final list<itemdata> datas = new arraylist<>(); itemdata data = new itemdata("北京"); datas.add(data); itemdata data1 = new itemdata("上海"); datas.add(data1); itemdata data2 = new itemdata("广州"); datas.add(data2); . . . itemdata data34 = new itemdata("hello china"); datas.add(data34); itemdata data35 = new itemdata("宁波"); datas.add(data35); sorthelper<itemdata> sorthelper = new sorthelper<itemdata>() { @override public string sortfield(itemdata data) { return data.gettitle(); } }; sorthelper.sortbyletter(datas);//将数据源按指定字段首字母排序 list<string> tags = sorthelper.gettags(datas);//提取已排序数据源的tag值 myadapter adapter = new myadapter(this, datas, false); final linearlayoutmanager layoutmanager = new linearlayoutmanager(this); layoutmanager.setorientation(linearlayoutmanager.vertical); recyclerview.setlayoutmanager(layoutmanager); //添加分割线 recyclerview.additemdecoration(new divideitemdecoration().settags(tags)); //添加groupheader recyclerview.additemdecoration(new groupheaderitemdecoration(this) .settags(tags)//设置tag集合 .setgroupheaderheight(30)//设置groupheader高度 .setgroupheaderleftpadding(20));//设置groupheader 左padding recyclerview.setadapter(adapter); sidebar.setonsidebartouchlistener(tags, new onsidebartouchlistener() { @override public void ontouch(string text, int position) { tip.setvisibility(view.visible); tip.settext(text); if ("↑".equals(text)) { layoutmanager.scrolltopositionwithoffset(0, 0); return; } //滚动列表到指定位置 if (position != -1) { layoutmanager.scrolltopositionwithoffset(position, 0); } } @override public void ontouchend() { tip.setvisibility(view.gone); } }); } }
这也就是文章开头的 gif 效果。如果需要自定义itemview的绘制可以这样写:
recyclerview.additemdecoration(new groupheaderitemdecoration(this) .settags(tags) .setgroupheaderheight(30) .setgroupheaderleftpadding(20) .setondrawitemdecorationlistener(new ondrawitemdecorationlistener() { @override public void ondrawgroupheader(canvas c, paint paint, textpaint textpaint, int[] params, int position) { c.drawrect(params[0], params[1], params[2], params[3], paint); int x = params[0] + utils.dip2px(context, 20); int y = params[1] + (utils.dip2px(context, 30) + utils.gettextheight(textpaint, tags.get(position))) / 2; bitmap icon = bitmapfactory.decoderesource(getresources(), r.mipmap.ic_launcher, null); bitmap icon1 = bitmap.createscaledbitmap(icon, utils.dip2px(context, 20), utils.dip2px(context, 20), true); c.drawbitmap(icon1, x, params[1] + utils.dip2px(context, 5), paint); c.drawtext(tags.get(position), x + utils.dip2px(context, 25), y, textpaint); } @override public void ondrawsuspensiongroupheader(canvas c, paint paint, textpaint textpaint, int[] params, int position) { c.drawrect(params[0], params[1], params[2], params[3], paint); int x = params[0] + utils.dip2px(context, 20); int y = params[1] + (utils.dip2px(context, 30) + utils.gettextheight(textpaint, tags.get(position))) / 2; bitmap icon = bitmapfactory.decoderesource(getresources(), r.mipmap.ic_launcher, null); bitmap icon1 = bitmap.createscaledbitmap(icon, utils.dip2px(context, 20), utils.dip2px(context, 20), true); c.drawbitmap(icon1, x, params[1] + utils.dip2px(context, 5), paint); c.drawtext(tags.get(position), x + utils.dip2px(context, 25), y, textpaint); } }) );
坐标计算有点复杂了......0_o......
看下效果:
当然不止于此,更多的效果等待着机智的你去创造。
更多代码细节及用法可参考:https://github.com/othershe/groupindexlib
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。