Main question:
What is the most efficient bug free method of lazy loading remote images in a simple ListView adapter that utilizes the ViewHolder pattern?
I currently have an implementation that will first check a SoftReference Bitmap HashMap for a soft caches version of the image. If that fails I check my hard cache for a copy of the image. If that fails I get it from the web. I do all of this on a separate thread and in a queue to eliminate concurrent or duplicate downloads.
The problem is when loading through a callback. Because I utilize the ViewHolder pattern my views are constantly being recycled and I have not found a solid way to eliminate different images being randomly attached to my ImageViews. I do default to a default image before each load but because the views are being recycled so quickly "old" listeners apply onto my ImageView providing the wrong image which are then replaced with the correct image.
The only semi-solid solution I have found is to use the ViewHolder itself as the listener but this only makes the problem less apparent. It still happens on a fast scroll.
Any help would be appreciated.
Update:
https://github.com/DHuckaby/Prime
I have found a solution to the image switching problem and I will provide a code chunk below. I will not accept it yet though because I do not know if this is the most efficient method which is my original question. If this is implemented correctly it will work perfectly.
public void getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
...
String imagePath = insertImageUrlHere();
Object tag = holder.userThumb.getTag();
if (tag != null && tag.equals(imagePath)) {
// Do nothing
} else {
holder.userThumb.setTag(imagePath);
holder.userThumb.setImageResource(R.drawable.default_image);
AsynchronousImageLoadingUtility.load(imagePath, holder);
}
...
return convertView;
}
private static class ViewHolder implements AsynchronousImageLoadingUtilityCallback {
private ImageView userThumb;
#Override
public void onImageLoad(String source, Bitmap image) {
if (image != null && userThumb.getTag().equals(source)) {
userThumb.setImageBitmap(image);
}
}
}
Related
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
I have a custom ArrayAdapter that gets images from the web. I understand that the views get recycled. My code seems to work but there is a problem with the images that are loaded from the web. Occassionally, the wrong image might show for another row. For example, Mickey Mouse might be the image on Row 0 and when I scroll down Mickey Mouse might appear briefly for Row 9 (example) before changing to Donald Duck. And when I scroll back up to the top, Donald Duck might appear for Row 0 before changing back to Mickey Mouse.
Here is my code:
class OffersCustomAdapter extends ArrayAdapter<Merchant>{
Context context;
ArrayList<User> userName;
private LayoutInflater inflater;
private ImageLoadingListener animateFirstDisplayListener;
private ImageLoader imageLoader;
public OffersCustomAdapter(Context c, ArrayList<User> users) {
super(c, R.layout.single_row, users);
this.context=c;
this.userName=users;
imageLoader = ImageLoader.getInstance();
imageLoader.init(ImageLoaderConfiguration.createDefault(context));
}
static class ViewHolder{
TextView title;
TextView cat;
TextView type;
TextView desc;
ImageView pic;
}
#Override
public int getViewTypeCount() {
return 1;
}
#Override
public int getItemViewType(int position) {
return position;
}
#Override //parent is listview
public View getView(int position, View convertView, ViewGroup parent) {
View row=convertView;
ViewHolder viewHolder;
if(convertView == null){
inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
//row contains our relative layout
row =inflater.inflate(R.layout.single_row, parent, false);
viewHolder = new ViewHolder();
viewHolder.title =
(TextView) row.findViewById(R.id.textView1);
viewHolder.pic = (ImageView) row.findViewById(R.id.imageView1);
row.setTag(viewHolder);
}else{
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder = (ViewHolder) row.getTag();
User u = userName.get(position);
String titleSt = userName.get(position).getName();
viewHolder.title.setText(titleSt);
imageLoader.displayImage(imageUrl+userName.get(position).getImg(), viewHolder.pic, animateFirstDisplayListener);
return row;
}
I've looked at other examples of SO but no luck.
It's because The view is being recycled. When you scroll down, the next view that comes up will use the same view that just scrolled out of view (i.e. Mickey Mouse). You can fix this by displaying a loading image while your imageLoader fetches the new image.
If you don't have a loading image, you can do something like this at the beginning of your getView(...) method:
viewHolder.pic.setImageDrawable(null);
Edit: fixed based on comment.
It's likely because the previous image loading operation isn't canceled if still underway when the view is recycled, so this introduces a race condition on setting the view's bitmap. This issue is discussed briefly in this post, which might be worth a read through: Multithreading for Performance
The author explains it in a bit more detail:
However, a ListView-specific behavior reveals a problem with our
current implementation. Indeed, for memory efficiency reasons,
ListView recycles the views that are displayed when the user scrolls.
If one flings the list, a given ImageView object will be used many
times. Each time it is displayed the ImageView correctly triggers an
image download task, which will eventually change its image. So where
is the problem? As with most parallel applications, the key issue is
in the ordering. In our case, there's no guarantee that the download
tasks will finish in the order in which they were started. The result
is that the image finally displayed in the list may come from a
previous item, which simply happened to have taken longer to download.
This is not an issue if the images you download are bound once and for
all to given ImageViews, but let's fix it for the common case where
they are used in a list.
and provides a workaround example that may be of help.
Try picasso
Once you have the jar in your workspace, you just need one line of code.
Replace imageLoader.displayImage(imageUrl+userName.get(position).getImg(), viewHolder.pic, animateFirstDisplayListener);
with
Picasso.with(context).load(your_image_Url).into(viewholder.pic);
I believe your_image_url in your case is imageUrl+userName.get(position).getImg();
There is a 5 year old Android blog post that describes how to solve this by hand, Multithreading for Performance.
Nowadays, you should use Picasso or Volley for image loading. These network APIs will easily solve your problem and give you additional benefits, such as caching. Volley is the API that Google uses inside their own apps. I use it in my apps and am a fan.
See a thorough introduction to both frameworks.
I am working on a small project where I create a listview bound to an ArrayAdapter. In the getView() function of ArrayAdapter, I do a loading of images from web urls on thread, and set them to list items (based on position of course, so url[0] image is set to list_item[0] etc). It all seems to work well.
However when I was testing the app, I noticed that if I wait my listview to fully display, then perform a fast scroll back and forth, I see sometimes the image on one list item is misplaced on other (like being in an intermediate state). However it's not going away until I scroll the wrongly-displayed-item out of screen and then back.
I do not know if it relates to my loading web url using thread, or maybe loading image from local resource folder can have the same issue.
This actually leads to a question I have about getView() function. I think the logic is correct in my getView() because it's as simple as a binding of url to view based on position. And whenever getView() get a chance to be called, like when I scroll an item out of screen then back, it will make the list item display correctly.
The thing I do not understand is how to explain the issue that happened (like an intermediate state), and how to avoid it when writing code?
I paste my adapter code piece below, but I think the question maybe a general one:
#Override
public View getView(int position, View v, ViewGroup parent) {
ViewHolder viewHolder = null;
if (v == null) {
LayoutInflater inflater = (LayoutInflater) getContext()
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
v = inflater.inflate(layoutResourceId, parent, false);
viewHolder = new ViewHolder();
viewHolder.title = (TextView) v.findViewById(R.id.title);
viewHolder.description = (TextView) v.findViewById(R.id.description);
viewHolder.image = (ImageView) v.findViewById(R.id.image);
v.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) v.getTag();
}
listItem item = items[position]; //items is some global array
//passed in to ArrayAdapter constructor
if (item != null) {
viewHolder.title.setText(item.title);
viewHolder.description.setText(item.description);
if (!(item.imageHref).equalsIgnoreCase("null")) {
mDrawableManager.fetchDrawableOnThread(item.imageHref, viewHolder.image);
} else {
viewHolder.image.setImageDrawable(null);
}
}
return v;
}
}
static class ViewHolder {
TextView title;
TextView description;
ImageView image;
}
I have same issue when scroll quickly it alternate the vales of some item to others, just like background color of some items if changes randomly. I solved this issue by searching a lot and find exact solution is just adding these two methods in your adapter if you are using ViewHolder in your adapter
#Override
public int getViewTypeCount() {
return getCount();
}
#Override
public int getItemViewType(int position) {
return position;
}
Assuming that you are not caching the downloaded image.. lets see the following code:
if (!(item.imageHref).equalsIgnoreCase("null")) {
mDrawableManager.fetchDrawableOnThread(item.imageHref, viewHolder.image);
} else {
viewHolder.image.setImageDrawable(null);
}
Now if the image view is getting reused then it would already have the old image for the assigned list item. So until the thread download the image from the network it would display the old image and when the thread download the image for the current item it would be replaced with the new image. Try to change it to:
if (!(item.imageHref).equalsIgnoreCase("null")) {
viewHolder.image.setImageDrawable(SOME_DEFULAT_IMAGE);
mDrawableManager.fetchDrawableOnThread(item.imageHref, viewHolder.image);
} else {
viewHolder.image.setImageDrawable(null);
}
Or you can use something link smart image view that supports HTTP URI and also caches the images. Check out following link for smart image view:
https://github.com/loopj/android-smart-image-view
http://loopj.com/android-smart-image-view/
Add ImageLoader class from below link in your project.
link
just call DisplayImage() methode of Image loader class as below in getView()
ImageLoader imageLoader = new ImageLoader();
yourImageView.setTag(URL_FOR_Your_Image);
imageLoader.DisplayImage(URL_FOR_Your_Image,ACTIVITY.this, yourImageView);
Your images will load in background as you want without wait.
I think you should declare your downloader method fetchDrawableOnThread() as "synchronized" . Because a lot of threads are working simultaneously and any thread which started later, can end earlier. So there are chances of images being misplaced.
It happened to me for a long time. Finally "synchronized" helped me do it cleanly. I hope it helps you too.
I give it a try with synchronization again. Either synchronize the fetchDrawableOnThread(), or synchronize the global hashmap data within fetchDrawableOnThread(). First i thought the issue is gone, but when i tried more times later, i found the issue is still there.
Then i thought about the synchronization. fetchDrawableOnThread() is called from getView(), and getview() itself does not have a concurrency issue. Even if as Yogesh said, what happened INSIDE getView() is thread-based, and return early or late, it can not affect the correctness of getView(), i.e. the list item's display, only the sooner or later.
What i did(synchronization) inside fetchDrawableOnThread() i think it's still correct, 'cause i used a hashmap to cache images downloaded from remote url, and this hashmap is read/write upon in a multi-thread situation, so it should be locked. But i do not think it's the rootcause of the UI misplace, if hashmap is messed up, the image misplacement will be permanent.
Then i looked further on convertView reuse mechanism based on Praful's explanation. He explained clearly what happened when image always comes from remote and no cache locally, but my situation is i waited my list to display fully, i.e. all images download complete and cached complete, before i do the fast scroll. So in my experiment, the images are read from cache.
Then when inspecting my code, i saw one minor difference in the use of convertView as in getView() method, a lot of the example usages are like this:
public View getView(int position, View convertView, ViewGroup parent) { // case 1
View v = convertView;
.... // modify v
return v;
}
However the example i happened to copy from use:
public View getView(int position, View convertView, ViewGroup parent) { // case 2
.... // modify convertView
return convertView;
}
I thought it makes no difference at first, 'cause according to what android says, 'ListView sends the Adapter an old view that it's not used any more in the convertView param.', so why not use 'convertView' para directly?
But i guess i was wrong. I changed my getView() code to case 1. Boom! everything works. No funny business ever no matter how fast i scroll the list.
Quite strange, is convertView only old, or is it old & in-use? If the later, we should only get a copy and then modify..... ??
I have an application that contains a couple of listviews. The listviews contains items that consist of imageviews and textviews.
All images are thumbnail sized on a server and the pattern used for loading them is like this:
Instantiate a DrawableManager
in the getView() method I do the following:
Pass the thumb uri and the ImageView instance to the DrawableManagers getImageAsync method
The method will first look on sd card if the image exists if so load it from SD card and save a softreference + update imageview drawable
If not exists on sd. Fetch from HTTP and save on SD (if there is enough space) put as softreference and update imageview drawable.
When the images exists on sd card everything works fine. But first time (or when using the app without sd card) the images seems to be populated into the wrong listviews rows when scrolling. When i stop scroll the problem fixes it self after a couple of seconds.
Its almost like if the ImageView references are pooled or something.
Any ideas?
I also include the getView method:
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder vh;
if (convertView == null) {
convertView = inflater.inflate(R.layout.informationrow, null);
vh = new ViewHolder();
vh.imageView = (ImageView) convertView.findViewById(R.id.rowInformationIcon);
vh.textView = (TextView) convertView.findViewById(R.id.rowInformationTitleLine);
convertView.setTag(vh);
}
else {
vh = (ViewHolder) convertView.getTag();
}
CustomCategory cc = items.get(position);
if (cc != null) {
vh.textView.setText(cc.get_name());
if (cc.getMediaUrl() != null) {
_drawMgr.fetchDrawableOnThread(cc.getMediaUrl(), vh.imageView);
vh.imageView.setBackgroundDrawable(getResources().getDrawable(R.drawable.imageframe));
}
else {
vh.imageView.setImageDrawable(getResources().getDrawable(R.drawable.trans4040));
vh.imageView.setBackgroundDrawable(null);
}
}
return convertView;
}
This is the View recycling used by ListView...
The convertView parameter passed to your getView() can refer to an existing item that has scrolled off the displayed part of the list, and can be reused to show an item that is now appearing.
So, yes, the same ImageView will be reused in multiple downloads in the code you posted. In your getView() you should check to see if a download is already pending and cancel it if it's no longer needed (or let it complete to a FIFO image cache somewhere, but not touch the ImageView which is now needed for a more recently-started download).
(An alternative, lazy developers implementation that assumes infinite memory would be to ignore the convertView parameter and instantiate fresh informationrow views on every call to it. Don't do that. :) ).
Use this Library for loading images in ListView.
https://github.com/nostra13/Android-Universal-Image-Loader
How to clear the cached image from memory programatically in Android?
I have a ListView with icons when I scroll its reloads the image. So its produce the OutofMemoryError. I want clear the cache while gets this exception. how to do that? any help?
EDIT:
i am just using this code on my program to loadimage:
http://ballardhack.wordpress.com/2010/04/10/loading-images-over-http-on-a-separate-thread-on-android/
Are you re-using the bitmap objects in the ListView?
Romain Guy talked about how important this is for memory and smooth performance in his Android talk on layouts and views at Google I/O last year.
Essentially, you should have a certain number of bitmap objects (he used 8) and every time you load the next image as you scroll, it should go into the object of the one that just disappeared.
You might think caching the images is faster, but it causes memory problems and garbage collecting issues which inevitably causes lag.
Yup... known "problem", let say this is the behaviour of the ListView.
How to fix it:
Watch the first 15min of the video as suggested by #HXCaine, which explains the ViewHolder.
If I'm not mistaken your example should set the default image when a bitmap is null! In the example you do not provide this to a view and so it gets cached. Shur this should be handled by the framework but it is not :(.
Example code:
public class DebtAdapter extends BaseAdapter {
...
#Override
public View getView(int position, View convertView, ViewGroup parent)
{
ViewHolder holder;
Bitmap bitmap;
if(convertView == null)
{
convertView = inflater.inflate(viewResourceId, null);
holder = new ViewHolder();
holder.photo = (ImageView) convertView.findViewById(R.id.photo);
convertView.setTag(holder);
}
else
{
holder = (ViewHolder) convertView.getTag();
}
bitmap = item.getContact().getPhoto();
if(bitmap != null)
{
holder.photo.setImageBitmap(bitmap);
}
else
{
holder.photo.setImageBitmap(null);
}
return convertView;
}
}
I hope it helps.
You can free up some memory by calling the recycle method if you're using Bitmap. However, I'm not really sure if this will solve your problem.