背景:
项目里有一个定时刷新的需求,刷新的数据是填充在Recyclerview里的
问题:
用户可能已经滑动Recyclerview到某一位置,这时候触发了定时刷新任务,新的数据到来会触发Recyclerview的adapter.notifydatasetchanged(),这时候
1.数据已经刷新,Recyclerview应该会滑动到初始位置
2.Recyclerview焦点丢失
探讨:
对于第一点,很奇怪没有发生,Recyclerview刷新数据后竟然保持在了原来的滚动位置没有变化,这里暂时没想明白
难道跟我用的控件有关系?
implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.7'
自定义adapter继承BaseQuickAdapter,并且使用setList刷新数据源,看了下setList的实现,没有发现有特殊处理的地方。
最开始想的是如果发生问题1,是不是可以通过setDiffNewData之类方式的进行局部刷新解决,但是很奇怪没有发生。
对于问题2,焦点确实会丢失,因为数据源变化了,重新绘制了Recyclerview。
但在应用场景里,用户很难接受焦点瞬移,导致操作不连续。
这里我们很容易想到的方案就是,在刷新数据源之前,先判断当前Recyclerview是否获取了焦点,如果有焦点,则将焦点位置记住,在刷新数据源后,强行将焦点位置还原回去。
那么问题就变成了
1.如何判断当前Recyclerview是否有焦点(当前用户可能用遥控器操控其它,焦点不一定在Recyclerview上,那么就不需要我们将焦点还原到Recyclerview的先前位置)
2.如何记录焦点的位置(当前焦点在Recyclerview的第几条,即position)
3.在数据刷新完成后还原位置(即将焦点还原到第2步中的position)
经过一系列的查询,我定义了如下方法
private int getRecyclerviewFocusPosition(){
//默认的无意义的焦点位置,用于该方法区分返回的position是否是有效值
int position = -100;
//解决第1点,判断是否有焦点,如果焦点不在Recyclerview,则此时返回null
View focusView = binding.recyclerview.getFocusedChild();
if (focusView != null){
//解决第2点,记录了焦点的position
position = binding.recyclerview.getChildAdapterPosition(focusView);
}
logger.i("getRecyclerviewFocusPosition: " + position);
return position;
}
该方法一定要在setList之前调用,因为此时才是检测Recyclerview是否有焦点的最佳时机(提前调用可能用户在后续操作中已将焦点移出Recyclerview,那么记录的position就没有意义),
然后在Recyclerview刷新了最新数据源后立马恢复焦点绘制
binding.recyclerview.post(() -> { if (focusPosition > -100){ binding.recyclerview.getLayoutManager().
findViewByPosition(focusPosition).requestFocus(); } });
当然最好的时机是监听Recyclerview绘制完成后触发,这里由于我们的数据源比较少且布局不复杂,直接view.post也是正常可行的
扩展:
另外查了下view.post方法
1、View.post(Runnable) 内部会自动分两种情况处理,当 View 还没 attachedToWindow 时,会先将这些 Runnable 操作缓存下来;否则就直接通过 mAttachInfo.mHandler 将这些 Runnable 操作 post 到主线程的 MessageQueue 中等待执行。
2、如果 View.post(Runnable) 的 Runnable 操作被缓存下来了,那么这些操作将会在 dispatchAttachedToWindow() 被回调时,通过 mAttachInfo.mHandler.post() 发送到主线程的 MessageQueue 中等待执行。
3、mAttachInfo 是 ViewRootImpl 的成员变量,在构造函数中初始化,Activity View 树里所有的子 View 中的 mAttachInfo 都是 ViewRootImpl.mAttachInfo 的引用。
4、mAttachInfo.mHandler 也是 ViewRootImpl 中的成员变量,在声明时就初始化了,所以这个 mHandler 绑定的是主线程的 Looper,所以 View.post() 的操作都会发送到主线程中执行,那么也就支持 UI 操作了。
5、dispatchAttachedToWindow() 被调用的时机是在 ViewRootImol 的 performTraversals() 中,该方法会进行 View 树的测量、布局、绘制三大流程的操作。
6、Handler 消息机制通常情况下是一个 Message 执行完后才去取下一个 Message 来执行(异步 Message 还没接触),所以 View.post(Runnable) 中的 Runnable 操作肯定会在 performMeaure() 之后才执行,所以此时可以获取到 View 的宽高或者其他的计算逻辑。