Reputation: 476
I need to display some images in a RecyclerView. These images should be rendered "on the fly" as user is scrolling the list. Rendering each image takes a long time: 50-500ms. Before the image is displayed to the user, a progress bar is displayed.
Due to the long rendering time this part is placed into an AsyncTask.
See the code below:
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);
If the user is scrolling the list slowly, e.g. 1-2 screens per second, the images are loaded correctly. Sometimes one can notice the progress bar, which quickly disappears.
But if the user is scrolling very fast, the images appear and then are replaced, sometimes two times:
In general it is clear, that on fast scrolling:
onBindViewHolder
are called and multiple AsyncTasks
per recyclable item ViewHolder
are started.onBindViewHolder
is triggered for the same item, the old AsyncTask
keep runningAsyncTasks
for the same ViewHolder
are completing one after another.AsyncTask
puts its own resulting bitmap to the ImageView
.My intentions would be:
setImageBitmap
from the outdated AsyncTasks
AsyncTasks
as soon as possible to save system resourcesI would appreciate to hear some hints or maybe solutions for this problem.
Upvotes: 1
Views: 1428
Reputation: 476
Here is the solution I came up with.
The first part is pretty simple:
A simple index was introduced:
int loadingPosition;
At the beginning of onBindViewHolder
the current position is saved:
holder.loadingPosition = position;
Before switching the bitmap the current position is checked against the last started. If a new task has been already started, the index was changed beforehand, then the image will not be updated:
if (holder.loadingPosition == position) {
holder.myImg.setImageBitmap(bitmap);
To prevent the task to run further when not needed a Future
object was introduced. It allows to manage the started task, while AsyncTask
does not:
Future<?> future;
Start the async task via ExecutorService
instead of AsyncTask
:
holder.future = executor.submit(() -> { ...
Stopping the running task, if it is still running:
if ((holder.future != null) && (!holder.future.isDone()))
holder.future.cancel(true);
The whole code looks like that:
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);
Maybe using the both methods is a little bit overkill, but it provides some level of insurance.
Upvotes: 2