Issue
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:
- Many
onBindViewHolderare called and multipleAsyncTasksper recyclable itemViewHolderare started. - Even a new
onBindViewHolderis triggered for the same item, the oldAsyncTaskkeep running - Then
AsyncTasksfor the sameViewHolderare completing one after another. - Each
AsyncTaskputs its own resulting bitmap to theImageView.
My intentions would be:
- minimum: do not
setImageBitmapfrom the outdatedAsyncTasks - maximum: to stop already outdated
AsyncTasksas soon as possible to save system resources
I would appreciate to hear some hints or maybe solutions for this problem.
Solution
Here is the solution I came up with.
Stop flickering
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);
Improve performance
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);
Complete code
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.
Answered By - user10475643

0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.