相关示例代码全部由kotlin编写,不了解kotlin的小伙伴也不需要太在意代码写法上的问题,主要了解思路即可。
在使用RecyclerView加载拥有大量图片的列表的时候,如果图片偏大或者网络环境不理想;那么用户滑动时很容易出现页面空白,需要等待一段时间才会显示图片的情况,这样用户体验明显是比较差的,需要做相关优化。
常规优化方案就是图片预加载;预先加载当前item之后若干item的图片到缓存中,滑动时直接加载缓存中图片,这样看起来会流畅很多。但是如果让我们自己来实现相关逻辑代码,还是略微麻烦;如果你正在使用的图片库是Glide,那么事情就简单多了,它提供了叫recyclerview-integration的相关拓展。
1. 添加依赖
implementation "com.github.bumptech.glide:glide:$glideVer"
implementation "com.github.bumptech.glide:recyclerview-integration:$glideVer"
2. 简单使用
非常简单,构造一个RecyclerViewPreloader添加给recyclerview就可以了
val preloadModelProvider = object : ListPreloader.PreloadModelProvider<String> {
// 图片url集合
val data: MutableList<String> = mutableListOf()
// 需要预加载的url,这里仅预加载后面一个item
override fun getPreloadItems(position: Int): MutableList<String> = data.subList(position, position + 1)
// 具体的预加载操作
override fun getPreloadRequestBuilder(item: String): RequestBuilder<*>? = GlideApp.with(recyclerView).load(item)
}
val sizeProvider = ViewPreloadSizeProvider<String>()
// maxPreLoad: 最大预加载数量
val preloader = RecyclerViewPreloader<String>(GlideApp.with(recyclerView),
preloadModelProvider, sizeProvider, maxPreLoad)
recyclerView.addOnScrollListener(preloader)
RecyclerViewPreloader需要四个参数:
- RequestManager或者context
- PreloadModelProvider用于提供数据源以及预加载具体操作(这里仅仅对图片进行了加载)
- sizeProvider用于提供预加载图片的尺寸,由于glide缓存图片的key和加载的图片宽高有关,所以需要预先提供确定的宽高。Glide提供了SizeProvider的两个实现,分别是FixedPreloadSizeProvider和ViewPreloadSizeProvider。
- ViewPreloadSizeProvider用于列表中图片大小统一的情况,需要在adapter的onCreateViewHolder或者onBindViewHolder中调用
sizeProvider.setView(预加载目标imageView)
,以便它自己测量目标viewde预加载合适尺寸的图片。 - FixedPreloadSizeProvider则用于确定尺寸,需要提供确定像素的宽高。
除此之外也可以自己实现SizeProvider,比如在用到瀑布流列表每个item中图片大小不一致的情况。
- ViewPreloadSizeProvider用于列表中图片大小统一的情况,需要在adapter的onCreateViewHolder或者onBindViewHolder中调用
- maxPreLoad参数用于指定触发预加载时最大预加载数量,比如position为1时触发预加载,如果1后面没有任何预加载项,就会预加载2-11项。
如果你开启了Glide日志,那么滑动列表时log应该是这个样子
附上adapter 示例
3. 一些问题
前面我们说到需要提供sizeProvider以便预加载时准确加载对应尺寸的图片,而官方文档中更是强调预加载所用的RequestBuilder必须和adapter中加载图片用的RequestBuilder所有配置都要一样,否则预加载将会无效。
但是实际上当你的磁盘缓存策略DiskCacheStrategy是DiskCacheStrategy.AUTOMATIC(默认)或者DiskCacheStrategy.ALL时,sizeProvider并不会影响什么,甚至提供和目标imageView相差很多的尺寸,预加载还是会成功。
因为这两种缓存策略在加载网络图片的时候都会把原始图片缓存到本地,读缓存的时候如果读不到目标尺寸的缓存图片,就会读取原始图片然后做相应转换再展示出来,总之是不会从网络加载了。
4. 稍微看一下相关源码
public final class RecyclerViewPreloader<T> extends RecyclerView.OnScrollListener {
private final RecyclerToListViewScrollListener recyclerScrollListener;
......
public RecyclerViewPreloader(@NonNull RequestManager requestManager,
@NonNull PreloadModelProvider<T> preloadModelProvider,
@NonNull PreloadSizeProvider<T> preloadDimensionProvider, int maxPreload) {
// 主要逻辑都在ListPreloader中
ListPreloader<T> listPreloader = new ListPreloader<>(requestManager, preloadModelProvider,
preloadDimensionProvider, maxPreload);
recyclerScrollListener = new RecyclerToListViewScrollListener(listPreloader);
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
// 监听recyclerview滑动然后调用RecyclerToListViewScrollListener的onScrolled方法
recyclerScrollListener.onScrolled(recyclerView, dx, dy);
}
}
public final class RecyclerToListViewScrollListener extends RecyclerView.OnScrollListener {
......
public RecyclerToListViewScrollListener(@NonNull AbsListView.OnScrollListener scrollListener) {
this.scrollListener = scrollListener;
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
// 获取最后一个可见item的位置
int firstVisible = layoutManager.findFirstVisibleItemPosition();
// 获取所有可见item数量
int visibleCount = Math.abs(firstVisible - layoutManager.findLastVisibleItemPosition());
int itemCount = recyclerView.getAdapter().getItemCount();
if (firstVisible != lastFirstVisible || visibleCount != lastVisibleCount
|| itemCount != lastItemCount) {
// 滑动超过一个item/可见item数量改变了/整体item数量改变了 调用ListPreloader的onScroll
scrollListener.onScroll(null, firstVisible, visibleCount, itemCount);
lastFirstVisible = firstVisible;
lastVisibleCount = visibleCount;
lastItemCount = itemCount;
}
}
}
上面这么多代码实际上就是监听了一下列表的滑动,经过判断过滤掉一些滑动操作,最终调用了ListPreloader的onScroll方法。
public class ListPreloader<T> implements AbsListView.OnScrollListener {
private final int maxPreload;
......
private int lastEnd;
private int lastStart;
private int lastFirstVisible = -1;
private int totalItemCount;
// true为上滑
private boolean isIncreasing = true;
@Override
public void onScroll(AbsListView absListView, int firstVisible, int visibleCount,
int totalCount) {
totalItemCount = totalCount;
// 向上滑动或向下滑动
if (firstVisible > lastFirstVisible) {
preload(firstVisible + visibleCount, true);
} else if (firstVisible < lastFirstVisible) {
preload(firstVisible, false);
}
lastFirstVisible = firstVisible;
}
private void preload(int start, boolean increasing) {
// 和上次滑动方向相反则取消所有预加载
if (isIncreasing != increasing) {
isIncreasing = increasing;
cancelAll();
}
preload(start, start + (increasing ? maxPreload : -maxPreload));
}
}
上面的代码主要是为了计算范围,比如当前可见最后一个item的position为10,maxPreLoad=10并且在上滑,则调用preload(11, 21)
private void preload(int from, int to) {
int start;
int end;
if (from < to) {
// lastEnd记录了上一次预加载到了哪个位置
start = Math.max(lastEnd, from);
end = to;
} else {
start = to;
end = Math.min(lastStart, from);
}
end = Math.min(totalItemCount, end);
start = Math.min(totalItemCount, Math.max(0, start));
if (from < to) {
// Increasing
for (int i = start; i < end; i++) {
// 真正开始进行预加载的地方,调用我们自己实现的预加载方法
preloadAdapterPosition(preloadModelProvider.getPreloadItems(i), i, true);
}
} else {
// Decreasing
for (int i = end - 1; i >= start; i--) {
preloadAdapterPosition(preloadModelProvider.getPreloadItems(i), i, false);
}
}
lastStart = start;
lastEnd = end;
}
}
上面的代码进一步计算了真正需要预加载的item,并通过preloadModelProvider拿到每个item需要预加载的数据进行预加载。比如当第一次触发预加载时最后一个可见item位置为10,最大预加载数为10,那么需要加载11 ~ 21(不包括21)的图片;第二次触发预加载时最后一个可见item位置为11,本需要加载12 ~ 22的item,但由于lastEnd = 21,所以只需要加载21,简单来说就是一直保证已经预加载的数量固定不变。
最后看一下preloadItem方法
private void preloadItem(@Nullable T item, int position, int perItemPosition) {
if (item == null) {
return;
}
int[] dimensions =
preloadDimensionProvider.getPreloadSize(item, position, perItemPosition);
if (dimensions == null) {
return;
}
RequestBuilder<Object> preloadRequestBuilder =
(RequestBuilder<Object>) preloadModelProvider.getPreloadRequestBuilder(item);
if (preloadRequestBuilder == null) {
return;
}
preloadRequestBuilder.into(preloadTargetQueue.next(dimensions[0], dimensions[1]));
}
通过PreloadSizeProvider.getPreloadSize获取到了每个item需要加载图片的目标尺寸,构造PreloadTarget;通过preloadModelProvider.getPreloadRequestBuilder拿到数据源,然后调用into方法将图片加载为相应尺寸,放入缓存。
5. 静默加载更多
既然已经做了列表预加载,何不顺便做一个静默加载更多数据呢,在用户上拉到一定位置的时候触发加载下一页数据,让用户感觉不到你做了分页,使看起来列表有无限长可以一直滑动。
直接贴一份相关代码
/**
* 滑动到阈值以后自动加载更多,阈值:[threshold]表示当页数据剩余数
*/
fun RecyclerView.attachLoadMore(threshold: Int = 12, loadMore: () -> Unit) {
addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (dy > 0) {
val adapter = recyclerView.adapter
adapter?.let {
val layoutManager = recyclerView.layoutManager
if (layoutManager is LinearLayoutManager) {
val last = it.itemCount - 1
val pos = layoutManager.findLastVisibleItemPosition()
if (pos < last) {
if (last - pos <= threshold) {
// 是否已经在加载中
var loading = false
val tag = recyclerView.getTag(R.id.tag_recyclerView_loadMore)
tag?.let {
if (tag is Boolean) {
loading = tag
}
}
if (!loading) {
recyclerView.setTag(R.id.tag_recyclerView_loadMore, true)
loadMore()
}
}else {
recyclerView.setTag(R.id.tag_recyclerView_loadMore, false)
}
}
}
}
}
}
})
}
比如总数据量为30,当用户上拉,滑动到30 - threshold位置时触发加载更多。
当然这只是一个简单示例,具体情况具体拓展即可。
博客新手,如果有错误和不足之处欢迎指出