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
onBindViewHolder
are called and multipleAsyncTasks
per recyclable itemViewHolder
are started. - Even a new
onBindViewHolder
is triggered for the same item, the oldAsyncTask
keep running - Then
AsyncTasks
for the sameViewHolder
are completing one after another. - Each
AsyncTask
puts its own resulting bitmap to theImageView
.
My intentions would be:
- minimum: do not
setImageBitmap
from the outdatedAsyncTasks
- maximum: to stop already outdated
AsyncTasks
as 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.