聊聊RecyclerView的像素级刷新
Posted microhex
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了聊聊RecyclerView的像素级刷新相关的知识,希望对你有一定的参考价值。
需求来源
当前我们所做的内容是施工管理的工具APP,有非常重的聊天系统,而且聊天系统中由于项目数比较大,大到什么概念呢?一个中层管理手中大概存在500个项目,每个项目分为施工群
和业主群
,然后加上公司OA群,设计群,管理群,大概在1000个群左右,当然你可能会说,1000个群也不算太大吧,我微信现在的群都有上千个,的确是这么个理,但是我们这1000个群每天都在发消息,业主要求推动施工的进程,假设每个群每天发10个消息,我们大概每天就有1w条消息,这是个很惊人的问题。所以问题就来,在上千个列表中,对RecyclerView的Item的刷新就变得很重要了。
具体环境分析
首先上图,我们来看看一般的聊天中需要显示哪些信息呢?
在上面的聊天item中,我们的动态数据有如下几个:
- 是否置顶 [置顶背景变灰]
- 头像更新 [有新的群成员进来,头像就会重组刷新]
- [是否有人@我]
- 免打扰
- 如果有免打扰,则消息未读数目变为小红点
- 不存在免打扰,则显示未读数目
- 消息时间
- 消息内容
- 群名称
- 关联的项目状态 [延期/结束/正常/未开始]
其实一个聊天还是有非常多的细节的,简简单单看一个聊天列表的item,我们可以看到它变化的内容其实还是蛮多的。那我们为什么会引入精细化刷新呢?其实原因有很多,我们在分析聊天例子中,其实这10项中刷新频率最高就是第6条未读消息数,和第8条消息内容,这个消息内容是群内最后一条消息的来源,你可以理解为和微信同理。
解决方案
关于精细化刷新,其实我以前写过这样一篇文章,对其原理有过简单的描述,也有过简单的demo。这里,我想讨论的是,如果我只想刷新消息的未读数目,和未读消息,我应该怎么做?
大多数人都会想到 DiffUtil.Callback 这个类,不清楚的大家可以google一下,也同时知道使用包含这三个参数的方案:
@Override
public void onBindViewHolder(@NonNull K holder, int position, @NonNull List<Object> payloads)
对于为啥要使用包含这三个参数的方案,其实我到现在也是分析到了一点点,等分析完了,我再写一篇博客吧。其实我们应该注重的是最后一个参数:payloads,而且它还是一个集合,其实我也不咋明白为啥是集合,当然这都是后话,今天就用它来展示应该怎么精细化刷新了。
废话不多说,首先我们看看 正常的 onBindViewHolder 该怎么写吧:
fun onBindViewHolder(@NonNull ViewHolder : ViewHolder, position: Int)
//更新消息时间
updateMsgTime();
//更新消息内容
updateMsgContent();
//更新是否置顶
updateSetTop();
//更新是否为免打扰 如果不是的话,则直接更新未读数
updateNotDisturb();
//更新关联项目状态
updateStatus();
//更新ait类型
updateAitType();
//更新群组头像
updateGroupAvatar();
那我们开始使用payload参数来开始我们的精细化刷新吧。
首先我们定义需要刷新的参数:
// 项目时间
public static final int DIFF_TIME_TYPE = 1;
// 是否置顶
public static final int DIFF_SET_TOP_TYPE = 1 << 1;
// 是否免打扰
public static final int DIFF_NOT_DISTURB_TYPE = 1 << 2;
// 关联项目状态
public static final int DIFF_STATUS_TYPE = 1 << 3;
// ait类型
public static final int DIFF_AIT_TYPE = 1 << 4;
// 未读数目
public static final int DIFF_UNREAD_COUNT_TYPE = 1 << 5;
// 消息内容
public static final int DIFF_MSG_CONTENT_TYPE = 1 << 6;
// 群组名称
public static final int DIFF_GROUP_NAME_TYPE = 1 << 7;
// 群组头像
public static final int DIFF_GROUP_AVATAR = 1 << 8;
定义我们的item对应的 ProjectChatGroup,属性比较多:
// 置顶功能标识符
public static final long RECENT_TAG_STICKY = 0x0000000000000001; // 联系人置顶tag
// 项目群id [普通群没有]
public int project_id ;
// 群组名称
public String group_name;
// 聊天tid
public String group_id ;
//群头像
public String group_avatar ;
//对应group_a 时间
public long lastMsgTime;
//对应group_a 时间
public String lastMsgContent;
// 1:置顶,0:不置顶
public int set_to_top ;
// 1. 免打扰 0 不打扰
public int not_disturb;
// 0:正常,1:延期,2:售后
public int status ;
//有@ 我的消息
public int messageAtAType = AitConstant.AIT_NONE_TYPE;
//项目未读数目
//对应Chat未读数目
public int unReadGroupCount;
//简单
public String simpleGroupName;
然后我们来比较 payload 的改变点:
@Nullable
@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition)
int targetValue = 0;
ProjectChatGroup oldChatGroup = getOldItem(oldItemPosition);
ProjectChatGroup newChatGroup = getNewItem(newItemPosition);
if(null == oldChatGroup || null == newChatGroup)
return targetValue;
// 时间不一致
if (oldChatGroup.getTime() != newChatGroup.getTime())
targetValue += DIFF_TIME_TYPE;
//是否置顶
if (oldChatGroup.set_to_top != newChatGroup.set_to_top)
targetValue += DIFF_SET_TOP_TYPE;
// 是否免打扰
if (oldChatGroup.not_disturb != newChatGroup.not_disturb)
targetValue += DIFF_NOT_DISTURB_TYPE;
// 关于项目状态
if (oldChatGroup.status != newChatGroup.status)
targetValue += DIFF_STATUS_TYPE;
// 是否有人ait我
if (oldChatGroup.totalAitType() != newChatGroup.totalAitType())
targetValue += DIFF_AIT_TYPE;
// 未读数目是否相同
if (oldChatGroup.getTotalUnreadCount() != newChatGroup.getTotalUnreadCount())
targetValue += DIFF_UNREAD_COUNT_TYPE;
// 消息内容是否相同
if (!Objects.equals(oldChatGroup.getLastRealContent(), newChatGroup.getLastRealContent()))
targetValue += DIFF_MSG_CONTENT_TYPE;
// 群组头像是否相同
if (!Objects.equals(oldChatGroup.group_avatar, newChatGroup.group_avatar))
targetValue += DIFF_GROUP_AVATAR;
return targetValue;
我们有了payload,现在看看应该怎么使用了:
override fun convert(helper: BaseViewHolder?,
item: ProjectChatGroup,
position: Int,
payloads: MutableList<Any>)
// 如果payload为空 则说明内容根本不同 此时可以看作为新的创建
if(payloads.isEmpty())
convert(helper, item)
return
updateInnerChange(helper,item,payloads)
我们来看看再 updateInnerChange
应该怎么写吧:
private fun updateInnerChange(
helper: BaseViewHolder?,
item: ProjectChatGroup,
payloads: MutableList<Any>
)
val payData = payloads[0]
if (payData is Int)
//更新当前时间
if (payData and DIFF_TIME_TYPE == DIFF_TIME_TYPE)
updateMsgTime()
//更新置顶
if (payData and DIFF_SET_TOP_TYPE == DIFF_SET_TOP_TYPE)
updateSetTop()
//更新免打扰
if (payData and DIFF_NOT_DISTURB_TYPE == DIFF_NOT_DISTURB_TYPE)
updateNotDisturb()
//更新状态
if (payData and DIFF_STATUS_TYPE == DIFF_STATUS_TYPE)
updateStatus()
//更新Ait状态
if (payData and DIFF_AIT_TYPE == DIFF_AIT_TYPE)
updateAitType()
//更新未读数目
if (payData and DIFF_UNREAD_COUNT_TYPE == DIFF_UNREAD_COUNT_TYPE)
updateNotDisturb()
//更新消息内容
if (payData and DIFF_MSG_CONTENT_TYPE == DIFF_MSG_CONTENT_TYPE)
updateMsgContent()
//群组名称
if (payData and DIFF_GROUP_NAME_TYPE == DIFF_GROUP_NAME_TYPE)
updateStatus()
//群组头像 更新
if (payData and DIFF_GROUP_AVATAR == DIFF_GROUP_AVATAR)
updateGroupAvatar()
我们通过payload的取值,然后通过运算变换取得更新的变换点,最终我们即可以实现我们需要更新的最小点。
然后我们在
adapter.notifyItemChanged(position: Int)
更改为:
val payload = DIFF_UNREAD_COUNT_TYPE | DIFF_MSG_CONTENT_TYPE
// 此时只刷新的是 消息内容 和 未读数目了
adapter.notifyItemChanged(position,payload)
出现的问题
java.lang.IndexOutOfBoundsException
报错为:
Inconsistency detected. Invalid view holder adapter positionViewHolder97fc989 position=1 id=-1, oldPos=0, pLpos:0 scrap [attachedScrap] tmpDetached no parent androidx.recyclerview.widget.RecyclerViewfa338c3 VFED… …ID 0,0-1080,1671 #7f080066 app:id/base_recycler_view
主要问题可以看解答
因为DiffUtils内部使用的是 notifyItemChanged,那么推荐使用:
public class FixLinearLayoutManager extends LinearLayoutManager
public FixLinearLayoutManager(Context context)
super(context);
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)
try
super.onLayoutChildren(recycler, state);
catch (Exception e)
e.printStackTrace();
继承LinearLayoutManager,在onLayoutChildren中加入try-catch。
总结
通过上面的例子,我们基本上可以实现一个最小粒度的更新了,以前我们可以通过adapter.notifyItemChanged(position:Int)
来刷新一个item列表,今天我们发现其实刷新一个item的粒度还是太大了,我们还可以对item的每个部位进行拆解,获得更细粒度的刷新。
以上是关于聊聊RecyclerView的像素级刷新的主要内容,如果未能解决你的问题,请参考以下文章
聊聊RecyclerView新出的ConcatAdapter如何使用
聊聊RecyclerView新出的ConcatAdapter如何使用