相关文章推荐
挂过科的油条  ·  Python ...·  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()

    ➡️RecyclerViewSmoothScrollerSmoothScrollerAction.update()

    该方法作用:

  • 计算位置,滚动
  • 如果找到了目标view,调用onTargetFound
  • mTargetView赋值时机是,RecyclerView滚动过程中调用LayoutManager.addViewInt方法时。

    4.1 是否开启offset效果

    是否开启offset的区别是:

    如果开启了offset,目标position的view无论是否在屏幕内,无论是在当前位置的上方还是下方,都会滚动到屏幕的顶部,而如果调用的是scrollToPosition,如果view已经在屏幕内,则不会有任何效果。如果目标position在屏幕下方,布局会从屏幕底部开始,如果目标position在屏幕上方,布局会从屏幕顶部开始。

    搜索框传播样式-标准色版.png

    分类:
    Android
  •