查看原文
其他

把RecyclerView撸出花儿来,自定义无限循环的LayoutManager!

d袋鼠b 承香墨影 2022-09-09


d袋鼠b | 作者

承香墨影 | 校对

https://juejin.cn/post/6909363022980972552 | 原文

大家好,这里是承香墨影!

在日常开发的过程中,同学们都遇到过需要 RecyclerView 无限循环的需求,但是在官方提供的几种 LayoutManager 中并未支持无限循环。

遇到此种问题,通常的解决方案是:

  1. adapter 返回 Integer.MAX_VALUE,并让 RecyclerView 滑动到某个足够大的位置;
  2. 选择自定义 LayoutManager,实现循环的 RecyclerView;

自定义 LayoutManager 的难度较高,本文将带大家一起实现这个自定义 LayoutManager,效果如下图所示。

同时,在熟悉了在自定义 LayoutManager 后,还可以根据需要调整 RecyclerView 的展示效果。


与自定义 ViewGroup 类似,自定义 LayoutManager 所要做的就是 ItemView 的「添加 (add)」、「测量(measure)」、「布局 (layout)」。

但是与自定义 ViewGroup 相比,LayoutManager 多了一个「回收 (recycle)」工作。

在自定义 LayoutManager 之前,需要对其提供的「测量」、「布局」以及「回收」相关的 API 进行了解。

measure

首先介绍测量方法,与自定义 ViewGroup 类似,测量通常是固定的逻辑,不需要自己实现,开发者无需复写测量方法,只需要在布局之前调用测量函数来获取将要布局的「View 的宽度」即可。

LayoutManager 提供了两个用来测量子 View 的方法:

public void measureChild(@NonNull View child, int widthUsed, int heightUsed)

public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed)

测量完成后, 便可以使用 getMeasuredWidth()getMeasuredHeight() 直接获取 View 的宽高,但是在自定义 LayoutManager 中需要考虑 ItemDecoration,所以需要通过如下两个 API 获取测量后的 View 大小:

public int getDecoratedMeasuredWidth(@NonNull View child) {
  final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets;
  return child.getMeasuredWidth() + insets.left + insets.right;
}

public int getDecoratedMeasuredHeight(@NonNull View child) {
  final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets;
  return child.getMeasuredHeight() + insets.top + insets.bottom;
}

layout

然后介绍 layout 方法,和自定义 ViewGroup 一样, LayoutManager 完成 ItemView 的测量后就是布局了。

在 LayoutManager 中,并非靠直接调用 ItemView 的 layout 函数进行子 View 的布局,而是使用 layoutDecoratedlayoutDecoratedWithMargins, 两者的区别是后者考虑了 Margins:

public void layoutDecorated(@NonNull View child, int left, int top, int right, int bottom) {
  final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets;
  child.layout(left + insets.left, top + insets.top, right - insets.right,
              bottom - insets.bottom);
}

public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, int right,
                int bottom) 
{
  final LayoutParams lp = (LayoutParams) child.getLayoutParams();
  final Rect insets = lp.mDecorInsets;
  child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin,
          right - insets.right - lp.rightMargin,
          bottom - insets.bottom - lp.bottomMargin);
}

recycle

回收是 RecyclerView 的灵魂,也是 RecyclerView 与普通 ViewGroup 的区别。众所周知,RecyclerView 中含有四类缓存,在布局过程中它们各自有各自的用途:

  1. AttachedScrap: 存放可见、不需要重新绑定的 ViewHolder;
  2. CachedViews: 存放不可见、不需要重新绑定的 ViewHoler;
  3. ViewCacheExtension: 自定义缓存(存放不可见、不需要重新绑定);
  4. RecyclerPool: 存放不可见、需要重新绑定的 ViewHolder;

在 LayoutManager 中提供了多个回收方法:

public void removeAndRecycleView(@NonNull View child, @NonNull Recycler recycler) {
  removeView(child);
  recycler.recycleView(child);
}

public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) {
  final View view = getChildAt(index);
  removeViewAt(index);
  recycler.recycleView(view);
}

1、实现抽抽象方法,并让 RecyclerView 可横向滑动

public class RepeatLayoutManager extends RecyclerView.LayoutManager {
  @Override
  public RecyclerView.LayoutParams generateDefaultLayoutParams() {
    return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
            ViewGroup.LayoutParams.WRAP_CONTENT);
  }

  @Override
  public boolean canScrollHorizontally() {
    return true;
  }
}

2、定义初始布局

onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) 方法中对 ItemView 进行添加、测量、布局。

具体步骤如下:

  • 使用recycler.getViewForPosition(int pos)从缓存中获取子 View;
  • 当可布局区域有多余的空间时, 通过 addView(View view) 将对子 View 进行添加,通过在 RecyclerView 中添加子 View, 并对子 View 进行测量与布局,直至子 View 超出 RecyclerView 的可布局宽度;
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
  if (getItemCount() <= 0) {
    return;
  }
  if (state.isPreLayout()) {
    return;
  }

  detachAndScrapAttachedViews(recycler);
  int itemLeft = getPaddingLeft();
  for (int i = 0; ; i++) {
    if (itemLeft >= getWidth() - getPaddingRight()) {
      break;
    }
    View itemView = recycler.getViewForPosition(i % getItemCount());

    addView(itemView);

    measureChildWithMargins(itemView, 00);

    int right = itemLeft + getDecoratedMeasuredWidth(itemView);
    int top = getPaddingTop();
    int bottom = top + getDecoratedMeasuredHeight(itemView) - getPaddingBottom();

    layoutDecorated(itemView, itemLeft, top, right, bottom);
    itemLeft = right;
  }
}

3、滑动与填充

offsetChildrenHorizontal(int x) 用作对 RecyclerView 中的子 View 进行整体左右移动。为了在滑动 RecyclerView 时有子 View 移动的效果,需要复写 scrollHorizontallyBy() 函数,并在其中调用 offsetChildrenHorizontal(int x)

当左滑后子 View 被左移动时,RecyclerView 的右侧会出现可见的未填充区域,这时需要在 RecyclerView 右侧添加并布局好新的子 View,直到没有可见的未填充区域为止。

同样,在右滑后需要对左侧的未填充区域进行填充。

具体代码如下:

@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
  fill(recycler, dx > 0);
  offsetChildrenHorizontal(-dx);
  return dx;
}


private void fill(RecyclerView.Recycler recycler, boolean fillEnd) {
  if (getChildCount() == 0return;
  if (fillEnd) {

    View anchorView = getChildAt(getChildCount() - 1);
    int anchorPosition = getPosition(anchorView);
    for (; anchorView.getRight() < getWidth() - getPaddingRight(); ) {
      int position = (anchorPosition + 1) % getItemCount();
      if (position < 0) position += getItemCount();

      View scrapItem = recycler.getViewForPosition(position);
      addView(scrapItem);
      measureChildWithMargins(scrapItem, 00);

      int left = anchorView.getRight();
      int top = getPaddingTop();
      int right = left + getDecoratedMeasuredWidth(scrapItem);
      int bottom = top + getDecoratedMeasuredHeight(scrapItem) - getPaddingBottom();
      layoutDecorated(scrapItem, left, top, right, bottom);
      anchorView = scrapItem;
    }
  } else {

    View anchorView = getChildAt(0);
    int anchorPosition = getPosition(anchorView);
    for (; anchorView.getLeft() > getPaddingLeft(); ) {
      int position = (anchorPosition - 1) % getItemCount();
      if (position < 0) position += getItemCount();

      View scrapItem = recycler.getViewForPosition(position);
      addView(scrapItem, 0);
      measureChildWithMargins(scrapItem, 00);
      int right = anchorView.getLeft();
      int top = getPaddingTop();
      int left = right - getDecoratedMeasuredWidth(scrapItem);
      int bottom = top + getDecoratedMeasuredHeight(scrapItem) - getPaddingBottom();
      layoutDecorated(scrapItem, left, top,
              right, bottom);
      anchorView = scrapItem;
    }
  }
  return;
}

前面讲到,当对 RecyclerView 进行滑动时,需要对可见的未填充区域进行填充。然而一直填充不做回收 Item,那就和普通的 ViewGroup 没有太多的区别了。

在 RecyclerView 中,需要在滑动、填充可见区域的同时,对不可见区域的子 View 进行回收,这样才能体现出 RecyclerView 的优势。

回收的方向与填充的方向恰好相反。那回收的代码具体如何实现呢?

代码如下:

@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
  fill(recycler, dx > 0);
  offsetChildrenHorizontal(-dx);
  recyclerChildView(dx > 0, recycler);
  return dx;
}


private void recyclerChildView(boolean fillEnd, RecyclerView.Recycler recycler) {
  if (fillEnd) {

    for (int i = 0; ; i++) {
      View view = getChildAt(i);
      boolean needRecycler = view != null && view.getRight() < getPaddingLeft();
      if (needRecycler) {
        removeAndRecycleView(view, recycler);
      } else {
        return;
      }
    }
  } else {

    for (int i = getChildCount() - 1; ; i--) {
        View view = getChildAt(i);
        boolean needRecycler = view != null && view.getLeft() > getWidth() - getPaddingRight();
        if (needRecycler) {
          removeAndRecycleView(view, recycler);
        } else {
          return;
        }
    }
  }
}

添加依赖

implementation 'cn.student0.manager:repeatmanager:1.0.3'

在代码中使用

RecyclerView recyclerView = findViewById(R.id.rv_demo);
recyclerView.setAdapter(new DemoAdapter());
recyclerView.setLayoutManager(new RepeatLayoutManager

到此,无限循环的 LayoutManager 的实现已经完成。文章的不足还请指出,谢谢大家。

-- End --

Github 地址:https://github.com/jiarWang/RepeatLayoutManager 欢迎大家 Star。

reference:

  • https://blog.csdn.net/u011387817/article/details/81875021
  • https://blog.csdn.net/zxt0601/article/details/52948009

本文对你有帮助吗?留言、转发、点好看是最大的支持,谢谢!

推荐阅读:

面试问Handler内存泄露的场景,别就只知道静态内部类&弱引用!

Flutter 利用 FFI,绕过 Android JNI 直接调用 C++ 层!

已开源!Flutter 基于分帧渲染的流畅度优化组件 Keframe


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存