方案一:使用两个嵌套的RecyclerView
Github示例
优点:
- 视图可以被回收利用(即良好的性能)
- 半无缝滚动(更新3和4之后)
缺点:
- 当到达内部最远端项目时,从内部滚动到外部滚动的过渡中程序传播的滚动不太平滑/自然,像手势那样。
- 代码复杂。
好吧,我不会讨论垂直嵌套的RecyclerViews
的性能问题;但是请注意:
- 内部
RecyclerView
可能失去回收视图的能力;因为外部RecyclerView
的显示行应该完全加载其项目。(幸运的是,根据下面的UPDATE 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)
outerAdapter.isOuterScrollingDown(true, dy);
else if (dy < 0)
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滚动。这由一个监听器处理,具有回调函数,接受传递给外部RV的自定义LinearLayoutManager类的布尔值。
同样地,为了重新启用外部RV的滚动:内部RecyclerView注册OnScrollListener以检查内部第一个/最后一个项目是否显示。
innerRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
if (!recyclerView.canScrollVertically(1)
&& newState == RecyclerView.SCROLL_STATE_IDLE) {
enableOuterScroll(true);
} else if (!recyclerView.canScrollVertically(-1)
&& 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);
}
}
日志:
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滚动值并使用任意滚动值来操作内部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()
会在决定是否可以滚动内部RecyclerView
之前被调用。
解决方法:通过向外部RecyclerView使用OnTouchListener
,并覆盖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;
} else if (oldY < newY) {
outerAdapter.isOuterScrollingDown(false, (int) (oldY - newY));
oldY = newY;
} else if (oldY > newY) {
outerAdapter.isOuterScrollingDown(true, (int) (oldY - newY));
oldY = newY;
}
break;
}
return false;
});
更新4
- 当内部
RecyclerView
滚动到顶部或底部边缘时,启用从内部到外部的滚动转换,使其以比例速度继续滚动到外部RV。
滚动速度的灵感来自这篇文章。通过在触摸的内部RV的OnTouchListener
和OnScrollListener
中应用第一个/最后一个项目检查,并在全新的触摸事件即MotionEvent.ACTION_DOWN
中重置一些东西。
- 禁用内部和外部
RecyclerView
的超出滚动模式
预览:
方案2:将外部RecyclerView包装在NestedScrollView中
Github示例
RecyclerView
的主要问题是它没有实现NestedScrollView
实现的NestedScrollingParent3接口;因此,RecyclerView
无法处理子视图的嵌套滚动。因此,通过将外部RecyclerView
包装在NestedScrollView
中,并禁用外部RecyclerView
的滚动来尝试弥补这一点。
优点:
- 简单的代码(您不必完全操纵内部/外部滚动)
- 没有故障
- 无缝滚动
缺点:
- 性能较低,因为外部
RecyclerView
的视图未被回收,因此它们必须在显示在屏幕上之前全部加载。
原因:由于NestedScrollView
的特性>>请查看讨论回收问题的(1)、(2)问题:
方案3:使用ViewPager2作为外部RecyclerView
Github示例
使用内部使用RecyclerView
的ViewPager2
解决了视图回收的问题,但一次只能显示一个页面(一个外部行)。
优点:
缺点:
因此,我们可能需要研究以下内容: