1.前言
最近有个同学要实现如下效果,点击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(); }
//recyclerview-1.2.0 LinearLayoutManager.java @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()
➡️RecyclerViewAction.update()
该方法作用:
- 计算位置,滚动
- 如果找到了目标view,调用onTargetFound
mTargetView赋值时机是,RecyclerView滚动过程中调用LayoutManager.addViewInt方法时。
4.1 是否开启offset效果
是否开启offset的区别是:
如果开启了offset,目标position的view无论是否在屏幕内,无论是在当前位置的上方还是下方,都会滚动到屏幕的顶部,而如果调用的是scrollToPosition,如果view已经在屏幕内,则不会有任何效果。如果目标position在屏幕下方,布局会从屏幕底部开始,如果目标position在屏幕上方,布局会从屏幕顶部开始。