前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >日常开发踩坑记-刷新抖动

日常开发踩坑记-刷新抖动

作者头像
韦东锏
发布2021-09-29 15:18:24
6970
发布2021-09-29 15:18:24
举报
文章被收录于专栏:Android码农Android码农

recyclerview刷新抖动踩坑记

问题:下拉刷新后,更新数据,页面顶部的UI会闪烁

看下目前的实现 整个页面是一个大的Recycle了View,顶部是一个item,内部也是采用RecyclerView实现的,代码如下

代码语言:javascript
复制
//顶部Item的Holder                                                           
private class TabMainCategoryHolder(                                      
    val binding: ItemTabMainCategoryTypeBinding,                          
    categoryItemClick: ItemClickListener                                  
) :                                                                       
    RecyclerView.ViewHolder(binding.root) {                               
                                                                          
    init {                                                                
        //Holder初始化的时候,就先设置好了layoutManger跟adapter                       
        binding.rvItemCategory.layoutManager =                            
            GridLayoutManager(binding.root.context, CATEGORY_SPAN_COUNT)  
        val categoryAdapter = TabMainCategoryAdapter()                    
        categoryAdapter.itemClickListener = categoryItemClick             
        binding.adapter = categoryAdapter                                 
    }                                                                     
                                                                          
    fun onBind(navigation: List<GoodsItem>?) {                            
        //在每次onBindViewHolder的时候,更新数据,刷新item                           
        val dataList = navigation ?: return                               
        binding.adapter?.dataList = dataList                              
        binding.adapter?.notifyDataSetChanged()                           
    }                                                                     
}                                                                         

然后看下TabMainCategoryAdapter的代码,也是非常的简单的adapter

代码语言:javascript
复制
class TabMainCategoryAdapter :
    BaseAdapter() {

    var dataList = listOf<GoodsItem>()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val binding = ItemTabMainCategoryItemBinding.inflate(
            LayoutInflater.from(parent.context),
            parent,
            false
        )
        return TabMainCategoryItemHolder(binding)
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if (holder is TabMainCategoryItemHolder) {
            holder.binding.item = dataList[position]
        }
    }

    override fun getItemCount(): Int {
        return dataList.size
    }
    
    class TabMainCategoryItemHolder(val binding: ItemTabMainCategoryItemBinding) :
        RecyclerView.ViewHolder(binding.root) {

    }
}

看到这里,基本能猜到闪烁的原因了吧

其实是在notifyDataChange后,holder在复用的时候,每个holder不是原来位置的holder了,所以重新绑定数据,发生了闪烁

我们验证下上面的猜想

代码语言:javascript
复制
class TabMainCategoryItemHolder(val binding: ItemTabMainCategoryItemBinding) :
    RecyclerView.ViewHolder(binding.root) {                                   
        //缓存上次的itemID的值                                                       
        var itemId = 0                                                        
}                                                                             

然后打印在onBind的时候,打印itemId信息

代码语言:javascript
复制
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {                    
    if (holder is TabMainCategoryItemHolder) {                                                     
        val item = dataList[position]                                                              
        //打印holder上次的id跟这次新的item的id的值                                                              
        Pug.d("TabMainCategoryAdapter", "last Id ${holder.itemId} current id ${item.id}")          
        holder.itemId = item.id                                                                    
        holder.binding.item = item                                                                 
    }                                                                                              
}                                                                                                  

然后下拉刷新,看下结果

代码语言:javascript
复制
//第一次刷新
D/TabMainCategoryAdapter: last Id 6 current id 1       
D/TabMainCategoryAdapter: last Id 7 current id 2       
D/TabMainCategoryAdapter: last Id 8 current id 3       
D/TabMainCategoryAdapter: last Id 9 current id 4       
D/TabMainCategoryAdapter: last Id 10 current id 5      
D/TabMainCategoryAdapter: last Id 0 current id 6       
D/TabMainCategoryAdapter: last Id 0 current id 7       
D/TabMainCategoryAdapter: last Id 0 current id 8       
D/TabMainCategoryAdapter: last Id 0 current id 9       
D/TabMainCategoryAdapter: last Id 0 current id 10      

//第二次刷新
D/TabMainCategoryAdapter: last Id 6 current id 1   
D/TabMainCategoryAdapter: last Id 7 current id 2   
D/TabMainCategoryAdapter: last Id 8 current id 3   
D/TabMainCategoryAdapter: last Id 9 current id 4   
D/TabMainCategoryAdapter: last Id 10 current id 5  
D/TabMainCategoryAdapter: last Id 0 current id 6   
D/TabMainCategoryAdapter: last Id 0 current id 7   
D/TabMainCategoryAdapter: last Id 0 current id 8   
D/TabMainCategoryAdapter: last Id 0 current id 9   
D/TabMainCategoryAdapter: last Id 0 current id 10  

可以发现,每次刷新前后,复用的holder都不是原来的holder,都需要重新设置图片跟文案,所以产生了闪烁

这里,问题来了,每次刷新,后面五个holder的初始ID都是0,说明每次刷新,都新建了五个viewHolder,viewHolder不是复用的?为什么还会不停的新建

在recyclerview的源码,可以发现答案

代码语言:javascript
复制
public static class RecycledViewPool {                  
    private static final int DEFAULT_MAX_SCRAP = 5;     

原来RecycledViewPool内部只最多缓存五个,看下具体缓存viewHolder的代码

代码语言:javascript
复制
/**                                                                                         
 * Add a scrap ViewHolder to the pool.                                                      
 * <p>                                                                                      
 * If the pool is already full for that ViewHolder's type, it will be immediately discarded.
 * @param scrap ViewHolder to be added to the pool.                                         
 */                                                                                         
public void putRecycledView(ViewHolder scrap) {                                             
    final int viewType = scrap.getItemViewType();                                           
    final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;       
    if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {                             //超过五个,不会加到缓存,会被清除掉  
        return;                                                                             
    }                                                                                       
    if (DEBUG && scrapHeap.contains(scrap)) {                                               
        throw new IllegalArgumentException("this scrap item already exists");               
    }                                                                                       
    scrap.resetInternal();                                                                  
    scrapHeap.add(scrap);                                                                   
}                                                                                           

相同的viewType的holder,最多缓存五个,超过的直接丢弃

因为这里一共有10个item,所以每次刷新调用notify后,原来的10个holder会被标记为失效,进入了缓存池,而缓存池最多容纳5个,在绑定新的数据,复用holder,还需要重新创建5个新的holder

分析到此,其实解决方案已经非常清楚了

解决方案1:把RecyclerViewPool的最大的缓存数量设置为10
代码语言:javascript
复制
rvItemCategory.recycledViewPool.setMaxRecycledViews(0, 10)

可以看下效果

除了上面的方案,其实还有另外一个修复方案,继续看下RecyclerView内部的源码

代码语言:javascript
复制
 if (viewHolder.isInvalid() && !viewHolder.isRemoved()       
         && !mRecyclerView.mAdapter.hasStableIds()) {        
     removeViewAt(index);                                    
     recycler.recycleViewHolderInternal(viewHolder);         
 } else {                                                    
     detachViewAt(index);                                    
     recycler.scrapView(view);                               
     mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
 }                                                           

可以发现,如果有设置stableId的话,就不会走recycledViewPool,而是进入scrap缓存

修复方案2:设置HasStableId为true
代码语言:javascript
复制
class TabMainCategoryAdapter :
    BaseAdapter() {

    init {
        //初始化的时候,设置stableId为true
        setHasStableIds(true)
    }

然后adapter需要返回的下每个item的id值

代码语言:javascript
复制
override fun getItemId(position: Int): Long { 
    return dataList[position].id.toLong()     
}                                             

这样也可以修复闪烁问题,两种对比,建议选用方案2

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-05-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Android码农 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 解决方案1:把RecyclerViewPool的最大的缓存数量设置为10
  • 修复方案2:设置HasStableId为true
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档