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:
Cursor
, I need use ContentResolver
to get album art file pathImageView
using the file pathThese 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:
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?
Actually this is due to Animation in RecyclerView. As stated in official documentation of RecyclerView that RecyclerView uses DefaltItemAnimator by default. Which means if you doesn’t specify ItemAnimator to RecyclerView then it still got animations.
99% of the time, this is a problem with recyclerview recycling the views. The 1% percent left could come from human error. Let’s quickly revisit how RecyclerView works. How does RecyclerView work? RecyclerView can easily be called the better ListView.
In RecyclerView if you’re using ImageView to display an image from your server in your RecyclerView items then specify the constant size for your ImageView. If the size of ImageView in RecyclerView items is not fixed then RecyclerView will take some time to load and adjust the RecyclerView item size according to the size of Image.
So to improve the performance of our RecyclerView we should keep the size of our ImageView to make it RecyclerView load faster. 2. Avoid using NestedView While creating an item for RecyclerView avoid using NestedView for RecyclerView item.
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.
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);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With