Android RecyclerView: Async image-loading

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/26525999/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-20 11:42:41  来源:igfitidea点击:

RecyclerView: Async image-loading

androidandroid-asynctaskandroid-recyclerview

提问by Paul Woitaschek

Im using RecyclerViewto display a list containing an imageView. To make the UI more fluently, I load 58dp thumbnails saved on sd card into these imageViews with an asyncTask.

Im using RecyclerViewto display a list containing an imageView. To make the UI more fluently, I load 58dp thumbnails saved on sd card into these imageViews with an asyncTask.

The problem is, that once a childViewcomes in visual display, an old image from another data is being reused and then replaced once the AsyncTaskfinishes. I can stop the shuffling by setting the imageViewbitmap to null in onPreExecute.

The problem is, that once a childViewcomes in visual display, an old image from another data is being reused and then replaced once the AsyncTaskfinishes. I can stop the shuffling by setting the imageViewbitmap to null in onPreExecute.

Is there a way to really reuse old images or do I really have to load the images from sd-card each time a new Viewcomes in place? This makes view quite ugly because either there are wrong images first or the image is plain white.

Is there a way to really reuse old images or do I really have to load the images from sd-card each time a new Viewcomes in place? This makes view quite ugly because either there are wrong images first or the image is plain white.

回答by MLProgrammer-CiM

Due to view reuse you'll fetch views with content already on them, this was a problem on ListViewstoo if you were using the ViewHolderpattern, which you should.

Due to view reuse you'll fetch views with content already on them, this was a problem on ListViewstoo if you were using the ViewHolderpattern, which you should.

There are two solutions here, the good practice and the bad hack:

There are two solutions here, the good practice and the bad hack:

  • In the good practice you set your ImageViewto display nothing at the beginning of bindViewHolder(VH holder, int position)using setDrawable(null)or similar.

  • In the bad hack you wouldn't recycle/reuse views, not enforcing the ViewHolderpattern, and you'd inflate it every time, but that's only allowed in ListViewand other old components.

  • In the good practice you set your ImageViewto display nothing at the beginning of bindViewHolder(VH holder, int position)using setDrawable(null)or similar.

  • In the bad hack you wouldn't recycle/reuse views, not enforcing the ViewHolderpattern, and you'd inflate it every time, but that's only allowed in ListViewand other old components.

回答by Gunhan

You should check the universal image loader. It has memory cache, disk cache and it loads your images asynchronously so doesn't block the ui. You can set default image and/or failed to fetch image etc. It can sample down your image to decrease the memory footprint of the bitmap. I really recommend you to use it for images.

You should check the universal image loader. It has memory cache, disk cache and it loads your images asynchronously so doesn't block the ui. You can set default image and/or failed to fetch image etc. It can sample down your image to decrease the memory footprint of the bitmap. I really recommend you to use it for images.

Do not disable recyclable for your case because it is pointless. Images must be recycled because their bitmap drawables generate very high memory overload if not properly sampled.

Do not disable recyclable for your case because it is pointless. Images must be recycled because their bitmap drawables generate very high memory overload if not properly sampled.

Sample usage in RecyclerViewAdapter:

Sample usage in RecyclerViewAdapter:

@Override
public void onBindViewHolder(CustomViewHolder viewHolder, int position) {
    String imageUri = "";//local or remote image uri address
    //viewHolder.imgView: reference to your imageview
    //before you call the displayImage you have to 
    //initialize imageloader in anywhere in your code for once.   
    //(Generally done in the Application class extender.)
    ImageLoader.getInstance().displayImage(imageUri, viewHolder.imgView);
}

edit: Nowadays, I consider Glide as my main image loading and caching library. You can use it like this:

edit: Nowadays, I consider Glide as my main image loading and caching library. You can use it like this:

Glide.with(context)
    .load(imageUri)
    .placeholder(R.drawable.myplaceholder)
    .into(imageView);

回答by Sebas LG

You should cancel the old request before starting a new one, but regardless of cancelling you can still show the wrong image if both images loaded more or less at the same time on the same container/view holder that has been recycled (happens easily with fast scroll and small images).

You should cancel the old request before starting a new one, but regardless of cancelling you can still show the wrong image if both images loaded more or less at the same time on the same container/view holder that has been recycled (happens easily with fast scroll and small images).

The solution is to:

The solution is to:

  1. Store some unique identifier in the View Holder during onBindViewHolder (this happens synchronously so if VH is recycled this will be overwritten)
  2. Then load the image asynchronously (with AsynchTask, RxJava, etc) and pass this unique id in the async call for reference
  3. Finally, in the image loaded method for post-processing (onPostExecute for AsyncTasks), check that the id passed in the async request is the same as the current id present in the View Holder.
  1. Store some unique identifier in the View Holder during onBindViewHolder (this happens synchronously so if VH is recycled this will be overwritten)
  2. Then load the image asynchronously (with AsynchTask, RxJava, etc) and pass this unique id in the async call for reference
  3. Finally, in the image loaded method for post-processing (onPostExecute for AsyncTasks), check that the id passed in the async request is the same as the current id present in the View Holder.

Example loading icons from apps in background with RxJava:

Example loading icons from apps in background with RxJava:

 public void loadIcon(final ImageView appIconView, final ApplicationInfo appInfo, final String uniqueAppID) {
    Single.fromCallable(() -> {
            return appIconView.getContext().getPackageManager().getApplicationIcon(appInfo);
        })
          .subscribeOn(Schedulers.computation())
          .observeOn(AndroidSchedulers.mainThread())
          .subscribe( drawable -> {
                 if (uniqueAppID.equals(mUniqueAppID)) { // Show always the correct app icon
                    appIconView.setImageDrawable(drawable);
                 }
             }
         );
}

Here mUniqueAppID is a field of the view holder changed by onBindViewHolder

Here mUniqueAppID is a field of the view holder changed by onBindViewHolder

回答by Hamidreza Hosseinkhani

you must cancel old request in "onBindViewHolder" method:

you must cancel old request in "onBindViewHolder" method:

try{
        ((SpecialOfferViewHolder)viewHolder).imageContainer.cancelRequest();
}catch(Exception e) {

}

remember to save image container in the viewHolder:

remember to save image container in the viewHolder:

public void onResponse(ImageContainer response, boolean arg1) {
                ((SpecialOfferViewHolder)viewHolder).imageContainer=response;

}

}

回答by Dieguinho

I would add to the good practice:

I would add to the good practice:

In the good practice you set your ImageView to display nothing at the beginning of bindViewHolder(VH holder, int position) using setDrawable(null) or similar.

In the good practice you set your ImageView to display nothing at the beginning of bindViewHolder(VH holder, int position) using setDrawable(null) or similar.

not to display nothing, but to display a loader image so to give the user a feedback that theres some processing going on in that view and in a little while it will see the results. Seeing just a blank view is not good practice, you need to give feedback to the user.

not to display nothing, but to display a loader image so to give the user a feedback that theres some processing going on in that view and in a little while it will see the results. Seeing just a blank view is not good practice, you need to give feedback to the user.

回答by Poras Bhardwaj

You should use Picasso. A powerful image downloading and caching library for Android.Easy to use and a powerful library. Using this library you can fetch images asynchronously or synchronously from Resources, assets, files, content providers.

You should use Picasso. A powerful image downloading and caching library for Android.Easy to use and a powerful library. Using this library you can fetch images asynchronously or synchronously from Resources, assets, files, content providers.

回答by Lisa Anne

What MLProgrammer says is quite correct.

What MLProgrammer says is quite correct.

The solution, fortunately, it's easy: stop the recycling

The solution, fortunately, it's easy: stop the recycling

holder.setIsRecyclable(false);


This choice has its consequences, since doing what I suggest above inhibits one of RecyclerView main purposes: recycling.

This choice has its consequences, since doing what I suggest above inhibits one of RecyclerView main purposes: recycling.

  • Your RecyclerView will be slower to scroll (a new Holder has to be created every time, inflating again from the resources);
  • your usage of memory will be bigger.
  • Your RecyclerView will be slower to scroll (a new Holder has to be created every time, inflating again from the resources);
  • your usage of memory will be bigger.