查看原文
其他

如何优雅地恢复Recyclerview的滚动位置

Flywith24 郭霖 2020-10-29


/   今日科技快讯   /


昨日,针对纳斯达克交易所通知瑞幸咖啡必须摘牌退市的消息,瑞幸咖啡董事长陆正耀于今日发布声明称,纳斯达克不等最终调查结果就要求公司退市,出乎意料,对此个人深感失望和遗憾。在声明中,陆正耀对瑞幸咖啡事件造成的恶劣影响,向投资人、全体瑞幸员工和客户道歉。


/   作者简介   /


本篇文章来自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可以做点什么吗?」


推荐阅读:

我新开发了一个特别好用的开源库

Android 11来了,快!扶我起来

这本《第三行代码》,让大家久等了!


欢迎关注我的公众号

学习技术或投稿



长按上图,识别图中二维码即可关注


Modified on

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存