最近有个同学要实现如下效果,点击
Tab
,
RecyclerView
会让
Tab
标签对应的第一个
Item
显示在
RecyclerView
的顶部。他通过
RecyclerView.scrollToPositionWithOffset()
实现了该效果,但是UI同学希望有一个平滑滚动效果,说到平滑滚动,大家也都知道
RecyclerView
有
smoothScrollToPosition()
方法。由于
TitleBar
和
Tab
标签栏覆盖在
RecyclerView上
的,所以滚动需要加上偏移量才行。但是
smoothScroll
相关的方法偏偏没有偏移量相关的重载方法。最终效果图如下:
2.问题分析
LinearLayoutManager
和
StaggeredGridLayoutManager
有三个方法可以实现滚动到指定位置效果:
scrollToPosition(int position)
scrollToPositionWithOffset(int position, int offset)
smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position)
首先
方法1和方法2的区别在于offset偏移量。scrollToPosition的作用是把position对应的ItemView放置到RecyclerView的顶部。scrollToPositionWithOffset在scrollToPosition的基础上还可以偏移指定的距离,当RecyclerView的顶部被遮挡的时候,我们就需要通过偏移方法来将遮挡的部分露出来。
其次
smoothScrollToPosition与scrollToPosition的区别是,后者根据计算,直接以position为锚点重新布局RecyclerView,给用户的视觉感觉是
非常突兀,没有过渡效果
,smoothScrollToPosition会从当前位置,发出类似fling的动作,fling到目标position处,它的优点是
平滑过渡,用户体验好
,但是它的缺点是
如果当前position离目标position比较远,由于每个Item都需要渲染出来,如果RV优化效果不好,会造成卡顿
。
最后
我们发现smoothScrollToPosition方法没有类似
smoothScrollToPositionWithOffset
的方法。那么我既想要
平滑滚动
又想要
带偏移量滚动
该怎么办呢?
3. 解决方案
图片版本代码
private fun RecyclerView.smoothScrollToPositionWithOffset(position: Int, offset: Int) {
val linearSmoothScroller = object : LinearSmoothScroller(context) {
override fun onTargetFound(targetView: View, state: RecyclerView.State, action: Action) {
super.onTargetFound(targetView, state, action)
val dx = calculateDxToMakeVisible(targetView, horizontalSnapPreference)
val dy = calculateDyToMakeVisible(targetView, SNAP_TO_START)
val distance = sqrt((dx * dx + dy * dy).toDouble()).toInt()
val time = calculateTimeForDeceleration(distance)
if (time > 0) {
action.update(-dx, -dy - offset, time, mDecelerateInterpolator)
linearSmoothScroller.targetPosition = position
layoutManager?.startSmoothScroll(linearSmoothScroller)
运行以下代码
fun smoothScrollToWithOffset(view: View) {
mRecyclerView.smoothScrollToPositionWithOffset(20, 100);
效果如下,平滑地将Item20滚动到RV顶部,并且留出了100px的offset:
4. 深入分析
Android系统提供的三个scrollToPosition相关的方法加上自己实现的smoothScrollToPositionWithOffset。我们得到了四个scrollToPosition方法
方法 |
---|
scrollToPosition |
scrollToPositionWithOffset |
smoothScrollToPosition |
smoothScrollToPositionWithOffset |
那么他们之间的区别是什么呢?我将从以下两个维度简单分析一下:
是否开启smooth效果
是否开启offset效果
4.1 是否开启smooth效果
//recyclerview-1.2.0 LinearLayoutManager.java
@Override
public void scrollToPosition(int position) {
mPendingScrollPosition = position
mPendingScrollPositionOffset = INVALID_OFFSET
if (mPendingSavedState != null) {
mPendingSavedState.invalidateAnchor()
requestLayout()
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
int position) {
LinearSmoothScroller linearSmoothScroller =
new LinearSmoothScroller(recyclerView.getContext());
linearSmoothScroller.setTargetPosition(position);
startSmoothScroll(linearSmoothScroller);
由上述代码我们可以看到,这两种效果的实现方式完全不同。
scrollToPosition 是通过修改mPendingScrollPosition变量,以该变量为锚点,重新布局,调用栈如下:
➡️LinearLayoutManager.onLayoutChildren
➡️LinearLayoutManager.updateAnchorInfoForLayout
➡️LinearLayoutManager.updateAnchorFromPendingData
更多关于RecyclerView布局原理请查看深入理解RecyclerView布局原理
smoothScrollToPosition 是通过调用SmoothScroller的start方法,模拟fling操作,动态找寻目标position的view,如果找到了则定位到顶部,调用栈如下:
➡️RecyclerView$SmoothScroller.start()
➡️RecyclerView$ViewFlinger.run()
➡️RecyclerView$SmoothScroller.onAnimation()
➡️RecyclerView$SmoothScroller.onTargetFound()
➡️RecyclerViewSmoothScrollerAction.update()
该方法作用:
计算位置,滚动
如果找到了目标view,调用onTargetFound
mTargetView赋值时机是,RecyclerView滚动过程中调用LayoutManager.addViewInt方法时。
4.1 是否开启offset效果
是否开启offset的区别是:
如果开启了offset,目标position的view无论是否在屏幕内,无论是在当前位置的上方还是下方,都会滚动到屏幕的顶部,而如果调用的是scrollToPosition,如果view已经在屏幕内,则不会有任何效果。如果目标position在屏幕下方,布局会从屏幕底部开始,如果目标position在屏幕上方,布局会从屏幕顶部开始。

- 6045
-
长安皈故里
Android
Android Jetpack
- 460
-
Halifax
Android
Kotlin
- 4527
-
DonKingLiang
Android