如何优雅地恢复Recyclerview的滚动位置
/ 今日科技快讯 /
昨日,针对纳斯达克交易所通知瑞幸咖啡必须摘牌退市的消息,瑞幸咖啡董事长陆正耀于今日发布声明称,纳斯达克不等最终调查结果就要求公司退市,出乎意料,对此个人深感失望和遗憾。在声明中,陆正耀对瑞幸咖啡事件造成的恶劣影响,向投资人、全体瑞幸员工和客户道歉。
/ 作者简介 /
本篇文章来自Flywith24的投稿,分享了他对在Android中恢复recyclerview的滚动位置问题研究的过程,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。
Flywith24的博客地址:
https://juejin.im/user/57c7f6870a2b58006b1cfd6c
/ 被忽视的更新 /
androidx-recyclerview 1.2.0-alpha02版本添加了新功能MergeAdapter,帮助开发者更容易地为RecyclerView添加Header和Footer。
该版本中还有一个改动:「RecyclerView.Adapter lazy state restoration」,帮助开发者恢复RecyclerView的状态:
我对这个功能并没有什么感觉。众所周知,Android中的View内部是有着状态保存和恢复的方法的。RecyclerView也是如此,它可以恢复自身已滚动的位置。
真实情况也是如此:
RecyclerView 内部可以恢复滚动位置
/ 意外发现 /
最近看到Florina Muntenescu的Restore RecyclerView scroll position ,其中介绍了「RecyclerView.Adapter lazy state restoration」,这勾起了我的兴趣。
如文中描述,RecyclerView在activity/fragment重建时失去滚动位置是因为Adapter中的数据是「异步」加载的,当RecyclerView layout时数据并没有加载,因此也恢复不了之前的位置状态。一个比较简单的例子是使用Navigation组件进行导航,返回时fragment中的RecyclerView由于再次调用接口获取数据,导致其滑动位置失去。
延迟加载数据,无法恢复滚动位置
/ 解决方案 /
有几种方法可以保证RecyclerView恢复到正确的滚动位置,最好的办法是借助缓存,ViewModel或Repository中缓存要显示的数据,确保始终在第一个布局传入前在Adapter上设置数据。
也有一些其他的方案,这些方案要么太复杂,要么不够优雅。recyclerview:1.2.0-alpha02中的解决方案是提供一个新的Adapter方法,该方法允许设置状态恢复策略,它有三个选项:
ALLOW
PREVENT_WHEN_EMPTYPREVENT
PREVENT
ALLOW
这是「默认」的状态,它会立即恢复RecyclerView的状态,该种策略无法解决延迟加载的数据的问题,可以使用PREVENT_WHEN_EMPTY。
PREVENT_WHEN_EMPTY
仅当Adapter不为空(adapter.getItemCount() > 0)时,才恢复RecyclerView状态。如果您的数据是异步加载的,那么RecyclerView会一直等到数据加载完毕,然后状态才能恢复。
如果您有默认item(例如Header或加载指示器)作为适配器的一部分,则应该使用PREVENT选项,除非使用 MergeAdapter 添加了默认item。MergeAdapter等待所有适配器准备就绪,然后才恢复状态。
PREVENT
状态不会恢复,直到配置了ALLOW或者PREVENT_WHEN_EMPTY。使用方式如下:
adapter.stateRestorationPolicy = PREVENT_WHEN_EMPTY
「加入了上面的配置后即使是异步加载数据也能恢复 RecyclerView 的位置」
设置 PREVENT_WHEN_EMPTY
/ 追踪引入过程 /
老规矩,我们沿着官方的commit log来看看其实现原理。首先我们看看IssueTracker上提的Feature:
表达的意思也很简单,就是当加载异步数据时RecyclerView的位置状态无法恢复,Adapter应该提供相关的解决方案。有意思的是,实现该功能时还重新实现了前一个版本的逻辑,我在git commit log中看到了revert操作。
为了防止LayoutManager#onRestore执行多次,没有采用最开始的实现方式。但Yigit Boyar(这个提交的开发者) 仍然希望使用最开始的实现方式,但是LayoutManager#onRestoreInstance的状态时public ,因此只能选取一个折中的方案。
过去,开发者会无意间调用onRestoreInstanceState(State) 方法。例如,一些开发者已使用它来手动设置自己更新的状态,这样即使在此状态之前已恢复,在此处传递状态也将导致LayoutManager接收它并相应地更新其内部状态。因此,即使看起来好像很奇怪,也必须始终调用requestLayout来保留功能。
/ 源码分析 /
接下来我们来分析这部分源码,内容很少,所以我们详细看下。首先是引入StateRestorationPolicy的枚举:
然后需要提供setStateRestorationPolicy和getStateRestorationPolicy方法,此时我们还需要一个方法来判断是否要将SavedState传递给LayoutManager。
前面的setStateRestorationPolicy方法中 调用了notifyStateRestorationPolicyChanged,而notifyStateRestorationPolicyChanged为静态类AdapterDataObservable中的方法,该类中的其他方法我们也很熟悉,均是刷新Adapter中数据的方法。
而notifyStateRestorationPolicyChanged中调用了mObservers list中元素的onStateRestorationPolicyChanged方法,通过源码我们得知该list中的元素类型为AdapterDataObserver,因此还需要在AdapterDataObserver中加入 onStateRestorationPolicyChanged方法。
该方法是个空实现,而RecyclerViewDataObserver重写了该方法。
配置恢复策略以及恢复策略变化时的监听都有了,接下来要做的就是如果之前有待恢复的装则恢复之前的状态。
注意:发布之前StateRestorationPolicy叫做StateRestorationStrategy,后来命名为StateRestorationPolicy,alpha版本的库可能随时更改API的命名和删除API,因此查看这部分源码的同学请注意。
至此,相关的源码都在这里了。
/ 总结 /
StateRestorationPolicy提供了RecyclerView异步加载数据恢复滚动位置的解决方案。原理就是通过配置StateRestorationPolicy来改变恢复策略,同时在策略改变时调用requestLayout方法。在dispatchLayoutStep2()(该方法会在onLayout和 measure 方法中调用) 方法中恢复状态(如果canRestoreState()返回true)。
demo地址如下所示:
https://github.com/Flywith24/Flywith24-Jetpack-Demo/tree/master/demo_recyclerview_scroll
「一点思考:我们都知道ViewPager2是使用RecyclerView实现的,那么借助本文介绍的API可以做点什么吗?」
推荐阅读:
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注