2

I have an extended BaseAdapter in a ListActivity:

private static class RequestAdapter extends BaseAdapter {

and some handlers and runnables defined in it

// Need handler for callbacks to the UI thread
    final Handler mHandler = new Handler();

    // Create runnable for posting
    final Runnable mUpdateResults = new Runnable() {
        public void run() {
            loadAvatar();
        }
    };

    protected static void loadAvatar() {
        // TODO Auto-generated method stub
        //ava.setImageBitmap(getImageBitmap("URL"+pic));
        buddyIcon.setImageBitmap(avatar);
    }

In the getView function of the Adapter, I'm getting the view like this:

if (convertView == null) {
            convertView = mInflater.inflate(R.layout.messageitem, null);

            // Creates a ViewHolder and store references to the two children views
            // we want to bind data to.
            holder = new ViewHolder();
            holder.username = (TextView) convertView.findViewById(R.id.username);
            holder.date = (TextView) convertView.findViewById(R.id.dateValue);
            holder.time = (TextView) convertView.findViewById(R.id.timeValue);
            holder.notType = (TextView) convertView.findViewById(R.id.notType);
            holder.newMsg = (ImageView) convertView.findViewById(R.id.newMsg);
            holder.realUsername = (TextView) convertView.findViewById(R.id.realUsername);
            holder.replied = (ImageView) convertView.findViewById(R.id.replied);
            holder.msgID = (TextView) convertView.findViewById(R.id.msgID_fr);
            holder.avatar = (ImageView) convertView.findViewById(R.id.buddyIcon);
            holder.msgPreview = (TextView) convertView.findViewById(R.id.msgPreview);


            convertView.setTag(holder);
        } else {
            // Get the ViewHolder back to get fast access to the TextView
            // and the ImageView.
            holder = (ViewHolder) convertView.getTag();
        }

and the image is getting loaded this way:

Thread sepThread = new Thread() {
                    public void run() {
                        String ava;
                        ava = request[8].replace(".", "_micro.");
                        Log.e("ava thread",ava+", username: "+request[0]);
                        avatar = getImageBitmap(URL+ava);
                        buddyIcon = holder.avatar;
                        mHandler.post(mUpdateResults);
                        //holder.avatar.setImageBitmap(getImageBitmap(URL+ava));
                    }
                };
                sepThread.start();

Now, the problem I'm having is that if there are more items that need to display the same picture, not all of those pictures get displayed. When you scroll up and down the list maybe you end up filling all of them.

When I tried the commented out line (holder.avatar.setImageBitmap...) the app sometimes force closes with "only the thread that created the view can request...". But only sometimes.

Any idea how I can fix this? Either option.

Bostjan
  • 1,397
  • 3
  • 21
  • 36
  • I had the same issue a few weeks ago, and I think it was due to the speed of loading from the web because when I changed the way of loading, every single picture displayed properly. now I am downloading the pictures at the beginning of the app (on request only ) and put them in database, and get them from the DB to the listview. – Sephy Jun 02 '10 at 11:54
  • It's also worth caching the images whilst your app is open. private static HashMap sImageCache; Store them in a hashmap and use the url as the identifier. – m6tt Jun 02 '10 at 12:19

3 Answers3

1

So you shouldn't try and add the image to the view in another thread. My advice would be to use an AsyncTask something like this:

class GetImageTask extends AsyncTask<String, int[], Bitmap> {

    @Override
    protected Bitmap doInBackground(String... params) {
      Bitmap bitmap = null;

      // Get your image bitmap here

      return bitmap;
    }

    @Override
    protected void onPostExecute(Bitmap bitmapResult) {
      super.onPostExecute(bitmapResult);
      // Add your image to your view
      holder.avatar.setImageBitmap(bitmapResult);
    }
}

You call an AsyncTask like:

new GetImageTask().execute(param1, param2, etc);

For more info on an AsyncTask have a look at: http://developer.android.com/reference/android/os/AsyncTask.html

m6tt
  • 4,223
  • 1
  • 22
  • 20
  • 1
    What happens during a fast scroll, if the view to which a bitmap is related is recycled before the bitmap has finished loading? – Sephy Jun 02 '10 at 11:57
  • I wasn't aware of this class, much cleaner than using `Thread`s and `Handler`s – chrisbunney Jun 02 '10 at 12:00
  • Yeh that can be tricky when loading images lazily in a listView. Your best bet would be to pass in an object that contains the imageView itself and the url of the image to load. Store these in a List<>. Then when you try to load a new image check if any earlier instances of the view exist in this List and remove them. Also if you are doing a fast scroll I would disable image load until the scrolling slows down. – m6tt Jun 02 '10 at 12:09
  • Let me see if I understand this ok. I make a list that contains the imageview and the url. Then when it comes a time for a pic to load I check in the list? Do you mean check for the same URL? And why remove them? – Bostjan Jun 02 '10 at 12:19
  • So every time you go to load an image you wrap the url and the imageview in an object and put it in a List. Then you check the earlier entries in the list to see if your new object.imageView.equals(old imageView from the list). If you get a match then you can remove the older entry from the list as that has now had it's view recycled. Once you have loaded an image make sure you remove it from the list also. – m6tt Jun 02 '10 at 12:27
1

When getView is called you've got a avatar ImageView. You should pass this instance to sepThread, and sepThread should pass this instance to mUpdateResults. This way the bitmap will be displayed exactly to ImageView it was downloaded for. Otherwise bitmap is displayed to some budddyIcon instance that is incorrect. Your thread was downloading image for some time and when it's ready budddyIcon references another ImageView because another getView has already been called.

Should look something like that:

public View getView(...){
  //skip
  (new SepThread(holder.avatar)).start();
}

public class SepThread extends Thread() {
    ImageView imageView;
    public SepThread(ImageVIew iv){
      imageView=iv;
    }
    public void run() {
        //skip
        Bitmap avatar = getImageBitmap(URL+ava);
        mHandler.post(new UpdateResults(imageView, avatar));
    }
};

class UpdateResults extends Runnable() {
  ImageView imageView;
  Bitmap bitmap;
  public UpdateResults(ImageView iv, Bitmap b){
    imageView=iv;
    bitmap=b;
  }
  public void run() {
      loadAvatar(imageView, bitmap);
  }
};

protected static void loadAvatar(ImageView iv, Bitmap b) {
    iv.setImageBitmap(b);
}

And of course you should be aware of recycled convertViews as disretrospect says above.

I made a complete example of LazyList and posted the source, may also be helpful Lazy load of images in ListView.

Community
  • 1
  • 1
Fedor
  • 43,261
  • 10
  • 79
  • 89
  • How do I pass the instance to a Thread though? And then back to the runnable? I agree that that would be the best way. – Bostjan Jun 02 '10 at 18:32
1

there's this awesome library guys for downloading/caching Images from the web https://github.com/koush/UrlImageViewHelper

just put the imageview and url and you're done =)

koush
  • 2,972
  • 28
  • 31
Vasile Surdu
  • 1,203
  • 1
  • 10
  • 11