最近在项目中使用 RecyclerView
+ StaggeredGridLayoutManager
实现了一个瀑布流加载图片的需求,遇到一些问题并成功解决,在此记录一下。
如何做到宽度固定,根据图片比例来设置高度
如果仅对 ImageView
设置 android:scaleType="fitCenter"
, 图片高度是无法根据图片比例动态确定的。
解决方案:
预读并缓存图片头部信息拿到图片尺寸(可以使用 BitmapFactory.Options ,因为只是读取头部字节所以效率并不会有太大影响),然后在 onBindViewHolder
中动态修改 ImageView
的 layoutParams
(根据图片比例和固定宽度计算出高度,并修改layoutParams
)。
注意:不要在 Glide 异步获取图片大小时才动态修改 ImageView
的 layoutParams
,这会导致 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();
}
这种方案在实际使用过程中发现:
- 会造成图片闪烁
- 列表的顺序还是发生了变化,往回滑动时填充的列表项和原本应该填充的列表项不符,是按照列表顺序填充的(排序依旧是错误的,并没有实际解决问题)
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 的 position
与 spanIndex
之间的映射,即可以通过 item 的 position
从 mLazySpanLookup
中获取到对应的 spanIndex
顺着 notifyDataSetChanged
的调用链,最终会调用 StaggeredGridLayoutManager.onItemsChanged()
,其中有一行重要代码 mLazySpanLookup.clear()
,对 mLazySpanLookup
做clear操作,将存储有 spanIndex
的 mData
数组中元素都置为了LayoutParams.INVALID_SPAN_ID
无效,至此,当查找 item 的spanIndex
时,因为无效了,所以重新生成了一个新 span,由此导致 item 所处在的列出现了变化,这就是重排序。
notifyItemRangeChanged不会造成列表重排序
查看源码可以发现,notifyItemRangeChanged
不会清除 spanIndex
,刷新时依然复用之前的spanIndex
,所以不会导致顺序的变化
总结
使用 notifyItemRangeChanged
替代 notifyDataSetChanged
进行列表刷新,可以避免重排序,是由于 notifyDataSetChanged
做列表刷新时,会导致item
的spanIndex
重新进行计算,item
所在列的位置出现了变化,导致了重排序,而notifyItemRangeChanged
依然是使用之前的spanIndex
,所以使用notifyItemRangeChanged
可以避免列表出现重排序的问题。
另外,可以看到,第一个方案中,staggeredGridLayoutManager.invalidateSpanAssignments()
方法也调用了mLazySpanLookup.clear()
,所以显然也会导致列表重排序。