方法一:使用两个嵌套的 RecyclerViews
Github sample
优点:
- 视图被回收(即良好的性能)
- 半无缝滚动(更新 3 和 4 后)
缺点:
- 当到达内部远端项目时,在从内部滚动到外部滚动的过渡期间以编程方式传播的滚动不像手势那样平滑/自然。
- 复杂的代码。
好吧,我不会解决垂直嵌套RecyclerViews 的性能问题;但请注意:
- 内部
RecyclerView可能失去了回收视图的能力;因为显示的外部 recyclerView 行应该完全加载它们的项目。 (谢天谢地,根据以下更新 1,这不是一个正确的假设)
- 我在
ViewHolder 而不是 onBindViewHolder 中声明了一个适配器实例,通过在每次回收视图时不为内部 RecyclerView 创建新的适配器实例来获得更好的性能。
演示应用程序将一年中的月份表示为外部RecyclerView,将每个月的天数表示为内部RecyclerView。
外部 RecyclerView 注册 OnScrollListener 每次滚动时,我们都会对内部 RV 进行检查:
- 如果外部向上滚动:检查内部第一项是否显示。
- 如果外部向下滚动:检查内部最后一项是否显示。
outerRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (dy > 0) //scrolled to BOTTOM
outerAdapter.isOuterScrollingDown(true, dy);
else if (dy < 0) //scrolled to TOP
outerAdapter.isOuterScrollingDown(false, dy);
}
});
在外部适配器中:
public void isOuterScrollingDown(boolean scrollDown, int value) {
if (scrollDown) {
boolean isLastItemShown = currentLastItem == mMonths.get(currentPosition).dayCount;
if (!isLastItemShown) onScrollListener.onScroll(-value);
enableOuterScroll(isLastItemShown);
} else {
boolean isFirstItemShown = currentFirstItem == 1;
if (!isFirstItemShown) onScrollListener.onScroll(-value);
enableOuterScroll(isFirstItemShown);
}
if (currentRV != null)
currentRV.smoothScrollBy(0, 10 * value);
}
如果相关项目未显示,那么我们决定禁用外部 RV 滚动。这由具有回调的侦听器处理,该回调接受传递给自定义 LinearLayoutManager 类的布尔值到外部 RV。
同样为了重新启用外部 RV 的滚动:内部 RecyclerView 注册 OnScrollListener 以检查是否显示内部第一个/最后一个项目。
innerRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
if (!recyclerView.canScrollVertically(1) // Is it not possible to scroll more to bottom (i.e. Last item shown)
&& newState == RecyclerView.SCROLL_STATE_IDLE) {
enableOuterScroll(true);
} else if (!recyclerView.canScrollVertically(-1) // Is it possible to scroll more to top (i.e. First item shown)
&& newState == RecyclerView.SCROLL_STATE_IDLE) {
enableOuterScroll(true);
}
}
});
仍然存在由于禁用/启用滚动而出现的故障;在下一次滚动之前,我们不能将滚动顺序传递给另一个 RV。这是通过反转初始外部 RV 滚动值来稍微操纵的;并使用currentRV.smoothScrollBy(0, 10 * initialScroll) 向内部使用任意滚动值。我希望有人可以提出任何其他替代方案。
更新 1
- 内部
RecyclerView可能失去了回收视图的能力;因为显示的外部 recyclerView 行应该完全加载它们的项目。
谢天谢地,这不是正确的假设,视图是通过使用 List 跟踪内部适配器中的回收项目列表来回收的,其中包含当前加载的项目:
假设某个月有 1000 天“2 月,因为它总是被压迫:)”,然后向上/向下滚动以注意加载的列表并确保 onViewRecycled() 被调用。
public class InnerRecyclerAdapter extends RecyclerView.Adapter<InnerRecyclerAdapter.InnerViewHolder> {
private final ArrayList<Integer> currentLoadedPositions = new ArrayList<>();
@Override
public void onBindViewHolder(@NonNull InnerViewHolder holder, int position) {
holder.tvDay.setText(String.valueOf(position + 1));
currentLoadedPositions.add(position);
Log.d(LOG_TAG, "onViewRecycled: " + days + " " + currentLoadedPositions);
}
@Override
public void onViewRecycled(@NonNull InnerViewHolder holder) {
super.onViewRecycled(holder);
currentLoadedPositions.remove(Integer.valueOf(holder.getAdapterPosition()));
Log.d(LOG_TAG, "onViewRecycled: " + days + " " + currentLoadedPositions);
}
// Rest of code is trimmed
}
日志:
onViewRecycled: 1000 [0]
onViewRecycled: 1000 [0, 1]
onViewRecycled: 1000 [0, 1, 2]
onViewRecycled: 1000 [0, 1, 2, 3]
onViewRecycled: 1000 [0, 1, 2, 3, 4]
onViewRecycled: 1000 [0, 1, 2, 3, 4, 5]
onViewRecycled: 1000 [0, 1, 2, 3, 4, 5, 6]
onViewRecycled: 1000 [0, 1, 2, 3, 4, 5, 6, 7]
onViewRecycled: 1000 [0, 1, 2, 3, 4, 5, 6, 7, 8]
onViewRecycled: 1000 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
onViewRecycled: 1000 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
onViewRecycled: 1000 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
onViewRecycled: 1000 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
onViewRecycled: 1000 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
onViewRecycled: 1000 [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
onViewRecycled: 1000 [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
onViewRecycled: 1000 [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
onViewRecycled: 1000 [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
onViewRecycled: 1000 [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
onViewRecycled: 1000 [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
onViewRecycled: 1000 [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
onViewRecycled: 1000 [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
onViewRecycled: 1000 [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
onViewRecycled: 1000 [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
onViewRecycled: 1000 [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
onViewRecycled: 1000 [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]
onViewRecycled: 1000 [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]
onViewRecycled: 1000 [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
onViewRecycled: 1000 [9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
onViewRecycled: 1000 [9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
onViewRecycled: 1000 [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
onViewRecycled: 1000 [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21]
onViewRecycled: 1000 [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21]
onViewRecycled: 1000 [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22]
onViewRecycled: 1000 [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22]
onViewRecycled: 1000 [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]
onViewRecycled: 1000 [13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]
onViewRecycled: 1000 [13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
onViewRecycled: 1000 [14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
onViewRecycled: 1000 [14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]
更新 2
仍然存在故障,因为禁用/启用滚动;在下一次滚动之前,我们不能将滚动顺序传递给另一个 RV。这是通过反转初始外部 RV 滚动值来稍微操纵的;并使用currentRV.smoothScrollBy(0, 10 * initialScroll) 向内部使用任意滚动值。我希望有人可以提出任何其他替代方案。
-
使用更大的任意值(如 30)使语法滚动看起来更流畅>>currentRV.smoothScrollBy(0, 30 * initialScroll)
-
并且在不反转滚动的情况下滚动外层滚动,使其在滚动的同一方向上也看起来更自然:
if (scrollDown) {
boolean isLastItemShown = currentLastItem == mMonths.get(currentPosition).dayCount;
if (!isLastItemShown) onScrollListener.onScroll(value);
enableOuterScroll(isLastItemShown);
} else {
boolean isFirstItemShown = currentFirstItem == 1;
if (!isFirstItemShown) onScrollListener.onScroll(value);
enableOuterScroll(isFirstItemShown);
}
更新 3
问题:在从外部到内部RecyclerView 的转换过程中出现故障,因为在决定是否可以滚动内部之前调用外部的onScroll()。
通过使用OnTouchListener 到外部RecyclerView 并覆盖onTouch() 并返回true 来消费事件(这样onScrolled() 就不会被调用),直到我们决定内部可以接管滚动。
private float oldY = -1f;
outerRecyclerView.setOnTouchListener((v, event) -> {
Log.d(LOG_TAG, "onTouch: ");
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
oldY = -1;
break;
case MotionEvent.ACTION_MOVE:
float newY = event.getRawY();
Log.d(LOG_TAG, "onTouch: MOVE " + (oldY - newY));
if (oldY == -1f) {
oldY = newY;
return true; // avoid further listeners (i.e. addOnScrollListener)
} else if (oldY < newY) { // increases means scroll UP
outerAdapter.isOuterScrollingDown(false, (int) (oldY - newY));
oldY = newY;
} else if (oldY > newY) { // decreases means scroll DOWN
outerAdapter.isOuterScrollingDown(true, (int) (oldY - newY));
oldY = newY;
}
break;
}
return false;
});
更新 4
- 每当内部 RV 滚动到其顶部或底部边缘时,启用从内部到外部的滚动过渡
RecyclerView,以便它继续以成比例的速度滚动到外部 RV。
滚动速度的灵感来自this post。通过在触摸的内部 RV 的 OnTouchListener 和 OnScrollListener 中应用第一项/最后一项检查,并在全新的触摸事件中重置内容,即在 MotionEvent.ACTION_DOWN
- 在内部和外部
RecyclerViews 中禁用过度滚动模式
预览:
方法二:在 NestedScrollView 中包裹外层 RecyclerView
Github sample
RecyclerView嵌套滚动的主要问题是它没有实现NestedScrollView实现的NestedScrollingParent3接口;所以RecyclerView 无法处理子视图的嵌套滚动。因此,尝试通过将外部RecyclerView 包裹在NestedScrollView 中来补偿NestedScrollView,并禁用外部RecyclerView 的滚动
优点:
- 简单的代码(您根本不必操作内/外滚动)
- 没有故障
- 无缝滚动
缺点:
- 由于外部
RecyclerView 的视图没有被回收,因此性能低下,因此它们必须全部加载后才能显示在屏幕上。
原因:由于NestedScrollView的性质>>查看(1)、(2)讨论回收问题的问题:
方法三:使用 ViewPager2 作为外部 RecyclerView
Github sample
使用 ViewPager2 在内部使用 RecyclerView 可以解决回收视图的问题,但一次只能显示一个页面(一个外部行)。
优点:
缺点:
所以我们可能会通过研究来解决这个问题: