58

I have implemented a RecyclerView that contains mainly images which is loading in through Picasso. My problem is that as soon as I scroll down or up the view, the placeholder image appears for approx. a second before it is replaced by the actual image. It scrolls very smoothly, but is practically unusable. This happens every time something scrolls off the screen.

Picasso is clearly caching the images to the disk, as they load faster than the initial load time, but they are definitely instantly reloading from the cache. What did i implement incorrectly?

My adapter code:

public class ExploreAdapter extends RecyclerView.Adapter<ExploreAdapter.ExploreViewHolder> {

    private List<ExploreItem> exploreItemList;
    private Context mContext;
    private int deviceWidth = PrefUtils.getDeviceWidth();

    public ExploreAdapter(Context context, List<ExploreItem> exploreItemList){
        this.mContext = context;
        this.exploreItemList = exploreItemList;
    }

    @Override
    public int getItemCount(){
        return exploreItemList.size();
    }

    @Override
    public void onBindViewHolder(ExploreViewHolder exploreViewHolder, int i){
        ExploreItem item = exploreItemList.get(i);
        exploreViewHolder.getvTitle().setText(item.getTitle());
        exploreViewHolder.getvUsername().setText(item.getUsername());
        exploreViewHolder.getvNumLikes().setText(item.getNbOfLikes());

        Picasso.with(mContext).load(item.getMediaLocation().trim())
                .resize(deviceWidth, deviceWidth)
                .centerCrop()
                .placeholder(R.drawable.profile_background)
                .into(exploreViewHolder.getvImage());

        Picasso.with(mContext).load(item.getUserProfilePicUrl().trim())
                .placeholder(R.drawable.profile_picture)
                .into(exploreViewHolder.getvProfilePic());
    }

    @Override
    public ExploreViewHolder onCreateViewHolder(ViewGroup viewGroup, int i){
        View itemView = LayoutInflater.
                from(viewGroup.getContext()).
                inflate(R.layout.explore_item, viewGroup, false);

        return new ExploreViewHolder(itemView);
    }

    public static class ExploreViewHolder extends RecyclerView.ViewHolder{
        private TextView  vTitle,
                          vUsername,
                          vNumLikes;

        private SquareImage vImage;
        private ImageView vProfilePic;

        public ExploreViewHolder(View v){
            super(v);
            this.vTitle = (TextView) v.findViewById(R.id.explore_item_title);
            this.vUsername = (TextView) v.findViewById(R.id.explore_item_username);
            this.vNumLikes = (TextView) v.findViewById(R.id.explore_item_number_likes);
            this.vImage = (SquareImage) v.findViewById(R.id.explore_item_image);
            this.vProfilePic = (ImageView) v.findViewById(R.id.explore_item_profile_picture);
        }

        public TextView getvTitle() {
            return vTitle;
        }

        public TextView getvUsername() {
            return vUsername;
        }

        public ImageView getvProfilePic() {
            return vProfilePic;
        }

        public SquareImage getvImage() {
            return vImage;
        }

        public TextView getvNumLikes() {
            return vNumLikes;
        }

        public void setvImage(SquareImage vImage) {
            this.vImage = vImage;
        }

        public void setvNumLikes(TextView vNumLikes) {
            this.vNumLikes = vNumLikes;
        }

        public void setvProfilePic(ImageView vProfilePic) {
            this.vProfilePic = vProfilePic;
        }

        public void setvTitle(TextView vTitle) {
            this.vTitle = vTitle;
        }

        public void setvUsername(TextView vUsername) {
            this.vUsername = vUsername;
        }
    }
}

Any help is appreciated.

Vivek Barai
  • 1,338
  • 13
  • 26
charrondev
  • 1,129
  • 2
  • 9
  • 13

9 Answers9

83

Setting recyclerView parameters as below solved my stutter problem :

recyclerView.setHasFixedSize(true);
recyclerView.setItemViewCacheSize(20);
recyclerView.setDrawingCacheEnabled(true);
recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);

i hope it helps someone else too.

ergunkocak
  • 3,334
  • 1
  • 32
  • 31
35

Use fit().

Picasso.with(context)
        .load(file)
        .fit()
        .centerCrop()
        .into(imageView);

The fit() actually resizes the image to fit into the imageView's bounds. This doesn't load the full image and smoothens the scrolling.

Mangesh
  • 5,491
  • 5
  • 48
  • 71
  • it gets even more slower if i use .fit().centerCrop() ? – umerk44 Apr 09 '16 at 19:27
  • 2
    Tried `.fit()` but `.resize().centerCrop()` worked much better. Faster result. – levibostian Nov 02 '16 at 23:05
  • @levibostian: `fit` internally uses `resize` on image to fit the exact size of `ImageView`. This explains it all - https://futurestud.io/tutorials/picasso-image-resizing-scaling-and-fit – Mangesh Nov 03 '16 at 06:23
11

I mixed the answer from @ergunkocak and @Mangesh Ghotage, using this works great:

recyclerView.setHasFixedSize(true);
recyclerView.setItemViewCacheSize(20);
recyclerView.setDrawingCacheEnabled(true);

And this on every image:

Picasso.with(context)
        .load(file)
        .fit()
        .into(imageView);

besides you could set Picasso to use a single instance like this:

Picasso.setSingletonInstance(picasso);

And finally enabling the cache indicators will give you clues on what's going wrong:

Picasso  
    .with(context)
    .setIndicatorsEnabled(true);

More about Picasso

lisandro101
  • 444
  • 5
  • 5
  • 1
    Picasso.with(context) .load(file) .fit() .into(imageView); This is saved my life..... Thanks. – Anandh Nov 16 '18 at 16:05
3

I have encountered this issue recently, and yes you are right. Picasso is a good library for showing images from sdcard but it has some issues regarding cache if you are fetching images online that is why you see a place holder image while scrolling. I tried different things and the issue was not resolved.

There is an alternate library i.e. "AQuery" which is very good for fetching and caching images from network.

http://code.google.com/p/android-query/

The main advantages of AQuery/Android Query is that you can clear the cache, no lag while scrolling and quite effective for caching images and not showing place holder images while scrolling.

I resolved my issue by removing picaaso and using Aquery. Its just a one line replacement and you are done with your issue.

(new AQuery(mContext)).id(exploreViewHolder.getvProfilePic()).image(item.getUserProfilePicUrl().trim(), true, true, device_width, R.drawable.profile_background, aquery.getCachedImage(R.drawable.profile_background),0);

Besides you can easily shift from picasso to AQuery as there is no such thing which is available in picasso but not in AQuery and the syntax is almost the same. So I would recommend you to use AQuery, until this is fixed by picasso.

Faizan Tariq
  • 351
  • 4
  • 14
1

I can't verify the correctness of this solution, but I was facing this problem as well and approached it following this idea:

@Override
public void onBindViewHolder(ViewHolder viewHolder, final int i) {

...

    if(mImageView.getDrawable() == null)
         Picasso.with(context)
            .load(path)
            .error(errorResId)
            .tag(tag)
            .into(mImageView);
...  
}

It is intended to forbid Picasso from loading anything unless the ImageView has nothing to display.

DFJ
  • 93
  • 2
  • 9
  • 4
    I don't think that this will work. Sure, will work for the first element in the RecyclerView, but to all the others, will display the same image. – 4gus71n Jan 26 '15 at 12:45
  • Well, doesn't work for me. My ViewHolder is doing this in the onBindViewHolder method. `image.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { GoTo.imageReelGalery(itemView.getContext(), v, 0, Image.toArray(publication.getImages())); } }); if (image.getDrawable() == null) PicassoHelper.downloadImage(itemView.getContext(),"http://" + publication.getImages()[0], image);` Maybe because Im loading from an url? – 4gus71n Jan 26 '15 at 12:53
  • 1
    To know, I made it work with the following code: `Picasso picasso = Picasso.with(ctx); picasso.load(url).stableKey(url).tag(viewHolder).into(image);` When before was: `Picasso picasso = Picasso.with(ctx); picasso.load(url).into(image);` – 4gus71n Jan 26 '15 at 13:06
  • Your approach to RecyclerView ViewHolder is wrong. By setting the onClickListener in onBindViewHolder, you're commanding something like "every time this view is rendered, tell to the imageview that, when it is clicked, it should load the image". That is the reason why the above solution was not working for you, and the way you said you "made it work" is not correct either, even if it looks like. [Here you are an example of proper RecyclerView ViewHolder pattern](https://github.com/Stoyicker/LoLin1/blob/master/app/src/main/java/org/jorge/cmp/ui/adapter/FeedAdapter.java). – Jorge Antonio Díaz-Benito Jan 26 '15 at 13:12
  • Jorge, thinking about ViewHolder pattern, you want to recycle the view, so the view holder will have ImageView that is already populated - with the other image, i.e. the wrong image. In your code you set 'viewHolder.imageView.setImageDrawable(null);' so Picasso get call anyway. if (true); – user802421 Mar 13 '15 at 08:57
  • It has been already commented by Jake Wharton, to not skip Picasso loading even if `imageURL` is null. If you skip Picasso loading in case of `null` then there are other apparent issues like picture swapping in recycler view – Harneev Sep 14 '16 at 14:20
1

Picasso automatically cache images in two levels:

  • disk cache (this is actually NOT in picasso but in the network layer that picasso use)
  • memory cache

They are bot initialized with defaults that suites most applications.

When the memory cache is getting too big the oldest used bitmap is removed from memory cache (which by default is 1/7 of the available heap space).

I think what's happening here is that you are close to the memory limit of the cache and thus images are decoded again every time you need them again.

See here: Android Picasso Configure LruCache Size

But I advice AGAINST increasing the memory cache size unless you really are re-sizing the images to the actual size you use..

I think the problem with your code is this:

.resize(deviceWidth, deviceWidth)

are you sure you really need an image of that size? What do you store in PrefUtils ? does it get updated when the device rotate? (landscape vs portrait)

If you need smaller images try to pass the resize the actual size of the image you are gonna use. If you don't know (computed at runtime with the layout) do with an approximation or write your code to obtain the actual size (I usually write custom classes extending ImageView for these stuff).

If you really need the images at that size just increase the cache size (see link above) and you should be good to go.

Community
  • 1
  • 1
Daniele Segato
  • 12,314
  • 6
  • 62
  • 88
1

Use Glide. it caches the resized image, not the full-size image, unlike Picasso. In this approach, you will lose image quality. If you want to retain the full-size quality, I suggest you use Glide to load and save the image into file storage, so whenever you need the full-size image, just access that image from file storage.

Yogi
  • 609
  • 1
  • 8
  • 21
j-j
  • 93
  • 1
  • 13
0

The best answer of this question is do not use round image view just use transformation with simple image view because of round image view the application takes a lot of memory just use simple image view and set transformation with it. Use this transformation

Asad Yasin
  • 93
  • 10
  • With Transformation we can set an image in a desired shape. How to make the view as circle. i.e there is a circle imageview where user click to attach an image? @Rana Asad – Arnold Brown May 03 '19 at 07:23
  • 1
    @ArnoldBrown if want to place image after user select image on clicking the imageview then set some placeholder to image view it will work like circle image view. – Asad Yasin May 03 '19 at 11:01
-3

just use noPlaceholder attribute.

Viral Patel
  • 32,418
  • 18
  • 82
  • 110
pichon
  • 1