RecyclerView瀑布流踩坑与解决方案

2,771 阅读4分钟

最近在项目中使用 RecyclerView + StaggeredGridLayoutManager 实现了一个瀑布流加载图片的需求,遇到一些问题并成功解决,在此记录一下。

如何做到宽度固定,根据图片比例来设置高度

如果仅对 ImageView 设置 android:scaleType="fitCenter", 图片高度是无法根据图片比例动态确定的。

解决方案:

预读并缓存图片头部信息拿到图片尺寸(可以使用 BitmapFactory.Options ,因为只是读取头部字节所以效率并不会有太大影响),然后在 onBindViewHolder 中动态修改 ImageViewlayoutParams(根据图片比例和固定宽度计算出高度,并修改layoutParams)。

注意:不要在 Glide 异步获取图片大小时才动态修改 ImageViewlayoutParams,这会导致 ImageView 没有及时获取高度而出现空白,需要在Glide加载图片之前确定 ImageView 高度!

瀑布流图片错位(重排序)问题

某些情况下,列表往下滑动一段距离后,在往回滑动时,会发现瀑布流的布局出现了错位的情况,这其实是 StaggeredGridLayoutManager 在对列表重排序来消除空白间隙。

网上看大多解决方案都是这样:

// 设置GAP_HANDLING_NONE,屏蔽重排序机制
layoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_NONE);

// 解决屏蔽重排序机制导致的,滑动到列表顶部时顶部留白的问题
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        super.onScrollStateChanged(recyclerView, newState);
        staggeredGridLayoutManager.invalidateSpanAssignments();//重新布局
    }

    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);     
    }
});

public void invalidateSpanAssignments() {
    // 将spanIndex数组清空,进行重绘
    mLazySpanLookup.clear();
    requestLayout();
}

这种方案在实际使用过程中发现:

  1. 会造成图片闪烁
  2. 列表的顺序还是发生了变化,往回滑动时填充的列表项和原本应该填充的列表项不符,是按照列表顺序填充的(排序依旧是错误的,并没有实际解决问题)
  3. invalidateSpanAssignments 实现是重新绘制一次,由于是在滚动状态发生变化时调用,每次滚动都会造成多次重绘,资源浪费

查找资料,发现了一个更好的解决方案

使用notifyItemRangeChanged替代 notifyDataSetChanged 进行列表刷新(或者 notifyItemRangeInsert 等方法,流程和 notifyItemRangeChanged 一样)。

原因可从源码中获取,参考这篇文章 www.jianshu.com/p/d34075c0f…

首先明确,为什么重排序会导致出现错位?

RecyclerView 的回收复用机制,当列表项不可见时 ViewHolder 会被回收,当往下滑动一段距离后往回滑动,就会重新填充上面的布局,这时如果如果按照列表项顺序填充,就可能出现错位(假设只有两列,原本最上方左侧index是5,右侧是3,当下滑后往回,就可能出现左侧index是5,右侧填充4的情况)

notifyDataSetChanged会造成列表重排序

原因:notifyDataSetChanged 刷新列表时 spanIndex 索引发生了变化

spanIndex 是什么?

简单来说,通过spanIndex可以找到列表 item view对应的span,从而可找到item是在第几列

StaggeredGridLayoutManager 中,有个 mLazySpanLookup,其中存储 adapter 每一个 item 的 positionspanIndex 之间的映射,即可以通过 item 的 positionmLazySpanLookup 中获取到对应的 spanIndex

顺着 notifyDataSetChanged 的调用链,最终会调用 StaggeredGridLayoutManager.onItemsChanged(),其中有一行重要代码 mLazySpanLookup.clear() ,对 mLazySpanLookup 做clear操作,将存储有 spanIndexmData 数组中元素都置为了LayoutParams.INVALID_SPAN_ID 无效,至此,当查找 item 的spanIndex时,因为无效了,所以重新生成了一个新 span,由此导致 item 所处在的列出现了变化,这就是重排序。

notifyItemRangeChanged不会造成列表重排序

查看源码可以发现,notifyItemRangeChanged 不会清除 spanIndex,刷新时依然复用之前的spanIndex,所以不会导致顺序的变化

总结

使用 notifyItemRangeChanged 替代 notifyDataSetChanged 进行列表刷新,可以避免重排序,是由于 notifyDataSetChanged 做列表刷新时,会导致itemspanIndex重新进行计算,item所在列的位置出现了变化,导致了重排序,而notifyItemRangeChanged依然是使用之前的spanIndex,所以使用notifyItemRangeChanged可以避免列表出现重排序的问题。

另外,可以看到,第一个方案中,staggeredGridLayoutManager.invalidateSpanAssignments() 方法也调用了mLazySpanLookup.clear(),所以显然也会导致列表重排序。