recyclerview刷新抖动踩坑记
问题:下拉刷新后,更新数据,页面顶部的UI会闪烁
看下目前的实现 整个页面是一个大的Recycle了View,顶部是一个item,内部也是采用RecyclerView实现的,代码如下
//顶部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
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了,所以重新绑定数据,发生了闪烁
我们验证下上面的猜想
class TabMainCategoryItemHolder(val binding: ItemTabMainCategoryItemBinding) :
RecyclerView.ViewHolder(binding.root) {
//缓存上次的itemID的值
var itemId = 0
}
然后打印在onBind的时候,打印itemId信息
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
}
}
然后下拉刷新,看下结果
//第一次刷新
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的源码,可以发现答案
public static class RecycledViewPool {
private static final int DEFAULT_MAX_SCRAP = 5;
原来RecycledViewPool内部只最多缓存五个,看下具体缓存viewHolder的代码
/**
* 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
分析到此,其实解决方案已经非常清楚了
rvItemCategory.recycledViewPool.setMaxRecycledViews(0, 10)
可以看下效果
除了上面的方案,其实还有另外一个修复方案,继续看下RecyclerView内部的源码
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缓存
class TabMainCategoryAdapter :
BaseAdapter() {
init {
//初始化的时候,设置stableId为true
setHasStableIds(true)
}
然后adapter需要返回的下每个item的id值
override fun getItemId(position: Int): Long {
return dataList[position].id.toLong()
}
这样也可以修复闪烁问题,两种对比,建议选用方案2