关于 RecyclerView 的滑动方法有很多:
- scrollToPosition
- scrollTo
- scrollBy
- smoothScrollBy
- smoothScrollToPosition
针对 LineaderLayoutManager 还有一个很重要的方法:scrollToPositionWithOffset,下面逐一进行说明。
scrollTo
1 | public void scrollTo(int x, int y) { |
滑动到绝对位置,直接不支持。很皮~
scrollBy & smoothScrollBy
基于当前位置进行相对滑动。
1 | public void scrollBy(int x, int y) { |
根据布局的方向进行滑动。scrollByInternal 为具体处理滑动的方法,这里不贴了,它会调用 dispatchOnScrolled:
1 | void dispatchOnScrolled(int hresult, int vresult) { |
会调用到 mScrollListener.onScrolled,也就是说可以与 ScrollListener 共同作用。这里先提一下,后面做详细说明。
1 | int dx, int dy, Interpolator interpolator) { void smoothScrollBy( |
最终调用的是 mViewFlinger.smoothScrollBy,mViewFlinger 是 RecyclerView 内部的一个 Runnable,通过不停执行 postOnAnimation 来实现平滑滑动。
scrollToPosition & smoothScrollToPosition
滑动到指定 position,也是可以直接或者平滑滑动。最后调用到的还是 LayoutManager 中的方法:
1 | public void scrollToPosition(int position) { |
改了属性直接 requestLayout。看下:smoothScrollToPosition
1 | public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, |
创建了一个 LinearSmoothScroller 来进行滑动,滑动也是调用的 RecyclerView 内部的 ViewFlinger 来实现的,它也会调用到 dispatchOnScrolled 方法,所以也可以与 ScrollListener 共用。
scrollToPositionWithOffset
1 | public void scrollToPositionWithOffset(int position, int offset) { |
与 scrollToPosition 几无二致,只不过带上了 offset。当 offset > 0,目标元素会距离顶部多出 offset 的距离,小于 0 则会被盖住 offset 的距离。注意:当 mPendingScrollPositionOffset 为 INVALID_OFFSET 时,滑动表现是 SNAP_TO_ANY,否则为 SNAP_TO_START,下面来具体说说。
LinearSmoothScroller SNAP
1 | /** |
根据注释很清晰了,SNAP_TO_START 为左上角对齐,SNAP_TO_END 为 右下角对齐,SNAP_TO_ANY 则是不限制,只要显示完对应 postion 的 Item 即可。
举个栗子:当前有 10个 Item。
- 处于列表顶部,调用 scrollToPosition(5),列表会从顶部定位到第 6 个 Item,且第 6 个 Item 位于屏幕最下方。
- 处于列表底部,调用 scrollToPosition(5),列表会从底部定位到第 6 个 Item,且第 6 个 Item 位于屏幕最上方。
- 处于列表中部,第 6 个 Item 可见时调用 scrollToPosition(5),界面不会发生任何改变。
这也就是网上众多文章吐槽 RecycleView 定位不准的原因了。因为默认是 SNAP_TO_ANY,RecycleView 处理起来就是让目标 position 的 Item 在列表中完全可见。如果目标是从底部滑上来,那么当目标完全可见时,滑动就会终止了,所以目标处理可见列表的最底端,反之亦然。当目标本身就是完全可见的,便不会做任何处理。所以,如若有定位目标必须处于列表顶端的需求,则可以这样:
1 | RecyclerView.SmoothScroller smoothScroller = new LinearSmoothScroller(context) { |
如果是底端则可以使用 SNAP_TO_END。
addOnScrollListener
通常会有这种需求,监听 RecyclerView 滑动多少了,来改变标题栏的表现等需求。所以会有这样的代码:
1 | private val onScrollListener = object : RecyclerView.OnScrollListener() { |
当滑动距离超过 100dp 则进行某些处理。上文有可以与 ScrollListener 共同作用这一说法,下面进行说明。
当设置了 onScrollListener 了之后,现在假设每个 Item 高度为 100, 现在处于第 2个 Item,那么 offsetY 的值为 100。现在分别执行下面几个操作:
- scrollBy(200):基于当前位置直接滑动 200 单位,会回调 onScrolled,dy 为 200,所以 offsetY 为 300,符合预期。
- smoothScrollBy(200):大体与 scrollBy 一直,只不过是平滑滑动,会多几个中间值,但最终结果是一样的。
- scrollToPosition(3):直接定位到 position 为 3 的位置,最终会调用 onScrolled(重新 layout 调用,而不是滑动操作调用),dy 为 0,offsetY 仍然为 100,即当前已经显示在第 4 个 Item 了,结果 offsetY 仍然为 100,这是有问题的。
- smoothScrollToPosition(3):会平滑定位到 position 为 3 的位置,与 smoothScrollBy 表现类似,最终结果与 1、2 一致。
- scrollToPositionWithOffset(3):与 3 表现一致,只不过允许 offset。
鉴于文章长度,没有粘贴测试代码及 Log,只是把现象描述出来,需要细心看看。综上所述:平滑滑动或相对滑动的方法可以与 onScrollListener 结合使用,scrollToPosition 和 scrollToPositionWithOffset 则不能与 onScrollListener 结合使用。
scrollToPositionWithOffset & onScrollListener
如上图,是一个文章详情页。
- 需要监听 onScrollListener 来改变标题栏。
- 文章是个列表,文章详情为一个 Item,每一条评论为一个 Item,新增一条评论时需要定位到这条评论。
- 标题栏覆盖在列表之上。
直接使用 smoothScrollToPosition 会导致标题栏覆盖一部分评论,所以需要加上偏移。可惜上文得出的结论 scrollToPositionWithOffset 与 onScrollListener 无法结合使用。所以如何处理呢?
这里我保持 onScrollListener 不变,针对偏移采用曲线救国的方法:给详情 Item 添加一个 layout_marginBottom=”-60dp”,给新增的一条评论添加 layout_marginBottom=”60dp”,这样当使用 smoothScrollToPosition 定位到最新一条评论时,它距离顶部有 60 dp 的距离,如此便不会被覆盖了。这个想法与Android 滑动吸顶效果是一致的,发现这个做法还真挺有用,哈哈~