Android bugs——RecyclerView scrollToPosition不会触发scrollListener?

首先说明下我遇到这个问题的背景吧。

Android Tv开发中常常会遇到RecyclerView初始化时焦点位置不为0的情况,比如,推荐一个节目集,希望给用户展示上次观看的集数,这时我们的初始化焦点位置大于0,焦点view可能在当前屏幕内,也可能不在当前屏幕内,如果当焦点view不在当前屏幕内,主动获取焦点则会失败,我的解决方法是让焦点view滚动到屏幕可见范围内,滚动结束再获取焦点。这时就需要监听RecyclerView的OnScrollListener了。但scrollToPosition(int)却没有触发OnScrollListener,导致无法上焦点。

下面我们就来分析下为什么不会触发OnScrollListener。

首先可以参考Stack Overflow的解决方法,这里大概解释了为什么不触发。但还不够明确,所以还需要看下源码来分析下。

1、Stack Overflow问题分析:

RecyclerView scrollToPosition not trigger scrollListener

This is a known issue. It is caused by the fact that RecyclerView does not know how LayoutManager will handle the scroll or if it will handle it at all.

In the next release, you’ll receive a call to onScrolled if first and or last child position changes after a layout (which is generally the result of calling scroll to position).

Unfortunately, dx and dy will be 0 because RecyclerView does not really know how much layout manager did scroll to handle scrollTo request. Alternatively, you can also use the the ViewTreeObserver’s scroll callback.

大致意思就是RecyclerView不知道LayoutManager如何处理滚动或者它是否会处理滚动。在下一个版本中,如果布局后第一个和最后一个子位置发生更改(通常是调用滚动到位置的结果)你将收到一个onScrolled回调。

但是,因为RecyclerView并不真正知道布局管理器滚动处理scrollTo请求的程度,所以dx和d为0。或者,你也可以使用ViewTreeObserver的滚动回调。

这里我尝试了ViewTreeObserver好像并没有用哈哈。

2、scrollToPosition源码分析

这里我大概说下流程,然后给大家推荐一篇非常详细的分析过程,有兴趣的同学可以去好好研究下。

RecyclerView的scrollToPosition调用了LayoutManager的scrollToPosition方法,
这里简单放下Layoutmanager的scrollToPosition方法源码:

public void scrollToPosition(int position) {
        this.mPendingScrollPosition = position;
        this.mPendingScrollPositionOffset = -2147483648;
        if (this.mPendingSavedState != null) {
            this.mPendingSavedState.invalidateAnchor();
        }

        this.requestLayout();
    }

由源码可以看到最终会调用requestLayout方法,这个方法最终会回调onLayoutChildren方法。
onLayoutChildren方法大概分为三步:

  • 1、确定锚点(Anchor)View, 设置好AnchorInfo
  • 2、根据锚点View确定有多少布局空间mLayoutState.mAvailable可用
  • 3、根据当前设置的LinearLayoutManager的方向开始摆放子View

其实由源码可以看出最终它还做了一件事情,就是处理布局间隙问题,由于onLayoutChildren的源码太长我只放这一段的内容,

 if (this.getChildCount() > 0) {
                if (this.mShouldReverseLayout ^ this.mStackFromEnd) {
                    fixOffset = this.fixLayoutEndGap(endOffset, recycler, state, true);
                    startOffset += fixOffset;
                    endOffset += fixOffset;
                    fixOffset = this.fixLayoutStartGap(startOffset, recycler, state, false);
                    startOffset += fixOffset;
                    endOffset += fixOffset;
                } else {
                    fixOffset = this.fixLayoutStartGap(startOffset, recycler, state, true);
                    startOffset += fixOffset;
                    endOffset += fixOffset;
                    fixOffset = this.fixLayoutEndGap(endOffset, recycler, state, false);
                    startOffset += fixOffset;
                    endOffset += fixOffset;
                }
            }

然后我们再看下fixLayoutStartGapfixLayoutEndGap方法:

 private int fixLayoutStartGap(int startOffset, Recycler recycler, State state, boolean canOffsetChildren) {
        int gap = startOffset - this.mOrientationHelper.getStartAfterPadding();
        int fixOffset = false;
        if (gap > 0) {
            int fixOffset = -this.scrollBy(gap, recycler, state);
            startOffset += fixOffset;
            if (canOffsetChildren) {
                gap = startOffset - this.mOrientationHelper.getStartAfterPadding();
                if (gap > 0) {
                    this.mOrientationHelper.offsetChildren(-gap);//滚动RecyclerView
                    return fixOffset - gap;
                }
            }

            return fixOffset;
        } else {
            return 0;
        }
    }


 private int fixLayoutEndGap(int endOffset, Recycler recycler, State state, boolean canOffsetChildren) {
        int gap = this.mOrientationHelper.getEndAfterPadding() - endOffset;
        int fixOffset = false;
        if (gap > 0) {
            int fixOffset = -this.scrollBy(-gap, recycler, state);
            endOffset += fixOffset;
            if (canOffsetChildren) {
                gap = this.mOrientationHelper.getEndAfterPadding() - endOffset;
                if (gap > 0) {
                    this.mOrientationHelper.offsetChildren(gap);//滚动RecyclerView
                    return gap + fixOffset;
                }
            }

            return fixOffset;
        } else {
            return 0;
        }
    }

看到这里我们已经明白了scrollToPosition如何滚动的,但同时我们应该发现onLayoutChildren并没有回调任何onScrollListnener的方法。所以由此得出,scrollToPosition不会回调onScrollListener。

图文搞懂 RecyclerView 刷新机制 | 源码分析
这篇文章详细分析了onLayoutChildren的方法的执行过程。

RecyclerView.smoothScrollToPosition了解一下
这篇文章详细分析了smoothScrollToPosition的源码。
这里我们可以知道最终其实是走到了RecyclerView.ViewFlipper的run方法中

  if (hresult != 0 || vresult != 0) {
          RecyclerView.this.dispatchOnScrolled(hresult, vresult);
  }

上面是run方法的最后几行代码,大家可以去找下哈,这里最终回调了OnScroll,神奇吧,由此我们也可以得出结论:

scrollToPosition不会回调onScrollListener,而smoothScrollToPosition会回调OnScrollListener。

最后我们回到开头的那个bug上面,如何解决RecyclerView初始化非0位置view获取焦点问题呢,这里我有两个答案:
1、

recyclerView.scrollToPosition(pos);
recyclerView.post(()->{
	//requestFocus.
|);

由源码可知,scrollToPosition会调用requestLayout,而requestLayout是一个非常霸道的方法,会锁死主线程,直到他完成才会重新解锁,所以我们可以在RecyclerView绘制完成时重新请求焦点,这时,view也滚动到屏幕可见范围内了。

2、

RecyclerView.addOnScrollListener(){
	 @Override
     public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
             super.onScrollStateChanged(recyclerView, newState);
             if (newState == RecyclerView.SCROLL_STATE_IDLE && customScroll) {//滚动结束
                       customScroll = false;
                       //requestFocus.
               }
      }
}
RecyclerView.smoothScrollToPosition(pos)

你可能感兴趣的:(Android,Bugs,android,Recycylerview)