当前位置: 移动技术网 > IT编程>移动开发>Android > Android ItemDecoration 实现分组索引列表的示例代码

Android ItemDecoration 实现分组索引列表的示例代码

2019年07月24日  | 移动技术网IT编程  | 我要评论

刘秋实,王震之子王军简历,馨炫

本文介绍了android itemdecoration 实现分组索引列表的示例代码,分享给大家。具体如下:

先来看看效果:


我们要实现的效果主要涉及三个部分:

  1. 分组 groupheader
  2. 分割线
  3. 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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持移动技术网。

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网