Android - How to stop images reloading in Recyclerview when scrolling - android

I was creating an app for loading images from internet in cards. I am using Recyclerview for listing purpose. For downloading images, I have tried Picasso, universal-image-downloader and other custom methods but everywhere I have this problem:
Whenever I scroll down and up the images in imageview are getting reloaded. I have even kept a condition to check if the imageview contains an image but that also doesn't seem to work or my code may be faulty. But I am not able to find a solution to this problem. Below I have attached the 'onBindViewHolder' function of my code which I think is the problematic part. Here images is a string arraylist containing image urls.
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.default_card, parent, false);
ViewHolder viewHolder;
viewHolder = new ViewHolder(view);
return viewHolder;
}
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
String url = images.get(position);
if (holder.imgView.getDrawable() != null) {
//Do Nothing
} else {
ImageLoader.getInstance().displayImage(url, holder.imgView);
}
}

You can use .fit() with Picasso. Hope that solves your problem
Picasso.with(persons.get(i).context)
.load(persons.get(i).photoId)
.placeholder(R.drawable.placeholder)
.fit()
.into(personPhoto);

At first, there is a bug in your code - you have to remove the condition from onBindViewHolder. All ImageView instances has to be updated each time they are about to display.
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
String url = images.get(position);
ImageLoader.getInstance().displayImage(url, holder.imgView);
}
The reloading part:
The problem is, that you are probably not using any cache (in-memory, or disk based). I would not elabarote on this a lot, caching of images in collection views is a really common problem and I'm sure a quick StackOverflow search will give you a lot of results.
Just in particular - Universal image does not have caching enabled by default You can turn it on this way.
DisplayImageOptions options = new DisplayImageOptions.Builder()
...
.cacheInMemory(true)
.cacheOnDisk(true)
...
.build();
ImageLoader.getInstance().displayImage(imageUrl, imageView, options); // Incoming options will be used
Or you can set it globally for your application, just check the docs.

You can use this to solve your issue.
mRecyclerView.getRecycledViewPool().setMaxRecycledViews(0, 0);

If you don't want to use Cache, you can use setItemViewCacheSize(size) and mention the size where you don't want to reload. That's it. Recyclerview version 24.0.0

Related

RecyclerView of RecyclerViews: Image Loading

I try to implement a layout that contains a vertical RecyclerView that itself contains multiple horizontal RecyclerViews (imagine a news site, with multiple news sections, where each section contains multiple news stories that can be scrolled horizontally).
The horizontal RecyclerView contains (among other things) an ImageView. I'm looking for the optimal strategy of loading an image from an URL into this ImageView.
Firstly, my code. The Adapter for the outer RecyclerView:
#Override
public ItemRowHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.content_preview_list_item, null);
ItemRowHolder mh = new ItemRowHolder(v);
mh.recycler_view_list.setAdapter(new SectionListDataAdapter(mContext, listener, new ArrayList<ContentPreviewButtonItem>()));
mh.recycler_view_list.setLayoutManager(new LinearLayoutManager(mContext, LinearLayoutManager.HORIZONTAL, false));
return mh;
}
#Override
public void onBindViewHolder(ItemRowHolder itemRowHolder, int i) {
Log.d("RecyclerViewDataAdapter", "onBindViewHolder");
final String sectionName = dataList.get(i).getTitle();
ArrayList singleSectionItems = dataList.get(i).getItems();
itemRowHolder.itemTitle.setText(sectionName);
((SectionListDataAdapter)itemRowHolder.recycler_view_list.getAdapter()).setItemList(singleSectionItems);
itemRowHolder.recycler_view_list.getAdapter().notifyDataSetChanged();
itemRowHolder.recycler_view_list.setHasFixedSize(true);
}
And for the inner RecyclerView that contains the images:
#Override
public SingleItemRowHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
Log.d("SectionListDataAdapter", "onCreateViewHolder");
View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.content_preview_item, null);
SingleItemRowHolder mh = new SingleItemRowHolder(v);
return mh;
}
#Override
public void onBindViewHolder(SingleItemRowHolder holder, int i) {
Log.d("SectionListDataAdapter", "onBindViewHolder");
ContentPreviewButtonItem singleItem = itemsList.get(i);
Uri imageUri = Uri.parse(singleItem.getImageUrl());
holder.itemImage.setImageURI(imageUri); //itemImage is a SimpleDraweeView
}
While this code works great on mobile, there's a noticeable lag on tablet when multiple inner RecyclerViews and images are loaded at once. Right now I use Facebook's Fresco library to load the image into a SimpleDraweeView. I also tried Picasso but that yield an OutOfMemoryException when multiple images had to be loaded at once.
I improved the performance by moving the adapter creation of the inner RecyclerViews into the onCreateViewHolder instead of the onBindViewHolder, but the lag is still noticeable.
What's the best strategy or library to load images in this scenario.
Thanks
Glide is a perfect fit for this I believe using which you can resize the image also while the image is loading you can display a thumbnail of the same
I am doing the same in my project and so far the best service I got is from Picasso library. Their resizing capabilities are the best. Might be a little slow on first load but subsequent loading is super fast.

ListView BaseAdapter is hanging

I am getting some odd behavior in my scroll-able listview. It is controlled by a BaseAdapter.
As I scroll up or down it will sometimes hang and then bounce back in the opposite direction. I have no idea why and do not know how to trouble shoot it.
My base adapter is below and it loads about 90 fragments with an image and a bunch of text.
public class PosterListAdapter extends BaseAdapter {
private final ArrayList<Poster> listItems;
private final LayoutInflater inflater;
public PosterListAdapter(ArrayList<Poster> listItems, LayoutInflater inflater) {
this.listItems = listItems;
this.categoryList = categoryItems;
this.inflater = inflater;
}
#Override
public int getCount() {
//Log.d("getCount", String.valueOf(this.listItems.size()));
return this.listItems.size();
}
#Override
public Poster getItem(int i) {
return this.listItems.get(i);
}
#Override
public long getItemId(int i) {
return 0;
}
#Override
public View getView(int i, View convertView, ViewGroup viewGroup) {
MyViewHolder mViewHolder;
SparseArray<String> categoryMap = db.getCategoryMap();
if (convertView == null) {
convertView = inflater.inflate(R.layout.catalog_list_fragment, viewGroup, false);
mViewHolder = new MyViewHolder(convertView);
convertView.setTag(mViewHolder);
} else {
mViewHolder = (MyViewHolder) convertView.getTag();
}
//LayoutParams params = (LayoutParams) imageView.getLayoutParams();
Poster item = this.listItems.get(i);
String filename = item.getPosterFilename();
mViewHolder.posterAuthor.setText("Author: "+item.getPresenterFname()+' '+item.getPresenterLname());
mViewHolder.posterAltAuthors.setText("Supporting Authors: "+item.getPosterAuthors());
mViewHolder.posterTitle.setText(item.getPosterTitle());
mViewHolder.posterSynopsis.setText(item.getPosterSynopsis());
mViewHolder.posterNumber.setText("Poster: "+String.valueOf(item.getPosterNumber()));
mViewHolder.posterPresentation.setText("Live Presentation: "+item.getSessionDate()+" at "+item.getSessionTime()+"\nAt Station: "+item.getPosterStation());
String category = categoryMap.get(item.getCatID());
mViewHolder.posterCategory.setText("Category: "+category);
File imgFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath(), "/"+filename+"/"+filename+".png");
//Here I thought maybe the resizing of the image or even the image itself
// was causing the hang up so I tried the list without it and it is still
// hanging.
//mViewHolder.imageView.setImageURI(Uri.fromFile(new File(imgFile.toString())));
if (imgFile.exists()){
/*Bitmap bmp = BitmapFactory.decodeFile(imgFile.toString());
int newWidth = 500;
Bitmap sizedBMP = getResizedBitmap(bmp, newWidth);
mViewHolder.imageView.setImageBitmap(sizedBMP);*/
}
else{
//set no image available
}
return convertView;
}
private class MyViewHolder {
TextView posterTitle, posterAuthor, posterSynopsis, posterCategory, posterNumber, posterAltAuthors,posterPresentation;
ImageView imageView;
public MyViewHolder(View item) {
posterTitle = (TextView) item.findViewById(R.id.poster_title);
posterAuthor = (TextView) item.findViewById(R.id.poster_author);
posterAltAuthors = (TextView) item.findViewById(R.id.poster_altAuthors);
posterSynopsis = (TextView) item.findViewById(R.id.poster_synopsis);
posterCategory = (TextView) item.findViewById(R.id.poster_category);
posterNumber = (TextView) item.findViewById(R.id.poster_number);
posterPresentation = (TextView) item.findViewById(R.id.poster_presentation);
imageView = (ImageView) item.findViewById(R.id.poster_thumb);
}
}
}
I know that is a big chunk of code and it may be ugly.. If you could point me in the right direction as to how to trouble shoot it that would be helpful as well. I am using eclipse.
I bet if you comment out that line File imgFile = new File(... (and the code that depends on imgFile) that your list scrolling will improve.
That's still an I/O operation and since getView() runs in the UI thread, it may cause hiccups.
What you should do is: once you have the ImageView in getView(), you start an AsyncTask to open the file, decode it, and assign the bitmap to the ImageView.
Then you have to handle things like: the ImageView gets recycled, but the task isn't completed.
Read this article: Multithreading for Performance | Android Developers Blog. It's dealing with images from a remote server, but all the principles are still the same.
Also: does this line SparseArray<String> categoryMap = db.getCategoryMap(); do a database lookup? That could cause a hiccup as well.
TL;DR getView() needs to be fast; put slow operations in a non-UI thread.
Database related operations should done in separate thread. as it might take time to get data if number of records are greater.
and getView() method will be called more then one time. we can say it will be called in loop till size of your array list (getCount()).
Second thing: File operation is also should be done in separate thread.because IO task is also some times time consuming.
Solution:
SparseArray<String> categoryMap = db.getCategoryMap();
put this line out of getView() method as i cant see any list dependent parameter in this line. this should be done in either constructor or separate thread.
Run your database operations and File IO in separate thread like using AsyncTask
I agree with previous answer, looks like creating new File is the only heavy operation which hangs your app.
Try to use some image loading library, for example Glide. It will simplify your code, also you won't have to deal with background job. Library will do it all for you.
1.Import library, e.g. if you use gradle insert this into your build.gradle file:
repositories {
mavenCentral() // jcenter() works as well because it pulls from Maven Central
}
dependencies {
compile 'com.github.bumptech.glide:glide:3.7.0'
compile 'com.android.support:support-v4:19.1.0'
}
2.In your adapter replace code related to loading picture with this:
Glide
.with(context)
.load(<full path to your local file>)
.into(mViewHolder.imageView);
3.And it is all. Glide will handle recycling and other things internally, also it is optimized for fast loading and smooth scrolling.
Image related processes really consumes so much memory on the UIThread of the app, you should be setting your image using asynchronous(background) approach. Or if its much of a hassle for you, there are tons of libraries available that will do this. I specifically recommend using Picasso for it is very dynamic, easy to use and is regularly updated. Loading an image would require one line of code:
Picasso.with(getApplicationContext()).load(<your image path or url or resource id>).into(YourImageView);
Picasso also have a variety of image options you can choose from.

RecyclerView adapter showing wrong images

I have a RecyclerView adapter that looks like this:
public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerAdapter.ViewHolder> {
private static Context context;
private List<Message> mDataset;
public RecyclerAdapter(Context context, List<Message> myDataset) {
this.context = context;
this.mDataset = myDataset;
}
public static class ViewHolder extends RecyclerView.ViewHolder implements View.OnCreateContextMenuListener, View.OnClickListener {
public TextView title;
public LinearLayout placeholder;
public ViewHolder(View view) {
super(view);
view.setOnCreateContextMenuListener(this);
title = (TextView) view.findViewById(R.id.title);
placeholder = (LinearLayout) view.findViewById(R.id.placeholder);
}
}
#Override
public RecyclerAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.message_layout, parent, false);
ViewHolder vh = new ViewHolder((LinearLayout) view);
return vh;
}
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
Message item = mDataset.get(position);
holder.title.setText(item.getTitle());
int numImages = item.getImages().size();
if (numImages > 0) {
View test = LayoutInflater.from(holder.placeholder.getContext()).inflate(R.layout.images, holder.placeholder, false);
ImageView image = (ImageView) test.findViewById(R.id.image);
Glide.with(context)
.load("http://www.website.com/test.png")
.fitCenter()
.into(image);
holder.placeholder.addView(test);
}
}
#Override
public int getItemCount() {
return mDataset.size();
}
}
However, some of the items in the RecyclerView are showing images when they shouldn't be. How can I stop this from happening?
I do the check if (numImages > 0) { in onBindViewHolder(), but that's still not stopping it from showing images for items that shouldn't have images.
You should set imageView.setImageDrawable (null)
In onBindViewHolder() before setting the image using glide.
Setting image drawable to null fix the issue.
Hope it helps!
The problem is in onBindViewHolder, here:
if (numImages > 0) {
View test = LayoutInflater.from(holder.placeholder.getContext()).inflate(R.layout.images, holder.placeholder, false);
ImageView image = (ImageView) test.findViewById(R.id.image);
Glide.with(context)
.load("http://www.website.com/test.png")
.fitCenter()
.into(image);
holder.placeholder.addView(test);
}
If numImages is equal to 0, you're simply allowing the previously started load into the view you're reusing to continue. When it finishes, it will still load the old image into your view. To prevent this, tell Glide to cancel the previous load by calling clear:
if (numImages > 0) {
View test = LayoutInflater.from(holder.placeholder.getContext()).inflate(R.layout.images, holder.placeholder, false);
ImageView image = (ImageView) test.findViewById(R.id.image);
Glide.with(context)
.load("http://www.website.com/test.png")
.fitCenter()
.into(image);
holder.placeholder.addView(test);
} else {
Glide.clear(image);
}
When you call into(), Glide handles canceling the old load for you. If you're not going to call into(), you must call clear() yourself.
Every call to onBindViewHolder must include either a load() call or a clear() call.
I also had issues with RecyclerView showing wrong images. This happens because RecyclerView is not inflating view for every new list item: instead list items are being recycled.
By recycling views we can ruffly understand cloning views. A cloned view might have an image set from the previous interaction.
This is especially fair if your are using Picasso, Glide, or some other lib for async loading. These libs hold reference to an ImageView, and set an image on that refference when image is loaded.
By the time the image gets loaded, the item view might have gotten cloned, and the image is going to be set to the wrong clone.
To make a long story short, I solved this problem by restricting RecyclerView from cloning my item views:
setIsRecyclable(false)in ViewHolder constructor.
Now RecyclerView is working a bit slower, but at least the images are set right.
Or else cansel loading image in onViewRecycled(ViewHolder holde)
The issue here is that, as you are working with views that are going to be recycled, you'll need to handle all the possible scenarios at the time your binding your view.
For example, if you're adding the ImageView to the LinearLayout on position 0 of the data source, then, if position 4 doesn't met the condition, its view will most likely have the ImageView added when binding position 0.
You can add the content of R.layout.images content inside your
R.layout.message_layout layout's R.id.placeholder and showing/hiding the placeholder depending on the case.
So, your onBindViewHolder method would be something like:
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
Message item = mDataset.get(position);
holder.title.setText(item.getTitle());
int numImages = item.getImages().size();
if (numImages > 0) {
holder.placeholder.setVisivility(View.VISIBLE);
ImageView image = (ImageView)holder.placeholder.findViewById(R.id.image);
Glide.with(context)
.load("http://www.website.com/test.png")
.fitCenter()
.into(image);
}else{
holder.placeholder.setVisibility(View.INVISIBLE);
}
}
Sometimes when using RecyclerView, a View may be re-used and retain the size from a previous position that will be changed for the current position. To handle those cases, you can create a new [ViewTarget and pass in true for waitForLayout]:
#Override
public void onBindViewHolder(VH holder, int position) {
Glide.with(fragment)
.load(urls.get(position))
.into(new DrawableImageViewTarget(holder.imageView,/*waitForLayout=*/ true));
https://bumptech.github.io/glide/doc/targets.html
I also had the same problem and ended with below solution and it working fine for me..
Have your hands on this solution might be work for you too (Put below code in your adapter class)-
If you are using Kotlin -
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getItemViewType(position: Int): Int {
return position
}
If you are using JAVA -
#Override
public long getItemId(int position) {
return position;
}
#Override
public int getItemViewType(int position) {
return position;
}
This works for me in onBindViewHolder!
if(!m.getPicture().isEmpty())
{
holder.setIsRecyclable(false);
Picasso.with(holder.profile_pic.getContext()).load(m.getPicture()).placeholder(R.mipmap.ic_launcher_round).into(holder.profile_pic);
Animation fadeOut = new AlphaAnimation(0, 1);
fadeOut.setInterpolator(new AccelerateInterpolator());
fadeOut.setDuration(1000);
holder.profile_pic.startAnimation(fadeOut);
}
else
{
holder.setIsRecyclable(true);
}
I was having same issue I solved by writing holder.setIsRecyclable(false).Worked for me.
#Override
public void onBindViewHolder(#NonNull RecylerViewHolder holder, int position) {
NewsFeed currentFeed = newsFeeds.get(position);
holder.textView.setText(currentFeed.getNewsTitle());
holder.sectionView.setText(currentFeed.getNewsSection());
if(currentFeed.getImageId() == "NOIMG") {
holder.setIsRecyclable(false);
Log.v("ImageLoad","Image not loaded");
} else {
Picasso.get().load(currentFeed.getImageId()).into(holder.imageView);
Log.v("ImageLoad","Image id "+ currentFeed.getImageId());
}
holder.dateView.setText(getModifiedDate(currentFeed.getDate()));
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getItemViewType(position: Int): Int {
return position
}
This Works for Me
I Had the same issue and i fixed it like this:
GOAL : onViewAttachedToWindow
#Override
public void onViewAttachedToWindow(Holder holder) {
super.onViewAttachedToWindow(holder);
StructAllItems sfi = mArrayList.get(position);
if (!sfi.getPicHayatParking().isEmpty()) {
holder.viewFliperMelk.addSlider(new TextSliderView(mContext.getApplicationContext()).image(T.GET_MELK_IMAGE + '/' + sfi.getPicHayatParking() + ".jpg").setScaleType(BaseSliderView.ScaleType.CenterCrop));
}
if (!sfi.getPicSleepRoom().isEmpty()) {
holder.viewFliperMelk.addSlider(new TextSliderView(mContext.getApplicationContext()).image(T.GET_MELK_IMAGE + '/' + sfi.getPicSleepRoom() + ".jpg").setScaleType(BaseSliderView.ScaleType.CenterCrop));
}
if (!sfi.getPicSalonPazirayi().isEmpty()) {
holder.viewFliperMelk.addSlider(new TextSliderView(mContext.getApplicationContext()).image(T.GET_MELK_IMAGE + '/' + sfi.getPicSalonPazirayi() + ".jpg").setScaleType(BaseSliderView.ScaleType.CenterCrop));
}
if (!sfi.getPicNamayeStruct().isEmpty()) {
holder.viewFliperMelk.addSlider(new TextSliderView(mContext.getApplicationContext()).image(T.GET_MELK_IMAGE + '/' + sfi.getPicNamayeStruct() + ".jpg").setScaleType(BaseSliderView.ScaleType.CenterCrop));
}
}
I had a similar issue when getting pictures from the photo gallery and putting them in a recyclerview with GridLayoutManager(never had the issue with Glide). So in the adapter onBindViewHolder use a HashMap or SparseIntArray to put the current hashcode(this is the common thing that the recycled views have in common) and adapter position inside it. Then call your background task and then once it's done and before you set the image, check to see if the hashcode key - which will always have the current adapter position as the value - still has the same value (adapter position) as when you first called the background task.
(Global variable)
private SparseIntArray hashMap = new SparseIntArray();
onBindViewHolder(ViewHolder holder, int position){
holder.imageView.setImageResource(R.drawable.grey_square);
hashMap.put(holder.hashCode(), position);
yourBackgroundTask(ViewHolder holder, int position);
}
yourBackGroundTask(ViewHolder holder, int holderPosition){
do some stuff in the background.....
*if you want to stop to image from downloading / or in my case
fetching the image from MediaStore then do -
if(hashMap.get(holder.hashCode())!=(holderPos)){
return null;
}
- in the background task, before the call to get the
image
onPostExecute{
if(hashMap.get(holder.hashCode())==(holderPosition)){
holder.imageView.setImageBitmap(result);
}
}
}
So i am just providing an extension to this answer since there is not much space to leave it as comment.
After trying out like mentioned in one of above solutions i found out that, the real issue can still be addressed even if you are using a static resource(is not being downloaded and is available locally)
So basically on onBindViewHolder event i just converted the resource to drawable and added it like below :
imageView.setImageDrawable(ContextCompat.getDrawable(context,R.drawable.album_art_unknown));
this way you wont have an empty space on the view while glide/async downloader is loading the actual image from network.
plus looking at that being reloaded every time i also added below code while calling the recycler adapter class;
recyclerView.setItemViewCacheSize(10);
recyclerView.setDrawingCacheEnabled(true);
so by using above way you wont need to set setIsRecyclable(false) which is degrading if you have larger datasets.
By doing this i you will have a flicker free loading of recyclerview of course except for the initial loads.
I would like to say that if you send the ImageView and any load-async command (for instance loading from S3), the recycler view does get confused.
I did set the bitmap null in the onViewRecycled and tested with attach and detach views etc. the issue never went away.
The issue is that if a holderView gets used for image-1, image-10 and stops at the scroll with image-19, what the user sees is image-1, then image-10 and then image-19.
One method that worked for me is to keep a hash_map that helps know what is the latest image that needs to be displayed on that ImageView.
Remember, the holder is recycled, so the hash for that view is persistent.
1- Create this map for storing what image should be displayed,
public static HashMap<Integer, String> VIEW_SYNCHER = new HashMap<Integer, String>();
2- In your Adapter, onBindViewHolder,
String thumbnailCacheKey = "img-url";
GLOBALS.VIEW_SYNCHER.put(holder.thumbnailImage.hashCode(), thumbnailCacheKey);
3- Then you have some async call to make the network call and load the image in the view right ?
In that code after loading the image from S3, you test to make sure what goes into the View,
// The ImageView in the network data loader, get its hash.
int viewCode = iim.imView[0].hashCode();
if (GLOBALS.VIEW_SYNCHER.containsKey(viewCode))
if (GLOBALS.VIEW_SYNCHER.get(viewCode).equals(bitmapKey))
iim.imView[0].setImageBitmap(GLOBALS.BITMAP_CACHE.get(bitmapKey).bitmapData);
So essentially, you make sure what is the last image key that should go into a view, then when you download the image you check to make sure that's the last image URL that goes in that view.
This solution worked for me.

ListView shows wrong and duplicates images

I have a ListView and 12 ImageViews in it.
Every ImageView has different image which is loading from url. Images are shuffled and sometimes duplicated either I scroll or not.
I tried 10 other ways to solve this problem but have not succeeded.
This is the code I download and show images:
private static class ViewHolder {
ImageView imageViewPhoto;
Bitmap photo;
boolean isDownloading;
}
#Override
public View getView(final int position, View convertView, ViewGroup parent) {
final ViewHolder viewHolder;
if (convertView == null) {
// ...classical view holder and other operations...
if (!viewHolder.isDownloading) {
viewHolder.isDownloading = true;
IImageDownload downloadInterface = new IImageDownload() {
#Override
public void onError(VolleyError error, String url) {
}
#Override
public void onDownloaded(Bitmap response, String url) {
viewHolder.photo = response;
notifyDataSetChanged();
}
};
imageDownloader.downloadImage(dataList.get(position).getPhotoPath(), true, downloadInterface);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
if (viewHolder.photo != null) {
viewHolder.imageViewPhoto.setImageBitmap(viewHolder.photo);
} else {
viewHolder.imageViewPhoto.setImageResource(R.drawable.gray_background);
}
}
Thanks in advance for any ideas!
Before:
imageDownloader.downloadImage(dataList.get(position).getPhotoPath(), true, downloadInterface);
Put:
viewHolder.photo.setImageBitmap(null);
This will reset the ImageView's bitmap, as it is being recycled and therefore keeping its image.
You should have something like this:
if (!viewHolder.isDownloading) {
// download the image in a worker thread
} else {
// cancel the current downloading and start a new one with the new url
}
Since ListView items are reusable. Your items are starting the image downloads, but when you start scrolling, those same items could still be downloading the images when they are already being reused. So when the worker thread has finished, the bitmaps are set in the wrong place and even worse, you never started the downloads for those reused items because the viewholder.isDownloading said it was already downloading an image.
A) You only initiate the download when the convertView is instantiated. You are recycling the rows so you may have a data set larger than the number of row Views that you actually use. This is not the right place to begin downloading an image. You want to do this per viewed position, not per View instantiated.
B) When you fire off a background task to download the image it may return later (after fetching) and replace a row with the wrong image as the row may now represent the wrong position (given row recycling).
Asynchronous image loading in a recycling ListView is slightly more complicated than it first seems. As the user scrolls through the list, you'll need to fire off downloads when a position is viewed, and cancel calls that are now redundant (as they are for a previously visible position).
You may wish to read more on view recycling in a ListView to get a better understanding of what is happening.
Also consider using an image downloading/caching library that handles these complexities such as Picasso.
Use UniversalImageLoader library to load images..
Try this
ImageLoader.getInstance().displayImage(url, holder.imgView, options);
to load images inside adapter..
Use DisplayImageOptions as follows inside constructor of adapter
options = new DisplayImageOptions.Builder()
.showImageOnLoading(android.R.color.transparent)
.showImageForEmptyUri(android.R.color.transparent)
.showImageOnFail(android.R.color.transparent)
.cacheInMemory(true)
.cacheOnDisk(true)
.considerExifParams(true)
.bitmapConfig(Bitmap.Config.RGB_565)
.build();
and add
StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
StrictMode.setThreadPolicy(policy);
inside onCreateView/onCreate of fragment/activity contaning the list

View Pager with Universal Image Loader Out of Memory Error

I am not really sure if a ViewPager with Universal Image Loader can/should be used as an alternate for a gallery like interface since I have run into an Out of Memory error while loading images from SD Card and viewing them in full screen mode. No matter what the number, it works all fine with a GridView but while viewing the images in the View Pager, each bitmap keeps eating up a lot of memory and after 10 or so images, it gives the out of memory error.
I have seen almost all the questions that have been posted here related to the Out of Memory Error while working with the Universal Image Loader and in each one of them, there has been a configurations error as the cause.
I dont know if I am using the wrong configurations or what but I have wasted a lot of time on it and am kind of stuck, any help/advice would be appreciated.
The configurations for the ImageLoader:
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(getApplicationContext())
.memoryCache(new WeakMemoryCache())
.denyCacheImageMultipleSizesInMemory()
.discCacheFileNameGenerator(new Md5FileNameGenerator())
.imageDownloader(new ExtendedImageDownloader(getApplicationContext()))
.tasksProcessingOrder(QueueProcessingType.LIFO)
// .enableLogging() // Not necessary in common
.build();
The Display Image Options are:
options = new DisplayImageOptions.Builder()
.showImageForEmptyUri(R.drawable.image_for_empty_url)
.resetViewBeforeLoading()
.imageScaleType(ImageScaleType.IN_SAMPLE_INT)
.bitmapConfig(Bitmap.Config.RGB_565)
.displayer(new FadeInBitmapDisplayer(300))
.build();
I am using the example project that was given with the library but those settings wont work either, it just crashes after some time. My guess is that there is a specific callback where I have to recycle bitmaps from the views from that are not visible.
EDIT: I know its a memory leak, the views that are not visible are destroyed when they should be but the memory is not released as it should. Heres the implementation of the destroyItem callback, followed the tips given in different questions but still cant find the memory leak.
#Override
public void destroyItem(View container, int position, Object object) {
// ((ViewPager) container).removeView((View) object);
Log.d("DESTROY", "destroying view at position " + position);
View view = (View)object;
((ViewPager) container).removeView(view);
view = null;
}
It's probably not the best implementation to solve it, but it worked for me. Removing the ImageViews is not enough, so I decided to recycle bitmaps in 'destroyItem':
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
View view = (View) object;
ImageView imageView = (ImageView) view.findViewById(R.id.image);
if (imageView != null) {
Bitmap bitmap = ((BitmapDrawable) imageView.getDrawable()).getBitmap();
bitmap.recycle();
bitmap = null;
}
((ViewPager) container).removeView(view);
view = null;
}
This does not clean the last 3 active pages when you leave the activity, although I hope that GC takes care of them.
Try to apply next suggestions:
Use ImageScaleType.EXACTLY
Enable caching on disc (in display options).
Finally try to use .discCacheExtraOptions(maxImageWidthForDiscCache, maxImageHeightForDiscCache, CompressFormat.PNG, 0);
Just posting this because this question is coming up on Google when searching for UIL and OOP. I had OOP problems no matter what configuration, what solved all my problems were the two classes RecyclingImageView and RecyclingBitmapDrawable from this sample project.
I also used the same library and had same error. As solution, i created a sparseArray to keep photoView instances. And use it like this:
private SparseArray<PhotoView> photoViewHolder;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
photoViewHolder = new SparseArray<PhotoView>();
...
}
private class GalleryPagerAdapter extends PagerAdapter {
#Override
public View instantiateItem(ViewGroup container, int position) {
PhotoView photoView = new PhotoView(container.getContext());
ImageHolder holder = new ImageHolder();
holder.position = position;
holder.loaded = false;
photoView.setTag(holder);
photoViewHolder.put(position, photoView);
// I used LazyList loading
loader.DisplayImage(items.get(position), photoView);
// Now just add PhotoView to ViewPager and return it
container.addView(photoView, LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
return photoView;
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
photoViewHolder.remove(position);
}
#Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
}
And to handle viewPager's listener:
pager.setOnPageChangeListener(new OnPageChangeListener() {
#Override
public void onPageScrollStateChanged(int position) {
}
#Override
public void onPageScrolled(int position, float arg1, int arg2) {
}
#Override
public void onPageSelected(int position) {
if(photoViewHolder.get(position) != null) {
ImageHolder holder = (ImageHolder)photoViewHolder.get(position).getTag();
// Do something...
}
}
});
Hope this helps...
I used kutothe's implementation from github issues page.
I had this problem when simply setting Uri to ImageView using: iv.setImageURI(Uri.fromFile(imgFile));
I had the same problem with Universal Image Loader, and I even looked for other Image Loaders out there, and found another good one called "Picasso", but it also had the same problem.
So what worked for me is using GestureImageView and setting gesture-image:recycle to true through XML, and load the images with the following code:
Drawable yourDrawable = null;
try {
InputStream inputStream = getActivity().getContentResolver().openInputStream(Uri.fromFile(img));
yourDrawable = Drawable.createFromStream(inputStream, Uri.fromFile(img).toString() );
inputStream.close();
} catch (FileNotFoundException e) {
yourDrawable = getResources().getDrawable(R.drawable.ic_launcher);
} catch (IOException e) {
e.printStackTrace();
}
if (yourDrawable != null)
iv.setImageDrawable(yourDrawable);
the reason it was crashing and giving OOM error is that the bitmaps aren't recycled when the image aren't displayed on the screen anymore, hence a memory leak occurs.
If there is another way to recycle the bitmap in the normal ImageView, that would be a better solution.
Hope I helped.
I know it's late, but maybe my answer will save someone's time. After hours and hours of trying to solve this issue (with almost every answer found on stack overflow) I finally solved it with Fresco image library. It'a a lib written by Facebook and it's primary goal is to use memory in efficient way. It's really great and my Out Of Memory Error disappeared. I highly recommend using it.
http://frescolib.org/

Categories

Resources