引言

我们对RecyclerView的使用不能仅仅停留在表面,可以探索一些高级的用法——自定义,不过要实现这些的前提是必须对RecycleView的源码流程有所了解。今天小编就略讲一下,主要是和今天的主体有关的内容。我们今天就来实现如下的吸顶效果。

通过绘制实现RecyclerView的吸顶效果

ItemDecoration

ItemDecoration允许应用给具体的View添加具体的图画或者layout的偏移,对于绘制View之间的分割线,视觉分组边界等等是非常有用的。

当我们调用addItemDecoration()方法添加decoration的时候,RecyclerView就会调用该类的onDraw方法区绘制分割线,也就是说:分割线是绘制出来的。

RecyclerView.ItemDecoration,该类为抽象类,官方目前只提供了一个实现类DividerItemDecoration。

并且只有LinearLayoutManager能够使用

image-20220719205304144

吸顶效果

要实现这样的吸顶效果,首先,就需要了解一下RecyclerView.ItemDecoration的绘制流程。

1.onDraw

在绘制每个itemView之前绘制,上图中的每个跟着滑动的头部就是在此处绘制

2.onDrawOver

在绘制了每个ItemView之后绘制,上图中固定的顶部在此处绘制

3.getItemOffsets

设置偏移值,可以在绘制itemView时设置偏移值,其实上demo中的每个头部(红色区域)就是在绘制itemView预留了部分空间用于绘制头部。

image-20220719210625576

也就是说绘制流程是:onDraw()->ItemView->onDrawOver

具体实现

  • 在Adapter中创建两个方法
    • 1.获取组名
    • 2.判断是否是组的头部
      • 首先,position=0一定是组的头部
      • 其次,可以通过前后两次组名判断当前item是不是组的头部
//是否是 组的第一个
public Boolean isGroupHeader(int position){
if (position==0){
return true;
}else{
String currentName = getGroupName(position);
String preGroupName = getGroupName(position-1);
if (preGroupName.equals(currentName)){
return false;
}else {
return true;
}
}
}

public String getGroupName(int position){
return modelList.get(position).getGroupName();
}
  • 自定义分割线

    @Override
    public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
    super.onDraw(c, parent, state);
    if (parent.getAdapter() instanceof ModelAdapter){
    ModelAdapter adapter = (ModelAdapter) parent.getAdapter();
    int count = parent.getChildCount();
    int left = parent.getPaddingLeft();
    int right = parent.getWidth() - parent.getPaddingRight();
    for (int i = 0; i < count; i++) {
    //获取对应的View
    View view = parent.getChildAt(i);
    //获取View的布局位置
    int position = parent.getChildLayoutPosition(view);
    //判断是不是头部
    boolean isGroupHeader = adapter.isGroupHeader(position);
    if (isGroupHeader && view.getTop() - groupHeaderHeight - parent.getPaddingTop() >= 0){
    c.drawRect(left,view.getTop()-groupHeaderHeight,right,view.getTop(),headPaint);
    String groupName = adapter.getGroupName(position);
    textPaint.getTextBounds(groupName, 0, groupName.length(), textRect);
    c.drawText(groupName,left+20,view.getTop()-groupHeaderHeight/2+textRect.height()/2,textPaint);

    }else if (view.getTop() - groupHeaderHeight - parent.getPaddingTop() >= 0){
    //分割线
    c.drawRect(left,view.getTop()-4,right,view.getTop(),headPaint);
    }
    }
    }
    }
    @Override
    public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
    super.onDrawOver(c, parent, state);
    if (parent.getAdapter() instanceof ModelAdapter){
    ModelAdapter adapter = (ModelAdapter) parent.getAdapter();
    //返回可见区域的第一个item的position
    int position = ((LinearLayoutManager)parent.getLayoutManager()).findFirstVisibleItemPosition();
    //获取对应的position的View
    View itemView = parent.findViewHolderForAdapterPosition(position).itemView;
    int left = parent.getPaddingLeft();
    int right = parent.getWidth() - parent.getPaddingRight();
    int top = parent.getPaddingTop();
    //当下一个是组的头部时
    boolean isGroupHeader = adapter.isGroupHeader(position+1);
    String groupName = adapter.getGroupName(position);
    textPaint.getTextBounds(groupName,0,groupName.length(),textRect);
    if (isGroupHeader){
    //对顶部进行缩小效果,在视觉上是被下一个组的顶部推进去的
    int bottom = Math.min(groupHeaderHeight,itemView.getBottom()-parent.getPaddingTop());
    c.drawRect(left,top,right,top+bottom,headPaint);
    c.clipRect(left,top,right,top+bottom);
    c.drawText(groupName,left+20,top+bottom-groupHeaderHeight/2+textRect.height()/2,textPaint);
    }else {
    c.drawRect(left,top,right,top+groupHeaderHeight,headPaint);
    c.drawText(groupName,left+20,top+groupHeaderHeight/2+textRect.height()/2,textPaint);
    }
    }
    }
    @Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
    super.getItemOffsets(outRect, view, parent, state);
    if (parent.getAdapter() instanceof ModelAdapter){
    ModelAdapter adapter = (ModelAdapter) parent.getAdapter();
    int position = parent.getChildAdapterPosition(view);
    boolean isGroupHeader = adapter.isGroupHeader(position);
    //判断itemView是不是头部
    if (isGroupHeader){
    //如果是头部,预留更大的地方
    outRect.set(0,groupHeaderHeight,0,0);
    }else {
    outRect.set(0,4,0,0);
    }
    }
    }

总结

其实这部分的重点就是自定义分割线(ItemDecoration),弄清绘制顺序以及设置偏移值的作用就简单多了。当然,难点(也不是难点)就是弄清除尺寸参数,特别是尺寸变化引起的换组顶部推动效果。

其次就是特别注意有padding的情况,相信大家看出小编在这设置了顶部padding,有padding的时候需要注意尺寸。(不过有设置padding的情况很少)

全部代码