Image flickering while scrolling in RecyclerView
Asked Answered
O

3

8

I'm trying to display a list of songs found on the device requesting data directly from the MediaStore. I'm using a RecyclerView and an adapter that uses a CursorAdapter to get data from MediaStore. When adapter's onBindViewHolder is called, the request is passed to the bindView function of the CursorAdapter the all visual elements are set.

public class ListRecyclerAdapter3 extends RecyclerView.Adapter<ListRecyclerAdapter3.SongViewHolder> {

    // PATCH: Because RecyclerView.Adapter in its current form doesn't natively support
    // cursors, we "wrap" a CursorAdapter that will do all teh job
    // for us
    public MediaStoreHelper mediaStoreHelper;
    CustomCursorAdapter mCursorAdapter;
    Context mContext;


    public class SongViewHolder extends RecyclerView.ViewHolder {

        public TextView textItemTitle;
        public TextView textItemSub;
        public ImageView imgArt;

        public int position;
        public String album_id;
        public String path_art;
        public String path_file;

        public SongViewHolder(View v) {
            super(v);
            textItemTitle = (TextView) v.findViewById(R.id.textItemTitle);
            textItemSub = (TextView) v.findViewById(R.id.textItemSub);
            imgArt = (ImageView) v.findViewById(R.id.imgArt);
        }
    }

    private class CustomCursorAdapter extends CursorAdapter {

        public CustomCursorAdapter(Context context, Cursor c, int flags) {
            super(context, c, flags);
        }

        @Override
        public View newView(final Context context, Cursor cursor, ViewGroup parent) {

            View v = LayoutInflater.from(parent.getContext())
                    .inflate(R.layout.song_item, parent, false);

            final SongViewHolder holder = new SongViewHolder(v);

            v.setTag(holder);

            return v;
        }

        @Override
        public void bindView(View view, Context context, Cursor cursor) {
            SongViewHolder holder = (SongViewHolder) view.getTag();

            holder.position = cursor.getPosition();

            holder.textItemTitle.setText(cursor.getString(cursor.getColumnIndex("title")));
            holder.textItemSub.setText(cursor.getString(cursor.getColumnIndex("artist")));

            holder.album_id = cursor.getString(cursor.getColumnIndex("album_id"));
            holder.path_file = cursor.getString(cursor.getColumnIndex("_data"));

            Picasso.with(holder.imgArt.getContext())
                    .cancelRequest(holder.imgArt);
            holder.imgArt.setImageDrawable(null);

            new DownloadImageTask(mediaStoreHelper, context, holder.imgArt).execute(holder.album_id);
        }

    }

    private class DownloadImageTask extends AsyncTask<String, String, String> {

        private MediaStoreHelper mediaStoreHelper;
        private ImageView imageView;
        private Context context;

        public DownloadImageTask(MediaStoreHelper mediaStoreHelper, Context context, ImageView imageView)
        {
            this.mediaStoreHelper = mediaStoreHelper;
            this.context = context;
            this.imageView = imageView;
        }
        @Override
        protected String doInBackground(String... ids) {
            return mediaStoreHelper.getAlbumArtPath(ids[0]);
        }

        protected void onPostExecute(String result) {
            Picasso.with(context)
                    .load(new File(result))
                    .placeholder(R.drawable.ic_music)
                    .fit()
                    .into(imageView);
        }
    }

    @Override
    public void onBindViewHolder(SongViewHolder holder, int position) {
        // Passing the binding operation to cursor loader
        mCursorAdapter.getCursor().moveToPosition(position);
        mCursorAdapter.bindView(holder.itemView, mContext, mCursorAdapter.getCursor());
    }

    @Override
    public SongViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // Passing the inflater job to the cursor-adapter
        View v = mCursorAdapter.newView(mContext, mCursorAdapter.getCursor(), parent);
        return new SongViewHolder(v);
    }
}

The problematic part is image loading with is made of two parts:

  • With the albumId I got from the Cursor, I need use ContentResolver to get album art file path
  • Load the image into the ImageView using the file path

These two passages need to be done in background otherwise the scrolling will become very laggy. In the bindView function I called a AsyncTask the does the job, but the problem is that, while scrolling fast, several image requests are elaborated and this is the result:

enter image description here

As you can see from the code I tried to cancel pending Picasso's requests on a specific ImageView, but that's not enough. Can this problem be fixed?

Oina answered 28/7, 2016 at 9:16 Comment(2)
I think you can put onScroll listener in the RecyclerView and only start image loading when it stopped scrolling.Bigeye
@Bigeye nice thought, I'll try itOina
O
3

I solved by adding a field in the ViewHolder containing the AsyncTask relative to that item. In the bindView function I fisrt set the AsyncTask.cancel(true) and, inside the task I made a check isCancelled() before applying the retrieved image using Picasso.with(...).load(...). This itself solved flickering.

bindView

if(holder.downloadImageTask != null)
      holder.downloadImageTask.cancel(true);

      holder.downloadImageTask = (DownloadImageTask) new DownloadImageTask(mediaStoreHelper, context, holder.imgArt).execute(holder.album_id);

AsyncTask

private class DownloadImageTask extends AsyncTask<String, String, String> {

        private MediaStoreHelper mediaStoreHelper;
        private ImageView imageView;
        private Context context;

        public DownloadImageTask(MediaStoreHelper mediaStoreHelper, Context context, ImageView imageView)
        {
            this.mediaStoreHelper = mediaStoreHelper;
            this.context = context;
            this.imageView = imageView;
        }
        @Override
        protected String doInBackground(String... ids) {
            return mediaStoreHelper.getAlbumArtPath(ids[0]);
        }

        protected void onPostExecute(String result) {
            if(!isCancelled())
                Picasso.with(context)
                        .load(new File(result))
                        .placeholder(R.drawable.ic_music)
                        .fit()
                        .into(imageView);
        }
    }

For the sake of completeness, this also remove image from recycled items and set a placeholder.

@Override
public void onViewRecycled(SongViewHolder holder) {
    super.onViewRecycled(holder);
    Picasso.with(holder.itemView.getContext())
            .cancelRequest(holder.imgArt);
    Picasso.with(holder.itemView.getContext())
            .load(R.drawable.ic_music)
            .fit()
            .into(holder.imgArt);
}

This solution made me think that the problem was the amount of time intercurring inside the AsyncTask between image retrieval from MediaStore and the time when the image is actually applied into the ImageView from Picasso.

Oina answered 28/7, 2016 at 10:26 Comment(1)
just onViewRecycled is enough to solve issue no need to change in asynch taskBespeak
T
1

Comment these line

Flickering is occurring because bind is not call only once for single item so it call again and again for single row and you are setting null every-time and also setting view on it. producing flickering.

 Picasso.with(holder.imgArt.getContext())
                    .cancelRequest(holder.imgArt);
            holder.imgArt.setImageDrawable(null);
Tennyson answered 28/7, 2016 at 9:19 Comment(4)
Unfortunately is not the solution. The flickering is occurring because of multiple requests over the same ImageView, not because I'm setting to null.Oina
flickering is occuring becasue bind is not call only once for single item so it call again and again for single row and you are setting null every-time and also setting view on it. producing flickering.Tennyson
First part of the sense is right, second isn't. By the way I tried as you suggested and the result is exactly the same.Oina
Commenting those lines will remove flickering, but won`t remove reloading one image then the other- I guess this is his real problemBigeye
M
1

The async task you are using is completely wrong.

Firstly you should never execute an anonymous async task.

Secondly, get rid of this async task in the adapter, because it seems to be causing the problem.

Get the data for the adapter, then show it.

Btw, terminating picasso is better done this way:

       @Override public void onViewRecycled(VH holder) {
            super.onViewRecycled(holder);
            Picasso.with(holder.itemView.getContext())
                   .cancelRequest(holder.getImageView());
        }
Menken answered 28/7, 2016 at 9:38 Comment(2)
Well, I use an AsyncTask to retrieve the image from the MediaStore. This process seems to be taking some time and makes scrolling very slowOina
Just call this mp3Adapter.setHasStableIds(true); even if you are using AsyncTask. Give it a try. Also override setHasStableIds() method in your Adapter class.Grackle

© 2022 - 2024 — McMap. All rights reserved.