johann
johann

Reputation: 1135

ANDROID : Load asynchronous pictures in listView

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

Answers (3)

Christian
Christian

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

Shubhayu
Shubhayu

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

Laith Alnagem
Laith Alnagem

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

Related Questions