0%

当快速滚动遇上嵌套滑动

帖子页面用RecyclerView-FastScroll来给RecyclerView加上快速滚动的滑块,同时,为了统一布局风格,标题用了CollapsingToolbarLayout,和RecyclerView有嵌套滑动。需要在关联滚动的同时保持滑块的位置,轮子并没有考虑到这个问题,那么魔改开始。

先来看效果:

prev

画个示意图:

fig1

首先,通过两个回调函数获取关联滚动的高度和关联滚动的距离

1
2
var nestedScrollRange = { 0 }    // 关联滚动的高度
var nestedScrollDistance = { 0 } // 关联滚动的距离

把关联滚动高度看作一个额外的子项,有:

FastScrollRecyclerView.onUpdateScrollbar里计算当前滑块位置:

由于多了嵌套滑动的距离,touchFraction就不能由FastScroller算出来,直接传滑块位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (mIsDragging) {
if (mLastY == 0 || Math.abs(mLastY - y) >= mTouchSlop) {
mLastY = y;
// Update the fastscroller section name at this touch position
- boolean layoutManagerReversed = mRecyclerView.isLayoutManagerReversed();
- int bottom = mRecyclerView.getHeight() - mThumbHeight;
- float boundedY = (float) Math.max(0, Math.min(bottom, y - mTouchOffset));
-
- // Represents the amount the thumb has scrolled divided by its total scroll range
- float touchFraction = boundedY / bottom;
- if (layoutManagerReversed) {
- touchFraction = 1 - touchFraction;
- }
-
- String sectionName = mRecyclerView.scrollToPositionAtProgress(touchFraction);
+ String sectionName = mRecyclerView.scrollToPositionAtProgress(y - mTouchOffset);
mPopup.setSectionName(sectionName);
mPopup.animateVisibility(!sectionName.isEmpty());
mRecyclerView.invalidate(mPopup.updateFastScrollerBounds(mRecyclerView, mThumbPosition.y));
}
}

FastScrollRecyclerView.scrollToPositionAtProgress里与当前滑块位置相减得滚动位移:


然后模拟关联滚动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)
val consumed = IntArray(2)
val offsetInWindow = IntArray(2)
dispatchNestedPreScroll(0, dy.roundToInt(), consumed, offsetInWindow)
dy -= consumed[1].toFloat()
val scrollY = Math.min(Math.max(scrolledPastHeight + dy, 0f), availableScrollHeight.toFloat())
dy -= scrollY - scrolledPastHeight
dispatchNestedScroll(
consumed[0],
consumed[1],
0,
Math.max(-nestedDistance + consumed[1], dy.roundToInt()),
offsetInWindow
)

这里用scrollY先预测了RecyclerView所能消耗的滚动距离,再从高度缓存中找到对应的子项,调用LayoutManager.scrollToPositionWithOffset滚到对应位置,并返回其标题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
val layoutManager = layoutManager
val adapter = adapter
var totalOffset = 0
itemHeightCache.forEachIndexed { index, height ->
if (scrollY >= totalOffset && scrollY <= totalOffset + height) {
val wrapIndex = getAdapterItemIndex(index)
if (layoutManager is LinearLayoutManager)
layoutManager.scrollToPositionWithOffset(wrapIndex, totalOffset - scrollY.roundToInt())
else if (adapter is MeasurableAdapter && layoutManager is StaggeredGridLayoutManager)
layoutManager.scrollToPositionWithOffset(wrapIndex, totalOffset - scrollY.roundToInt())
val sectionedAdapter = (adapter as? SectionedAdapter) ?: return ""
return sectionedAdapter.getSectionName(wrapIndex)
}
totalOffset += height
}
return ""

完整代码传送门