Search code examples
androidandroid-galleryandroid-adapter

android gallery view "stutters" with deferred image loading adapter


I would like to create an deferred loading adapter for use with a Gallery widget.

That is to say getView() returns an ImageView immediately, and later some other mechanism will asynchronously call its setImageBitmap() method. I did this by creating a "lazy" ImageView that extends ImageView.

public class GalleryImageView extends ImageView {

    // ... other stuff here ...

    public void setImage(final Looper looper, final int position) {

    final Uri uri = looper.get(position);
    final String path = looper.sharePath(position);

    new Thread(new Runnable() {

        @Override
        public void run() {
            GalleryBitmap gbmp = new GalleryBitmap(context, uri, path);
            final Bitmap bmp = gbmp.getBitmap(); // all the work is here
            handler.post(new Runnable() {

                @Override
                public void run() {
                    if (GalleryImageView.this.getTag().equals(uri)) {
                        setImageBitmap(bmp);
                    }
                }
            });
        }
    }).start();
}

}

When I scroll slowly in the Gallery, the center image keeps popping into the center. It's hard to explain, exactly, but it's really annoying. I also tried the same approach for a spinner adapter and it works perfectly there.

Any ideas?


Solution

  • The solution is to implement a more intelligent method of when to fetch thumbnails - it is pointless fetching thumbnails while the user is flinging through the list. Essentially you want something like that implemented in Romain Guy's Shelves application.

    To get the most responsive Gallery you'll need to implement some form of in-memory cache and do the following:

    • Only set an image if it exists in the in-memory cache from your getView. Set a flag indicating whether the image was set or whether a download is required. You could also maintain a memory in a cache on the SD card and internal memory, and if a fling is not currently ongoing then show a low res (inSampleSize set to 16 or 8) version which will be visible when just scrolling through - the high res version will load when the user lets go and settles on an image.
    • Add an OnItemSelectedListener (and make sure to call setCallbackDuringFling(false) when initializing) that downloads new thumbnails for all the visible items that require a download only if the users finger is up (you can use getFirstVisiblePosition and getLastVisiblePosition to find the range of views visible)
    • Also when the user lifts their finger check to see 1. if the selected position changed since the user put their finger down and if so 2. whether a download was initiated due to your OnItemSelectedListener - if it wasn't then initiate one. This is to catch the case where no flinging occurs, and thus OnItemSelected never does anything because it is always called with the finger down in this situation. I'd use a Handler to delay starting the downloading by the animation time of your gallery (make sure to clear any delayed messages posted to this handler whenever onItemSelected is called or when you get an ACTION_DOWN event.
    • After an image is downloaded check if any visible views requested this image then and update those views

    Also be aware that the default Gallery component does not properly implement View recycling (it assumes each position in the adapter has a unique view, and also clears the recycler of these items when they go offscreen making it pretty pointless). Edit: on more looking it isn't pointless - but it's not a recycler in terms of next/previous views, rather it serves to avoid having to call getView for the current views during layout changes.

    This means the convertView parameter passed to your getView method will more often that not be null, meaning you'll be inflating a lot of views (which is expensive) - see my answer to Does a replacement for Gallery with View recycling exist? for some hints on that. (PS: I have since modified that code - I would use a different recycle bin for layout phases and scroll phases, in the layout phase place and retrieve the views in the layout recycle bin according to their position, and DO NOT call getView if the view you get from the bin is non-null since it will be exactly the same view; also clear the layout recycle bin after the layout phase -- this makes things a bit more snappier)

    PS: Also be very careful with what you do in OnItemSelected - namely unless it's in the places mentioned above then try to do as little as possible. For instance I was setting some text in a TextView above my Gallery in OnItemSelected. Just moving this call into the same points as where I updated thumbnails made a noticable difference.