I am working on social application and it's about to complete but I got stuck on one issue that is image flickering. When there is around 9 to 10 images on screen and if I scroll the page then the image flicker take place.
#Override
public View getView(final int position, View convertView, ViewGroup parent) {
final ViewHolder holder;
if (convertView == null) {
LayoutInflater inf = (LayoutInflater) act.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inf.inflate(R.layout.view_grid_explore, null);
holder = new ViewHolder();
holder.img = (ImageView) convertView.findViewById(R.id.img_grid_album);
} else {
holder = (ViewHolder) convertView.getTag();
}
ImageLoader.getInstance().displayImage(
Static_Urls.explore_pic + data.get(position).talk_pic,
holder.img);
convertView.setTag(holder);
notifyDataSetChanged();
return convertView;
}
Note : Don't forget to remove notifyDataSetChanged();.
This is happening because once the images are downloaded in the device by UIL(Universal Image Loader), it caches the images in Memory and device.
By using this code :
ImageLoader.getInstance().displayImage(Static_Urls.explore_pic +data.get(position).talk_pic,
holder.img);
every time getView() is called UIL tries to fetch the image from network, but by the time it releases that image is already being cached so it shows the image after making a network request first.
so in order to get rid of this flickering use this code :
ImageLoader imageLoader = ImageLoader.getInstance();
File file = imageLoader.getDiskCache().get(Static_Urls.explore_pic +data.get(position).talk_pic);
if (file==null) {
//Load image from network
imageLoader.displayImage(Static_Urls.explore_pic +data.get(position).talk_pic,
holder.img);
}
else {
//Load image from cache
holder.img.setImageURI(Uri.parse(file.getAbsolutePath()));
}
This code will first check whether the image is already cached or not, Then accordingly fetch image from Network or from cache.
The notifyDataSetChanged() line is redundant there. Working with adapters always keep in mind that(in case of adapters extending BaseAdapter) the getView() method is responsible for inflating the layout of the list item and also updating the UI if if you handle it so(normally you do)
Calling notifyDataSetChanged() will cause the getView() being called again right away which is why you see the flickering.
You should only call notifyDataSetChanged() when you would like to update the adapter content. One example would be when you build yourself a "refresh()" method inside your adapter like:
public void refresh(List<Object> list) {
data.clear();// Assuming data is a List<> object or an implementation of it like ArrayList();
data.addAll(list);
notifyDataSetChanged(); // This will let the adapter know that something changed in the adapter and this change should be reflected on the UI too, thus the getView() method will be called implicitly.
}
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
This question came to me after reading this: Performance tips (specifically the part named "Async loading"). Basically he's trying to save info about a row to see if it's been recycled yet and only set the downloaded image if the row is still visible. This is how he saves the position:
holder.position = position;
new ThumbnailTask(position, holder)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null);
Where ThumbnailTask constructor is:
public ThumbnailTask(int position, ViewHolder holder) {
mPosition = position;
mHolder = holder;
}
In onPostExecute() he then does the before mentioned check:
if (mHolder.position == mPosition) {
mHolder.thumbnail.setImageBitmap(bitmap);
}
I just don't see how this gives any result. The Holder and the position are set in the constructor at the same time, to the same value (the position in the holder is the same as the position in mPosition). They don't get changed during the AsyncTask (it's true that the position might change in getView(), but the ones stored in the AsyncTask as private members are never manipulated). What am I missing here?
Also saving the position doesn't seem like a good option in the first place: I believe that it's not guaranteed to be unique, and if I recall correctly it resets itself to 0 after scrolling. Am I thinking in the right direction?
Background (you probably know this, but just in case): An adapter contains a collection of objects and uses info from these objects to populate Views (each view is a line item in the list). The list view is in charge of displaying those views. For performance reasons the ListView will recycle views that are no longer visible because they scrolled off the top or the bottom of the list. Here's how it does it:
When the ListView needs a new view to display it calls the Adapter's getView with an integer argument "position" to indicate which object in the Adapter's collection it wants to see (position is just a number from 1 to N -1) where N is the count of objects in the adapter.
If it has any views that are no longer visible, it will pass one of them in to the Adapter, too, as "convertView" This says "reuse this old view rather than creating a new one". A big performance win.
The code in the article attaches a ViewHolder object to each view it creates that, among other things, contains the position of the object requested by the ListView. In the article's code, this position is stashed away inside the ViewHolder along with a pointer to the field within the view that will contain the image. The ViewHolder is attached to the View as a tag (a separate topic).
If the view gets recycled to hold a different object (at a different position) then ListView will call Adapter.getView(newPosition, oldView...) The code in the article will store new position into the ViewHolder attached to the oldView. {make sense so far?) and start loading this new image to put into the view.
Now in the article, it is starting an AsyncTask to retrieve data that should go into the view) This task has the position (from the getView call) and the holder (from the oldView). The position tells it what data was requested. The holder tells it what data should currently be diplayed in this view and where to put it once it shows up.
If the view gets recycled again while the AsyncTask is still running, the position in the holder will have been changed so these numbers won't match and the AsyncTask knows it's data is no longer needed.
Does this make it clearer?
When AsyncTask is passed with ViewHolder and position it is given value of position (say 5) and value of reference (not a copy) to ViewHolder object. He also puts current position in ViewHolder (said 5), but the whole "trick" here is that for recycled views, the old ViewHolder object is also re-used (in linked article):
} else {
holder = convertView.getTag();
}
so whatever code references that particular ViewHolder object, will in fact check against its position member value at the moment of doing check, not at the moment of object creation. So the onPostExecute check makes sense, because position value passed to task constructor remains unchanged (in our case it has value of 5) as it is primitive, but ViewHolder object can change its properties, if view will be reused before we reach onPostExecute.
Please note we do NOT copy ViewHolder object in the constructor of the task, even it it looks so. It's not how Java works :) See this article for clarification.
Also saving the position doesn't seem like a good option: I believe
that it's not guaranteed to be unique, and it resets itself to zero
after scrolling. Is this true?
No. Position here means index in *source data set, not visible on the screen. So if you got 10 items to display, but your screen fits only 3 at the time, your position will be in range 0-9 and visibility of the rows does not matter.
As I understand you are trying to cancel the async-loading-task of the image when the view recycled, and no longer on screen.
To achieve that you can set up an RecyclerListener to the listview. It will be invoked when the listview don't need this view (when is not on screen), just before it passes it as a recycled view to the Adapter.
within this listener you can cancel your download task:
theListView.setRecyclerListener(new RecyclerListener() {
#Override
public void onMovedToScrapHeap(View view) {
for( ThumbnailTask task : listOfAllTasks )
task.viewRecycled(task);
}
});
and within ThumbnailTask :
public void viewRecycled(View v){
if(mHolder.theWholeView == v)
v.cancel();
}
Don't for to implement the cancel.
Note that its not the best approach since you should keep track of all your asynctask tasks. note that you could also cancel the task within the adapter where you also get the
public View getDropDownView (int position, View recycledView, ViewGroup parent){
//.. your logic
}
but note that this might require you to allocate the ThumbnailTask within the adapter with is not good practice.
note that you could also use image loading libraries that do eveything for you, from async download to chaching. for instance : https://github.com/nostra13/Android-Universal-Image-Loader
The accepted answer and Marcin's post already describe perfectly what's supposed to happen. However, the linked webpage does not and the google site on this topic is also very vague and only a reference for people who already know about the "trick". So here's the missing part, for future references, which shows the necessary additions to getView().
// The adapter's getView method
public View getView(int position, View convertView, ViewGroup parent) {
// Define View that is going to be returned by Adapter
View newViewItem;
ViewHolder holder;
// Recycle View if possible
if (convertView == null) {
// No view recycled, create a new one
LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
newViewItem = inflater.inflate(R.layout.image_grid_view_item, null);
// Attach a new viewholder
holder = new ViewHolder();
holder.thumbnail = (ImageView) newViewItem.findViewById(R.id.imageGridViewItemThumbnail);
holder.position = position;
newViewItem.setTag(holder);
} else {
// Modify "recycled" viewHolder
holder = (ViewHolder) convertView.getTag();
holder.thumbnail = (ImageView) convertView.findViewById(R.id.imageGridViewItemThumbnail);
holder.position = position;
// Re-use convertView
newViewItem = convertView;
}
// Execute AsyncTask for image operation (load, decode, whatever)
new LoadThumbnailTask(position, holder).execute();
// Return the ImageView
return newViewItem;
}
// ViewHolder class, can be implemented inside adapter class
static class ViewHolder {
ImageView thumbnail;
int position;
}
Ok, so my approach is something like this. I am decoding locally stored encrypted and serialized Objects in an AsyncTask in an Activity. The Activity uses a BaseAdapter (HistoryAdapter) for the data to display. The AsyncTask shows a ProgressDialog until decoding is done. When onProgressUpdate() is first called, the ProgressDialog is cancelled. So far, so good. Next, in onProgressUpdate(), the HistoryAdapter is notified of the changes in the common way, triggering it's getView() method. In the HistoryAdapter's getView(), a second AsyncTask is run to modify the created convertView and set the data onto the View.
Here is where it all fails on me. I inflate the final layout in onProgressUpdate(), and set properties and data on convertView just fine here. The changes just don't show, even though all the data is set...
So, the AsyncTask in HistoryAdapter in itself in fact works perfectly, the changes are just not visible. I tried numurous suggestions mentioned on SO, like invalidating convertView, passing a reference to the ListView and using invalidateViews() (causes an eternal loop but no visible changes, which makes sense).
I really want this, because I really don't want to load the layout with image placeholders before data is available. That I got working, but looks nasty and like the easy way out. So I need the ListView to update (add the item) only when progress is done. Any ideas?
EDIT: to clarify: the data is set on the adapter in just the right time. The problem is, the adapter creates a blank View (placeholder) first (don't know any other way, otherwise you will get a NullPointerException in getView), then this View is inflated / replaced with another View in onProgressUpdate(). The second View is the one who should be visible. This works somewhat, because I can get and set properties on the newly inflated View. The changes are just not visible, and I am still seeing the blank, initially created View. I want to update the ListView on each added item, not when all items are done loading...
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
//convertView = mInflater.inflate(R.layout.history_list_item_holo_dark, null);
convertView = mInflater.inflate(R.layout.blank, parent, false); // CHEAT: LOAD BLANK/ EMPTY LAYOUT
HistoryHolder item = history.get(position);
new AsyncRequest(convertView, position).execute(item);
}
this.parent = parent;
return convertView;
}//end method
static class ViewHolder {
TextView TITLE;
TextView SUMMARY;
TextView DATE;
ImageView CONTACT_ICON;
ImageView OPTIONS_ICON;
ImageView TYPE_ICON;
}//end class
private class AsyncRequest extends AsyncTask<HistoryHolder, View, View> {
ViewHolder holder = null;
String title = "";
String summary = "";
String date = "";
long id = 0;
private View convertView = null;
private String type = "";
private int position = -1;
public AsyncRequest(View superView, int position){
this.convertView = superView;
this.position = position;
}//end constructor
#Override
protected View doInBackground(HistoryHolder... item) {
Thread.currentThread().setName(getClass().getSimpleName());
if (item[0].TYPE.equals("log")){
//massive decrypting going on here 50-100 ms
//values like title and summray set here
}
if (item[0].TYPE.equals("sms")){
//massive decrypting going on here 50-100 ms
//values like title and summray set here
}
publishProgress(convertView);
return convertView;
}// end method
#Override
protected void onProgressUpdate(View... view) {
super.onProgressUpdate(view);
}// end method
#Override
protected void onPostExecute(View result) {
super.onPostExecute(result);
if (!MathUtils.isEven(position)){
result .setBackgroundColor(context.getResources().getColor(R.color.darker)); //this works as expected, list items vary in color
} else {
result .setBackgroundColor(context.getResources().getColor(R.color.medium_dark));
} //this works as expected, list items vary in color
result = mInflater.inflate(R.layout.history_list_item_holo_dark, parent, false);
result.setTag(id);
holder = new ViewHolder();
holder.TITLE = (TextView) result .findViewById(R.id.title);
holder.SUMMARY = (TextView) result .findViewById(R.id.summary);
holder.DATE = (TextView) result .findViewById(R.id.date);
holder.CONTACT_ICON = (ImageView) result .findViewById(R.id.icon);
holder.TYPE_ICON = (ImageView) result .findViewById(R.id.type);
holder.OPTIONS_ICON = (ImageView) result .findViewById(R.id.options);
holder.OPTIONS_ICON.setFocusable(false);
holder.OPTIONS_ICON.setTag(id);
holder.TITLE.setText(title); //this change doesnt show
holder.SUMMARY.setText(summary); //and so on
result .setTag(holder);
}//end method
}//end inner class
And I know I could modify my AsynTask and that I don't need to pass reference to the View in so many places, but then again, it's code in progress. Simplified example...
EDIT
Okay, so it seems my approach was poor to begin with, resulting in the need to have a AsyncTask in my HistoryAdapter. I adressed a few issues to resolve this.
Based on #Delyan 's suggestion, I decided it was good to load/ decrypt data before it is actually needed. I am using a PropertyChangeListener for this. This implements a OnSharedPreferenceChangeListener so that it get's notified of changes to the data I need. Changes are then propagated to any interested listeners. The data is decrypted on application start and stored in a global variable, which is accesible throughout the application. See this as the 'memory cache' he referred to.
Based on the comments and on the accepted answer, decrypting now is done in the background, so there is no longer a need for AsyncTasks.
To further optimise the performance of my adapter, I am storing images needed for the ListView in a SparseArray, so they are only created and stored once. Don't use a HashMap for this! Furthermore, the images are only created for the current View if they aren't already in a HashMap (images aren't unique).
public class HistoryAdapter extends BaseAdapter{
private static Context context = ApplicationSubclass.getApplicationContext_();
private Contacts contacts = Contacts.init(context);
private SparseArray<Drawable> c_images = new SparseArray<Drawable>();
private HashMap<Long, Drawable> contact_imgs = new HashMap<Long, Drawable>();
private ArrayList<HistoryHolder> history;
private LayoutInflater mInflater;
public HistoryAdapter(Context context) {
HistoryAdapter.context = context;
mInflater = LayoutInflater.from(context);
}//end constructor
...
public View getView(int position, View convertView, ViewGroup parent) {
final HistoryHolder item = history.get(position);
Drawable d = null;
if (c_images.get(position) == null){
if (!contact_imgs.containsKey(item.CONTACT_ID)){
if (item.IN_CONTACTS && item.CONTACT_ID != 0 && item.CONTACT_ID != -1){
Bitmap photo = contacts.getContactPhotoThumbnailByContactId(item.CONTACT_ID);
if (photo != null){
d = Convert.bitmapToDrawable(context, photo, 128, 128);
} else {
d = context.getResources().getDrawable(R.drawable.ic_contact_picture);
}
} else {
d = context.getResources().getDrawable(R.drawable.ic_contact_picture);
}
contact_imgs.put(item.CONTACT_ID, d);
}
}
c_images.put(position, contact_imgs.get(item.CONTACT_ID));
ViewHolder holder;
if (convertView == null) {
convertView = mInflater.inflate(R.layout.history_list_item_holo_dark, null);
holder = new ViewHolder();
holder.POSITION = position;
holder.TITLE = (TextView) convertView.findViewById(R.id.title);
holder.SUMMARY = (TextView) convertView.findViewById(R.id.summary);
holder.DATE = (TextView) convertView.findViewById(R.id.date);
holder.CONTACT_ICON = (ImageView) convertView.findViewById(R.id.icon);
holder.CONTACT_ICON.setTag(position);
holder.OPTIONS_ICON = (ImageView) convertView.findViewById(R.id.options);
holder.OPTIONS_ICON.setFocusable(false);
holder.OPTIONS_ICON.setTag(position);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.CONTACT_ICON.setBackgroundDrawable(c_images.get(position));
holder.TITLE.setText(item.TITLE);
holder.SUMMARY.setText(item.SUMMARY);
holder.SUMMARY.setMaxLines(2);
holder.DATE.setText(item.DATE);
if (!MathUtils.isEven(position)){
convertView.setBackgroundColor(context.getResources().getColor(R.color.darker));
} else {
convertView.setBackgroundColor(context.getResources().getColor(R.color.medium_dark));
}
return convertView;
}//end method
static class ViewHolder {
TextView TITLE;
TextView SUMMARY;
TextView DATE;
ImageView CONTACT_ICON;
ImageView OPTIONS_ICON;
int POSITION;
}//end inner class
}//end class
Dmmh, what you say, you are wanting to do (So I need the ListView to update (add the item) only when progress is done) and what you are doing (AsyncTask in getView) are quite the opposite things.
AsyncTask in the end of getView is used (however in different way) for lazy image load i.e. to show large images and show text+imageplaceholder until download is complete.
You are trying to gradually fill in your adapter's datasourse in the First AsyncTask, but every time, you notify observers about changes in dataset you will have another cycle of getView calls for every item in dataset. No good.
First, never, NEVER!!! assume that getView will supply you back a convertview, previously filled for this very position. So you MUST either refill convert view with new values, or turn off performance optimization and supply new view every time you are asked for it. There's no way for ListView to turn off recycling attempts because this is the essence of ListView, the feature it is build upon.
Second (resulted from first), avoid at all means storing time-expensive (or user input) data into your newly created Views only. Views come and go, and you do not want to walk the long way to get the expensive data (or just lose user input). The only partial exclusion are simple ineffective implementations of big image lazy loading. To reduce memory usage they download only images that are currently visible by user. More effective implementations use off-ListView caching.
So, if you really want to have the items in your ListView to be added one at a time, but in full glory, you should:
0*. If you have to load user-provided icons and this takes significant time to load (I have not understand your initial post about that) make an empty ArrayList to cache loaded ones and access them by index. If all images are already available by some index, ignore this matter.
class DecryptedRecord {
String title = "";
String summary = "";
String date = "";
int contactViewIndex = -1;
int contactOptionsIndex = -1;
int contactImageIndex = -1;
int typeImageIndex = -1;
int optionsImageIndex = -1; //if option icon varies. ignore otherwise
}
Declare DecryptedRecord class, containing necessary data to fill in the view, e.g.:
In your AsyncTask: after loading every HistoryHolder, perform "heavy decrypting" and fill new DecryptedRecord with the values. If it is necessary to load cusom image(see no.0*), load it here and store its index in cache(ignore this if 0* is irrelevant). Attach filled DecryptedRecord to the HistoryHolder with setTag(). Call publishProgress(HistoryHolder)
In onProgressUpdate just add HistoryHolder to Adapter and call historyAdapter.notifyDataSetChanged();
In getView() synchronously inflate the view if convertView is null, and IN ALL CASES populate it with the data from DecryptedRecord acqired from HistoryHolder.getTag(). Inclusive, call setImageBitmap() on your ImageViews adressing necessary Bitmap in the corresponding list by index.
This should do what you want. Please, if you would have errors/problems, try to include complete code in your question, or it will be very difficult to understand the details of your situation. Hope that helps.
You need to separate your concerns. Decrypting/decoding data has no place in the UI layer.
In general, your current approach to have AsyncTasks per item is difficult and (some would say) wrong for multiple reasons:
Honeycomb and above, there's only ever one AsyncTask running at any one point in time, unless you explicitly set the Executor.
More importantly, by holding a reference to convertView, you're leaking abstraction from the ListView - it's possible that the view you're holding a reference to is being reused for a different position. Unless you take painstaking care to cancel AsyncTasks and ensure proper result delivery, this will cause you trouble.
As mentioned above, decrypting/decoding data has no place in the UI layer (and I consider the Adapters UI layer, since they have similar constraints on execution speed).
If I were you, I'd use a memory cache of decrypted data, expanding/shrinking it as demand changes. Then, I would just fetch decrypted data in the getView() method of the adapter. In order to avoid decrypting items when scrolling, you can set up a scroll listener on the ListView, so that you only show the items when the list is not moving. There's a demo in ApiDemos that does something similar.
EDIT:
As for your obvious problem, you're reinflating a view (result) without adding it to the list item (the convertView field in the task). You can fix that by adding it to convertView (in an empty layout, for example). Again, this will not work as you expected in all cases.
You could create a content provider which has your decoded data stored in a db/temp data structure and use that to update your views. The decoding etc could happen in a background via a service/thread. The adapter could talk to the decoded data via this provider. This is related to alexei burmistrov idea mentioned above.
Not the good option - To use your current layout - a layout file that has both blank & filled layouts. When the layout is first shown, the blank view is visible. and when task is finished the visibility is set to gone.
This solution is not optimal as the asynctask would run each time a getView is called. And Android re-uses views so it could be many times based on how you scroll on the UI etc.
sample code:
public void getView(){
result = mInflater.inflate(R.layout.history_list_item_holo_dark, parent, false);
holder = new ViewHolder();
holder.TITLE = (TextView) result .findViewById(R.id.title);
holder.SUMMARY = (TextView) result .findViewById(R.id.summary);
..
holder.OPTIONS_ICON.setTag(id);
result.setTag(holder);
result.setTag(R.id.view_id ,id);
AsyncTask.execute(result);
convertView = result;
}
AsyncTask(View ...){
onPostExecute(View view){
ViewHolder holder = view.getTag();
if(holder != null){
//set visibility of views in holder
//update Text & data
}
}
}
First, when the async task runs the onPostExecute you will have to set the data to the adapter and notifydatasetchanged().
Now be a little more clean with the list view when is empty using something like creating a progressBar and set it to the listview like this:
listView.setEmptyView(progressbar);
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