views:

521

answers:

3

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.

+1  A: 

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

disretrospect
What happens during a fast scroll, if the view to which a bitmap is related is recycled before the bitmap has finished loading?
Sephy
I wasn't aware of this class, much cleaner than using `Thread`s and `Handler`s
chrisbunney
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.
disretrospect
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
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.
disretrospect
Ok, thanks. I'll try to do that.
Bostjan
A: 

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 http://stackoverflow.com/questions/541966/android-how-do-i-do-a-lazy-load-of-images-in-listview/3068012#3068012.

Fedor
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
Updated my answer with code sample
Fedor
A: 

Ok,

I used Fedor's way of passing parameters to the thread and disretrospect's way of checking for recycled spinners and image caching. Great help from both of you guys. Thank you.

One more thing though. In another activity (the starting one of the app), I wanted to the initializing in an asynctask.

So I made this:

class initApp extends AsyncTask<String, Integer, Long> {
    private ProgressDialog Dialog = new ProgressDialog(overview.this);  
    private int results;

     protected void onPreExecute() {  
         Dialog.setMessage(getString(R.string.initApp));  
         Dialog.show();              
     }  
     protected Long doInBackground(String... dummy) {
        results = initApplication();
        return null;
     }


     protected void onPostExecute(Long result) {
         Dialog.dismiss();
         if (results == 1) {
             //msgSent();
         } else {
             //
             showError();
         }
     }

 }

The initApplication() method basically sets all the params I need for my app. One of them being values for a spinner item. Those are set inside an initRadius(), which is called inside initApplication(). initRadius() does these amongst other things:

if (UNITS == 0) {
        Log.e("units","kms");
        rValues = ArrayAdapter.createFromResource(this, R.array.radiusValues,android.R.layout.simple_spinner_item );        
    } else {
        Log.e("units","miles");
        rValues = ArrayAdapter.createFromResource(this, R.array.radiusUSValues,android.R.layout.simple_spinner_item );  
    }
    rValues.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    radius.setAdapter(rValues);

the aSyncTask crashes on radius.setAdapter line. initApplication executes: radius = (Spinner) findViewById(R.id.radius); right before calling initRadius() and radius is declared in the main activity class as:

private Spinner radius;

The task crashes again with "only the thread that created the view can request" exception. The task is called on the "onCreate" method of the main activity class as:

new initApp().execute("dummy");

I thought that asynctask should have the same thread as the UI one? What can I do to fix this? I guess I'm not too clear on threading here. Thanks for the help again.

Bostjan
AsyncTask.doInBackground method is executed in another thread. That's exactly the point of AsyncTask. By using AsyncTask lon-running operations don't block the UI. So you can just call setAdapter in onCreate, that would be the main UI thread and it will work fine.
Fedor