如何在 RecyclerView 中显示 "heavy" 张图片?
How to display "heavy" images in a RecyclerView?
我需要在 RecyclerView 中显示一些图像。
当用户滚动列表时,这些图像应该“即时”呈现。渲染每张图像需要很长时间:50-500ms。在向用户显示图像之前,会显示一个进度条。
由于渲染时间较长,此部分被放入 AsyncTask 中。
查看下面的代码:
class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
class ViewHolder extends RecyclerView.ViewHolder {
ImageView myImg;
ProgressBar myProgressBar;
public ViewHolder(View view) {
super(itemView);
myImg = view.findViewById(R.id.myImg);
myProgressBar = view.findViewById(R.id.myProgressBar);
}
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.myProgressBar.setVisibility(View.VISIBLE);
AsyncTask.execute(() -> {
Bitmap bitmap = longRenderingFunction();
holder.myImg.post(() -> {
holder.myImg.setImageBitmap(bitmap);
holder.myProgressBar.setVisibility(View.GONE);
如果用户缓慢滚动列表,例如每秒 1-2 个屏幕,图像已正确加载。有时可以注意到进度条,它很快就消失了。
但是如果用户滚动速度非常快,图像会出现然后被替换,有时会替换两次:
一般来说,在快速滚动时很明显:
- 许多
onBindViewHolder
被调用,每个可回收物品 ViewHolder
被启动多个 AsyncTasks
。
- 即使同一物品触发新的
onBindViewHolder
,旧的AsyncTask
保持运行
- 然后
AsyncTasks
同样ViewHolder
正在陆续完成。
- 每个
AsyncTask
将其自己的结果位图放入 ImageView
。
我的意图是:
- 最小值:不要
setImageBitmap
从过时的AsyncTasks
- 最大:尽快停止已经过时的
AsyncTasks
以节省系统资源
如果能听到一些提示或解决此问题的方法,我将不胜感激。
这是我想出的解决方案。
停止闪烁
第一部分很简单:
引入了一个简单的索引:
int loadingPosition;
在onBindViewHolder
开头保存当前位置:
holder.loadingPosition = position;
在切换位图之前,会检查当前位置与上次启动位置的对比。如果已经启动了一个新任务,索引已经被预先更改,那么图像将不会被更新:
if (holder.loadingPosition == position) {
holder.myImg.setImageBitmap(bitmap);
提高性能
为了防止任务在不需要时进一步 运行,引入了 Future
对象。它允许管理启动的任务,而 AsyncTask
不允许:
Future<?> future;
通过ExecutorService
而不是AsyncTask
启动异步任务:
holder.future = executor.submit(() -> { ...
正在停止 运行ning 任务,如果它仍然是 运行ning:
if ((holder.future != null) && (!holder.future.isDone()))
holder.future.cancel(true);
完整代码
整个代码如下所示:
class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
private ExecutorService executor = Executors.newSingleThreadExecutor();
class ViewHolder extends RecyclerView.ViewHolder {
ImageView myImg;
ProgressBar myProgressBar;
int loadingPosition;
Future<?> future;
public ViewHolder(View view) {
super(itemView);
myImg = view.findViewById(R.id.myImg);
myProgressBar = view.findViewById(R.id.myProgressBar);
}
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.loadingPosition = position;
if ((holder.future != null) && (!holder.future.isDone()))
holder.future.cancel(true);
holder.myProgressBar.setVisibility(View.VISIBLE);
holder.future = executor.submit(() -> {
Bitmap bitmap = longRenderingFunction();
holder.myImg.post(() -> {
if (holder.loadingPosition == position) {
holder.myImg.setImageBitmap(bitmap);
holder.myProgressBar.setVisibility(View.GONE);
也许同时使用这两种方法有点矫枉过正,但它提供了一定程度的保险。
我需要在 RecyclerView 中显示一些图像。 当用户滚动列表时,这些图像应该“即时”呈现。渲染每张图像需要很长时间:50-500ms。在向用户显示图像之前,会显示一个进度条。
由于渲染时间较长,此部分被放入 AsyncTask 中。
查看下面的代码:
class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
class ViewHolder extends RecyclerView.ViewHolder {
ImageView myImg;
ProgressBar myProgressBar;
public ViewHolder(View view) {
super(itemView);
myImg = view.findViewById(R.id.myImg);
myProgressBar = view.findViewById(R.id.myProgressBar);
}
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.myProgressBar.setVisibility(View.VISIBLE);
AsyncTask.execute(() -> {
Bitmap bitmap = longRenderingFunction();
holder.myImg.post(() -> {
holder.myImg.setImageBitmap(bitmap);
holder.myProgressBar.setVisibility(View.GONE);
如果用户缓慢滚动列表,例如每秒 1-2 个屏幕,图像已正确加载。有时可以注意到进度条,它很快就消失了。
但是如果用户滚动速度非常快,图像会出现然后被替换,有时会替换两次:
一般来说,在快速滚动时很明显:
- 许多
onBindViewHolder
被调用,每个可回收物品ViewHolder
被启动多个AsyncTasks
。 - 即使同一物品触发新的
onBindViewHolder
,旧的AsyncTask
保持运行 - 然后
AsyncTasks
同样ViewHolder
正在陆续完成。 - 每个
AsyncTask
将其自己的结果位图放入ImageView
。
我的意图是:
- 最小值:不要
setImageBitmap
从过时的AsyncTasks
- 最大:尽快停止已经过时的
AsyncTasks
以节省系统资源
如果能听到一些提示或解决此问题的方法,我将不胜感激。
这是我想出的解决方案。
停止闪烁
第一部分很简单:
引入了一个简单的索引:
int loadingPosition;
在onBindViewHolder
开头保存当前位置:
holder.loadingPosition = position;
在切换位图之前,会检查当前位置与上次启动位置的对比。如果已经启动了一个新任务,索引已经被预先更改,那么图像将不会被更新:
if (holder.loadingPosition == position) {
holder.myImg.setImageBitmap(bitmap);
提高性能
为了防止任务在不需要时进一步 运行,引入了 Future
对象。它允许管理启动的任务,而 AsyncTask
不允许:
Future<?> future;
通过ExecutorService
而不是AsyncTask
启动异步任务:
holder.future = executor.submit(() -> { ...
正在停止 运行ning 任务,如果它仍然是 运行ning:
if ((holder.future != null) && (!holder.future.isDone()))
holder.future.cancel(true);
完整代码
整个代码如下所示:
class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
private ExecutorService executor = Executors.newSingleThreadExecutor();
class ViewHolder extends RecyclerView.ViewHolder {
ImageView myImg;
ProgressBar myProgressBar;
int loadingPosition;
Future<?> future;
public ViewHolder(View view) {
super(itemView);
myImg = view.findViewById(R.id.myImg);
myProgressBar = view.findViewById(R.id.myProgressBar);
}
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.loadingPosition = position;
if ((holder.future != null) && (!holder.future.isDone()))
holder.future.cancel(true);
holder.myProgressBar.setVisibility(View.VISIBLE);
holder.future = executor.submit(() -> {
Bitmap bitmap = longRenderingFunction();
holder.myImg.post(() -> {
if (holder.loadingPosition == position) {
holder.myImg.setImageBitmap(bitmap);
holder.myProgressBar.setVisibility(View.GONE);
也许同时使用这两种方法有点矫枉过正,但它提供了一定程度的保险。