聊聊RecyclerView的像素级刷新

Posted microhex

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了聊聊RecyclerView的像素级刷新相关的知识,希望对你有一定的参考价值。

需求来源

当前我们所做的内容是施工管理的工具APP,有非常重的聊天系统,而且聊天系统中由于项目数比较大,大到什么概念呢?一个中层管理手中大概存在500个项目,每个项目分为施工群业主群,然后加上公司OA群,设计群,管理群,大概在1000个群左右,当然你可能会说,1000个群也不算太大吧,我微信现在的群都有上千个,的确是这么个理,但是我们这1000个群每天都在发消息,业主要求推动施工的进程,假设每个群每天发10个消息,我们大概每天就有1w条消息,这是个很惊人的问题。所以问题就来,在上千个列表中,对RecyclerView的Item的刷新就变得很重要了。

具体环境分析

首先上图,我们来看看一般的聊天中需要显示哪些信息呢?


在上面的聊天item中,我们的动态数据有如下几个:

  1. 是否置顶 [置顶背景变灰]
  2. 头像更新 [有新的群成员进来,头像就会重组刷新]
  3. [是否有人@我]
  4. 免打扰
  5. 如果有免打扰,则消息未读数目变为小红点
  6. 不存在免打扰,则显示未读数目
  7. 消息时间
  8. 消息内容
  9. 群名称
  10. 关联的项目状态 [延期/结束/正常/未开始]

其实一个聊天还是有非常多的细节的,简简单单看一个聊天列表的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如何使用

android recyclerview添加了一个数据怎么刷新

RecyclerView局部刷新

RecyclerView底部刷新实现具体解释

RecyclerView 刷新踩坑记