RecyclerView滚动时图像闪烁

8
我正在尝试从MediaStore直接请求数据来显示设备上找到的歌曲列表。我使用了一个RecyclerView和一个适配器,该适配器使用CursorAdapter从MediaStore获取数据。 当调用适配器的onBindViewHolder时,请求被传递给CursorAdapter的bindView函数,所有可视元素都被设置。
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);
    }
}

有问题的部分是由两个部分组成的图像加载:
  • 使用从Cursor获取的albumId,需要使用ContentResolver获取专辑艺术文件路径
  • 使用文件路径将图像加载到ImageView
这两个部分需要在后台完成,否则滚动会变得非常卡顿。在bindView函数中,我调用了一个执行此任务的AsyncTask,但问题在于,在快速滚动时,会处理多个图像请求,结果如下所示:

enter image description here

从代码中可以看出,我尝试取消挂起的Picasso请求,但这还不够。这个问题能解决吗?

1
我认为你可以在RecyclerView中放置onScroll监听器,并且只有在停止滚动时才开始加载图像。 - X3Btel
@X3Btel 很好的想法,我会尝试的。 - fillobotto
3个回答

3
我通过在ViewHolder中添加一个字段,该字段包含与该项相关的AsyncTask来解决了这个问题。在bindView函数中,我首先设置AsyncTask.cancel(true),然后在任务中使用Picasso.with(...).load(...)应用检索到的图像之前,进行了isCancelled()检查。这本身解决了闪烁问题。

bindView

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

      holder.downloadImageTask = (DownloadImageTask) 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) {
            if(!isCancelled())
                Picasso.with(context)
                        .load(new File(result))
                        .placeholder(R.drawable.ic_music)
                        .fit()
                        .into(imageView);
        }
    }

为了完整性,这也会从可回收的项目中删除图像并设置占位符。

@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);
}

这个解决方案让我想到问题出在AsyncTask中的时间,这段时间用来从MediaStore检索图像并将图像实际应用于ImageView中使用Picasso。

只需要在 onViewRecycled 中进行更改,就可以解决问题,无需更改异步任务。 - Jignesh Ansodariya

1

请注释这些代码行

闪烁是发生的原因是绑定(bind)不仅一次调用单个项目,而是为单个行再次调用它,您每次都将其设置为null,并在其上设置视图,从而产生闪烁。

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

1
不幸的是,这并不是解决方案。闪烁是因为在同一个ImageView上进行了多次请求,而不是因为我将其设置为空。 - fillobotto
闪烁是因为绑定只针对单个项目调用一次,因此它会一遍又一遍地为单行调用,并且每次都将其设置为 null,同时在其上设置视图,从而产生闪烁。 - Sohail Zahid
感觉的第一部分是正确的,第二部分不正确。顺便说一下,我按照你的建议尝试了一下,结果完全相同。 - fillobotto
1
注释掉这些行将消除闪烁,但不会消除先加载一张图片再加载另一张图片的问题——我想这才是他真正的问题。 - X3Btel

1
你正在使用的异步任务完全错误。
首先,您不应该执行匿名异步任务。
其次,在适配器中摆脱这个异步任务,因为它似乎引起了问题。
获取适配器的数据,然后显示它。
顺便说一句,终止 Picasso 最好是通过这种方式完成:
       @Override public void onViewRecycled(VH holder) {
            super.onViewRecycled(holder);
            Picasso.with(holder.itemView.getContext())
                   .cancelRequest(holder.getImageView());
        }

1
好的,我使用一个AsyncTask从MediaStore中检索图像。这个过程似乎需要一些时间,并使滚动变得非常缓慢。 - fillobotto
只需调用 mp3Adapter.setHasStableIds(true); 即可,即使您正在使用 AsyncTask。试一试吧。还要在您的 Adapter 类中重写 setHasStableIds() 方法。 - Subhojit Halder

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接