mspnr
mspnr

Reputation: 476

How to display "heavy" images in a RecyclerView?

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:

example

In general it is clear, that on fast scrolling:

My intentions would be:

I would appreciate to hear some hints or maybe solutions for this problem.

Upvotes: 1

Views: 1428

Answers (1)

mspnr
mspnr

Reputation: 476

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.

Upvotes: 2

Related Questions