欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2023-11-23 21:04:22
本文介绍了android itemdecoration 实现分组索引列表的示例代码,分享给大家。具体如下: 先来看看效果: 我们要实现的效果主要涉及三个部分...

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

先来看看效果:

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

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设置偏移量,具体怎么设置呢,咱们看图说话:

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

图中左边的是原始recyclerview列表,右边是设置了itemview偏移量的列表,其实相当于在itemview外部添加了一个矩形区域
其中left、top、right、bottom就是itemview在四个方向的偏移量,对应的设置代码如下:

outrect.set(left, top, right, bottom)

在我们的分组索引列表中,只需要对itemview设置顶部的偏移量,其它三个偏移量为0即可。这样就可以在itemview顶部预留出一定高度的区域,如下图:

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

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......

看下效果:

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

当然不止于此,更多的效果等待着机智的你去创造。

更多代码细节及用法可参考:https://github.com/othershe/groupindexlib

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