Reputation: 1135
I 'd like to display a ListView with a customized adapter (with picture and text).
Images are loaded from distant servers, so I have decided to use AsyncTask.
Actually, pictures are well displayed but if I scroll down quickly, a wrong picture is displayed during 1/2 secondes (after loading, correct picture appears)
Here is my adapter's code:
public class GiAdapter extends BaseAdapter {
private Context mContext;
private List<SiteStaff> mListAppInfo;
private HashMap<Integer, ImageView> views;
private HashMap<String,Bitmap> oldPicts = new HashMap<String,Bitmap>();
private LayoutInflater mInflater;
private boolean auto;
private final String BUNDLE_URL = "url";
private final String BUNDLE_BM = "bm";
private final String BUNDLE_POS = "pos";
private final String BUNDLE_ID = "id";
public GiAdapter(Context context, List<SiteStaff> list) {
mContext = context;
mListAppInfo = list;
views = new HashMap<Integer, ImageView>();
mInflater = LayoutInflater.from(mContext);
}
@Override
public int getCount() {
return mListAppInfo.size();
}
@Override
public Object getItem(int position) {
return mListAppInfo.get(position).getId();
}
@Override
public long getItemId(int position) {
return mListAppInfo.get(position).getId();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
LinearLayout layoutItem;
// reuse of convertView
if (convertView == null) {
layoutItem = (LinearLayout) mInflater.inflate(R.layout.auto_gi, parent, false);
} else {
layoutItem = (LinearLayout) convertView;
}
// infos for the current element
SiteStaff entry = mListAppInfo.get(position);
//set some text fields
TextView name = (TextView) layoutItem.findViewById(R.id.name);
TextView size = (TextView) layoutItem.findViewById(R.id.size);
name.setText(entry.getName());
size.setText(entry.getSize());
// get the imageView for the current object
ImageView v = (ImageView) layoutItem.findViewById(R.id.gi_image);
// put infos in bundle and send to the LoadImage class
Bundle b = new Bundle();
//url of the pict
b.putString(BUNDLE_URL, entry.getUrl());
//position in the listView
b.putInt(BUNDLE_POS, position);
//id of the current object
b.putInt(BUNDLE_ID, entry.getId());
//put info in the map in order to display in the onPostExecute
views.put(position, v);
// thread
new LoadImage().execute(b);
return layoutItem;
}
//asyncTackClass for loadingpictures
private class LoadImage extends AsyncTask<Bundle, Void, Bundle> {
@Override
protected Bundle doInBackground(Bundle... b) {
Bitmap bm =null;
//cache: for better performance, check if url alredy exists
if(oldPicts.get(b[0].getString(BUNDLE_URL))==null){
bm = Utils.getBitMapFromUrl(b[0].getString(BUNDLE_URL));
oldPicts.put(b[0].getString(BUNDLE_URL),bm);
}else{
bm = oldPicts.get(b[0].getString(BUNDLE_URL));
}
// get info from bundle
Bundle bundle = new Bundle();
bundle.putParcelable(BUNDLE_BM, bm);
bundle.putInt(BUNDLE_POS, b[0].getInt(BUNDLE_POS));
return bundle;
}
@Override
protected void onPostExecute(Bundle result) {
super.onPostExecute(result);
//get picture saved in the map + set
ImageView view = views.get(result.getInt(BUNDLE_POS));
Bitmap bm = (Bitmap) result.getParcelable(BUNDLE_BM);
if (bm != null){ //if bitmap exists...
view.setImageBitmap(bm);
}else{ //if not picture, display the default ressource
view.setImageResource(R.drawable.unknow);
}
}
}
}
Thank you !
Upvotes: 3
Views: 8587
Reputation: 799
Like Shubhayu mentioned, you're seeing wrong images in your ListView rows because your adapter is recycling a view to construct each row, and your Async Tasks are maintaining references to that recycled view. When your tasks complete, they'll update the ImageView, which may actually correspond to some other row by this point.
The problem with inflating a new view in every call to getView is you'll end up with bad performance for your ListView when scrolling. You are (correctly) using convertView to reduce this overhead, you're just missing the piece of correctly tying the ImageView to the Async. I found a solution here: http://android-developers.blogspot.com/2010/07/multithreading-for-performance.html
In summary, it's basically extending a Drawable to encapsulate a task, and only updating the image if the task is appropriately tied to the image. The section "Handling Concurrency" tackles the very issue you're facing. In particular, read the paragraph right before that section for a summary of what's causing your problem.
Upvotes: 6
Reputation: 13552
This happened to me too. What I figured out was that before the new image was getting bound to the imageview, the old one was displayed for a while. In your case, what is happening is that your correct info is showing onPostExecute(), which takes sometime, and before that whatever data was set in the convertview is being shown. There are 2 ways u could mostly fix this,
In your getView(), change the if{}... else{} block to just
layoutItem = (LinearLayout) mInflater.inflate(R.layout.auto_gi, parent, false);
or
set the imageview with some placeholder image in getView() till onPostExecute() is called.
Hope this helps.
Upvotes: 5
Reputation: 654
When you extend BaseAdapter and implement getView(int position, View convertView, ViewGroup parent) the convertView is actually a recycled view.
It is the view that was destroyed (last view to be pushed off the screen). What you can do is clear the convertView so it wont show the stale data. perhaps find the view with the image and do a layoutItem.removeAll() . Hopefully that will clear the views and give you a clean layout to work with.
Upvotes: 1