ListView recycles images - android

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.

Related

Best strategy to add elements dynamically to an activity

I'm trying to make an activity able to capture up to 4 images for sending them to our server.
I know how to capture the image and to add them to the activity, this already works, in a non efficient nor elegant way, and I would like to improve that.
Right now, I have Button with an onClick method that attaches an image to an empty ImageView, and and keeps track of how many images have been attached, because I can delete an image in order to pick a new one.
I'm wondering the best strategy, code-wise for future changes.
Options that I have considered but I have not (yet) implemented:
Button adds the image to a GridView so I can add and remove images from it's adapter instead.
The ImageView can attach and remove image from itself by onClick and therefore remove the Button
Any suggestions, ideas, strategies, thoughts?
Thank you in advance!
EDIT 1: my first approach to improve the code
I've implemented a GridView with a custom adapter (GridImageAdapter)
public class GridImagesAdapter extends BaseAdapter{
private List<Bitmap> images = new ArrayList<Bitmap>();
private int img_height;
public GridImagesAdapter(Context context, List<Bitmap> imagenes){
this.imagenes = imagenes;
inflater = (LayoutInflater) context.getSystemService( Context.LAYOUT_INFLATER_SERVICE );
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
img_height = (int) (metrics.density * Constants.ONE_ROW_IMG_HEIGHT);
}
/*
* other common methods
*/
#Override
public View getView(int position, View row, ViewGroup parent) {
ViewHolder holder;
if(row == null){
row = inflater.inflate(R.layout.grid_img_item, parent, false);
holder = new ViewHolder();
holder.image = (ImageView) row.findViewById(R.id.img_add_in_grid);
holder.image.setLayoutParams(new GridView.LayoutParams(LayoutParams.MATCH_PARENT, img_height));
holder.image.setScaleType(ImageView.ScaleType.CENTER_CROP);
}else{
holder = (ViewHolder)row.getTag();
}
holder.image.setImageBitmap(getItem(position));
row.setTag(holder);
return row;
}
private class ViewHolder{
ImageView image;
}
}
So I can populate my GridView in the activity like:
private void populateGridView(){
Bitmap a;
a = BitmapFactory.decodeResource(getResources(), R.drawable.no_image);
images.add(a);
images.add(a);
images.add(a);
images.add(a);
GridImagesAdapter adapter = new GridImagesAdapter(this, images);
mGridView.setAdapter(adapter);
mGridView.setOnItemClickListener(new GetImage());
}
where new GetImage() is an OnItemClickListener who takes care of the image capture itself, and replaces one of the R.drawable.no_image Bitmap
(no relevant code in there, just showing a Dialog to the user in order to choose from camera or gallery and start such Activity, and then in the onActivityResult method where I have a Bitmap to work with is where I handle the adapter change)
My question is more about the strategy chosen here than the actual code itself.
Drawbacks? Any more elegant or proper way to achieve the same result?
Everything is welcome, thank you for your answers.
Have u create a gridview and set up the adapter class?
Because I have implemented a similar case. Items are added to listview dynamically (by user input), and there is a button in each row that deletes that particular row (also from the database).
If u have, Let me know from where shall we begin.
Your general idea is more or less correct. You mentioned GetImage is irrelevant, but it absolutely isn't. From I understood, you are changing the view directly in there. Instead you should manipulate the adapter, changing the data it's holding, and then call notifyDataSetChanged. The adapter will take care of notifying the gridview that the data changed and the view will be updated. I would probably use an ArrayAdapter instead of a BaseAdapter as well.

What is the working of setTag and getTag in ViewHolder pattern?

I have a simple code snippet for implementing custom listview.
My code is as follows:
WeatherAdapter.java :
public class WeatherAdapter extends ArrayAdapter<weather>{
Context mcontext;
int mlayoutResourceId;
weather mdata[] = null;
View row;
public WeatherAdapter(Context context, int layoutResourceId, weather[] data) {
super(context, layoutResourceId, data);
mlayoutResourceId = layoutResourceId;
mcontext = context;
mdata = data;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
row = convertView;
WeatherHolder holder = null;
if(row == null)
{
LayoutInflater inflater = ( (Activity) mcontext).getLayoutInflater();
row = inflater.inflate(mlayoutResourceId, parent, false);
holder = new WeatherHolder(row);
row.setTag(holder);
}
else
{
holder = (WeatherHolder)row.getTag();
}
weather w = mdata[position];
holder.txtTitle.setText(w.mtitle);
holder.imgIcon.setImageResource(w.micon);
return row;
}
WeatherHolder.java:
class WeatherHolder
{
ImageView imgIcon;
TextView txtTitle;
public WeatherHolder(View v){
imgIcon = (ImageView)row.findViewById(R.id.imgIcon);
txtTitle = (TextView)row.findViewById(R.id.txtTitle);
}
}
}
I have seen so many answers on SO and other sites and I understood the recycling mechanism of listview.
I also understood that from viewholder, we can hold the child views in the adapter and we do not have to call findViewById() many times. So, it is for optimization.
But I have only the confusion in setTag(holder) and getTag() methods. From this question, I came to know that it is for making a key-value pair on multiple objects, so that we can access them easily. But, I do not understand why they are required here...because, we do not have multiple holder objects...only we have to change holder's variables each time. can we code here without using setTag and getTag?
can anyone explain better that what setTag and getTag do "here"?
tag is a mechanism to make your views remember something, that could be an object an integer a string or anything you like.
so when your ListView is going to create for the first time your convertView is null. so you create a new convertView and put all of your references of the objects of that row in a viewHolder. then save your viewHolder into the memory of that convertView(setTag). Android takes your convertView and puts it in its pool to recycle it and passes it again to you. but its pool may not have enough convertViews so it again passes a new convertView thats null. so again the story is repeated till the pool of android is filled up. after that android takes a convertView from its pool and passes it to you. you will find that its not null so you ask it where are my object references that I gave to you for the first time? (getTag) so you will get those and do whatever you like.
More elaboration on below line
but its pool may not have enough convertViews so it again passes a new convertView thats null
android pool is empty when your listView is going to create. so for the first item of your listView it sends you a convertView that must be displayed. after that android saves it in its pool, so its pool now contains just one convertView. for your second item of your listView that is going to create android can not use its pool because it is actually has one element and that element is your first item and it is being shown right now so it has to pass another convertView. this process repeates until android found a convertView in its pool thats not being displayed now and passes it to you.
Android inflates each row till the screen filled up after that when you scroll the list it uses holder.
Lets Look in a Different Perspective:
Lets imagine that the Helicopter is the "row" while the rope is the "setTag" and the car below is "WeatherHolder", but the pilot of the Helicopter is inside that car and he/she the one managing controlling the helicopter using a "WIRED REMOTE".
When you cut the Rope Which is "setTag" the Hellicopter still fly but the pilot can no longer control it since the pilot is drop in the ground which means the pilot is now dead! (In java when an object loss its reference the Garbage Collector will collect that and free from the memory).
When you did not place or attach the rope to the car while the Helicopter is about to fly where the Pilot is sitting - you potentially loss a control on the helicopter because you are using "WIRED REMOTE".
I hope this help :).
But, I do not understand why they are required here...because, we do not have multiple holder objects
This is where you are wrong - there is one holder per view (aka visible or cached ListView entry).

StaggeredGridView layout inflation issue

I am using StaggeredGridView currently in the project I am working on. But what is happening is the GridView layout is not getting inflated in a correct way. when my layout is loaded the GridView is rendering with let's say 4 column in my tablet(10" Sony Xperia Z) in landscape mode, but sometime somewhere at any random row number the items are not rendering sequentially but at any random position with uneven void space like the following image:
The same type of situation arises when the tablet is in the portrait mode. but surprisingly when the orientation changes then this issue is getting resolved automatically. It's kind of weird problem I am facing right now. I've gone through some research but with no luck.
I've also tried This StaggeredGridView, but the same issue.
Can anyone please help me out there? Thanks in advance.
FYI I am using Viewholder pattern in my Gridview Adapter.
PS:
Is the fix Fixed whitespace related to my concern? I've tried it but no improvement.
I had the same problem in StaggeredGridView during loading images via picasso from network.
When you use:
Picasso.with(mContext).load(getItem(position)).into(holder.imageView);
in your adapter, picasso reserve one grid for your image and SGV make rendering for the content. Then image is loaded at last and SGV makes rendering one more time and images change their position in SGV.
Solution: I check size of the image needed for rendering (I my situation I read it from json) in order to reserve demanding space for the image at the beginning. Please take a look in example:
public class StaggeredAdapter extends ArrayAdapter<String> {
private Context mContext;
public StaggeredAdapter(Context context, int textViewResourceId,
String[] objects) {
super(context, textViewResourceId, objects);
mContext = context;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
LayoutInflater layoutInflator = LayoutInflater.from(getContext());
convertView = layoutInflator.inflate(R.layout.row_staggered_demo, null);
holder = new ViewHolder();
holder.imageView = (ImageView) convertView .findViewById(R.id.imageView1);
convertView.setTag(holder);
}
holder = (ViewHolder) convertView.getTag();
// Calculate size of the element.
int height = (position % 3 == 0) ? 600 : (1200 / (position % 3));
convertView.setLayoutParams(new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, height));
Picasso.with(mContext).load(getItem(position)).into(holder.imageView);
return convertView;
}
static class ViewHolder {
ImageView imageView;
}
}
It may be caused by the item layout. Does it contain image? When items are inflated, they may have different size. So the layouting process is computed using different item size. Try to use stub images which are used before image loading.

Forcing a BaseAdapter to update it's views when AsyncTask is done

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);

Issue when fast scroll a listview

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..... ??

Categories

Resources