2

I have a ListView whose elements are photos I need to load from a remote API which I'm using loopj to access.

After loopj downloads the photo data, I use an AsyncTask to process the data into a scaled bitmap.

This usually works great, however..

The problem

In my AsyncTask's onPostExecute method, I call setImageBitmap on the ImageView. Occasionally, the bitmap simply doesn't appear in the ImageView after ImageView.setImageBitmap is called. I have verified through logs and also the debugger that ImageView.setImageBitmap is indeed called on the offending ImageView.

If I scroll the list a bit, the list element redraws, and the image is loaded from memory cache and correctly appears on the ImageView.

Here's my code:

public void setPersonImage(final ImageView personImageView, final Person p) {
    personImageView.setImageResource(R.drawable.empty_image);

    //kills unecessary requests for the imageview's previous occupant photo
    if(personImageView.getTag() != null) {
        AsyncHttpClient client = mPhotoRequestsMap.get(p.getEmployeeId());
        if(client != null) {
            client.cancelRequests(getActivity().getApplicationContext(), true);
            Log.d(TAG, "Cancled photo request for " + String.valueOf(p.getEmployeeId()));
        }
    }

    personImageView.setTag(p.getEmployeeId());

    //first attempt to grab photo from cache
    Bitmap cachedBitmap = mMemoryCache.get(p.getPhotoCacheKey());
    if(cachedBitmap != null) {
        personImageView.setImageBitmap(cachedBitmap);
        Log.d(TAG, "bitmap for " + String.valueOf(p.getEmployeeId()) + " retrieved from memory cache");
    } else {

        AsyncHttpClient client = ApiWrapper.getPersonImage(180,mUserSession, 
            getActivity().getApplicationContext(), p.getEmployeeId(),
            new MyAsyncHttpResponseHandler(this) {
               @Override
               public void onSuccess(String response) {
                    if(personImageView.getTag().equals(p.getEmployeeId())) {
                        // use async task to process the bitmap off the UI thread
                        AsyncTask<String,Integer,Bitmap> task = new AsyncTask<String,Integer,Bitmap>() {
                            @Override
                            protected Bitmap doInBackground(String... params) {
                                String response = params[0];
                                byte[] bitmapBytes = ApiWrapper.processImageResponse(response);
                                Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapBytes, 0, bitmapBytes.length);
                                Bitmap bmpScaled = ThumbnailUtils.extractThumbnail(bitmap, 180, 180);
                                return bmpScaled;
                            }

                            protected void onPostExecute(Bitmap bitmap) {
                                //set on imageview if tags agree 
                                if(personImageView.getTag().equals(p.getEmployeeId())) {
                                    Log.d(TAG, "setting image view for " + p.getEmployeeId());
                                    personImageView.setImageBitmap(bitmap);
                                    mMemoryCache.put(p.getPhotoCacheKey(), bitmap);

                                }
                                else {
                                    Log.d(TAG, "tags don't match after decode bitmap, discarding...");
                                }
                            }

                        };

                        task.execute(response);
                    }
                    else {
                        Log.d(TAG, "tags don't match BEFORE decode bitmap employee id: " + p.getEmployeeId() + " tag: " + personImageView.getTag());
                    }
               }


           }
        );

        //create entry in PhotoRequestsMap table
        mPhotoRequestsMap.put(p.getEmployeeId(), client);

    } // end if not in cache
}

I've tried calling invalidate() on the ImageView after the bitmap is set on it, but sadly that doesn't solve it..

I'd greatly appreciate any help or pointers.

Update

The getView method from my Adapter:

public View getView(int position, View convertView, ViewGroup parent) {

            // If we weren't given a view, inflate one
            if (convertView == null) {
               convertView = getActivity().getLayoutInflater().inflate(R.layout.list_item_person, null);
            }

            Person p = getItem(position);

            TextView nameTextView = (TextView)convertView.findViewById(R.id.person_ListItemNameTextView);
            nameTextView.setText(p.getFirstName() + " " + p.getLastName());

            TextView idTextView = (TextView)convertView.findViewById(R.id.person_ListItemIdTextView);
            idTextView.setText(String.valueOf(p.getEmployeeId()));

            ImageView personImageView = (ImageView)convertView.findViewById(R.id.person_ListItemImageView);
            setPersonImage(personImageView, p);

            return convertView;
        }
James
  • 5,273
  • 10
  • 51
  • 76
  • `personImageView` is being called multiple times, before the first AsyncTask finishes, the second overwrites it. Try making array of objects. – Shakti Jan 24 '14 at 11:22
  • I'm guessing that you are re-using the same Imageviews for different List rows? Is it possible to see the `getView(...)` method from your adapter? – Ernir Erlingsson Jan 24 '14 at 20:43
  • @ErnirErlingsson I think android will recycle the imageViews themselves by default.. I am caching the bitmaps though. I updated the question with my getView() method. Thanks! – James Jan 24 '14 at 20:53

1 Answers1

1

I think the .setImageBitmap(Bitmap) method is working fine, you are just setting it with null.

Check for the case of bitmap == null in the onPostExecute(Bitmap bitmap). This also fits to your observation that the image appears when you scroll, because Android calls getView(..) again for the visible rows at that time.

Ernir Erlingsson
  • 2,150
  • 1
  • 19
  • 17
  • The bitmaps being set are not null. I've verified this through the debugger and also just now by adding a null check like you described. I'm seeing the same result.. Thanks for trying, though :) – James Jan 24 '14 at 21:46
  • 1
    Did you try to inflate a new View every time `getView(..)` is called? Try removing the `if (convertView == null)` and check if the problem persists (not that I am recommending it, just to check if the problem lies with the View's re-usage). – Ernir Erlingsson Jan 24 '14 at 21:56
  • I removed the if (convertView == null) and it appears to be working! I'm trying to figure out what I should do now since inflating the view every time probably isn't a sustainable approach? – James Jan 24 '14 at 22:26
  • 1
    Correct! That is too slow. I suspect the problem is related to the canceling code. Put `if (convertView == null)` back in, and this time try removing the `client.cancelRequest..` code. – Ernir Erlingsson Jan 24 '14 at 22:52
  • It looks like you are right. I removed the canceling code and it now works as expected. The canceling code deals with the problem of if the user flings through a long list of images that need to be loaded, the app will make a lot of unnecessary requests. How is this problem typically dealt with? – James Jan 27 '14 at 00:09
  • Typically this is correct, however, there is obviously something wrong with this particular code. My hunch is that it lies with the `AsyncHttpClient` library, which I have not used. Make sure that .getPersonImage(..) is returning a unique object, also make sure that .getEmployeeId() returns a unique identifier for different instantiations. – Ernir Erlingsson Jan 27 '14 at 09:32