In the preceding sections we developed an AsyncTaskLoader
that can load a single image thumbnail as a Bitmap
, and a CursorLoader
that loads a list of all available images on the device. Let's bring them together to create an app that tiles thumbnails of all the images on the device in a scrollable grid, performing all loading in the background.
Thanks to our CursorLoader
, we already have access to the IDs of the images we need to load—we're displaying them as text—so we just need to pass those IDs to our ThumbnailLoader
for it to asynchronously load the image for us.
Recall that ThumbnailLoader
was set up to load one Bitmap
and cache it forever (that is, until explicitly removed from LoaderManager
). We want to change that so that a single ThumbnailLoader
can first be constructed without a mediaId
, and later be told to load an image with a particular mediaId
.
public ThumbnailLoader(Context context) { super(context); }
We'll enable ThumbnailLoader
to load a new image instead of its current one, by setting a new mediaId
. Since the bitmap is cached, just setting a new ID won't suffice—we also need to trigger a reload:
public void setMediaId(Integer newMediaId) {
if ((mediaId != null) && (!mediaId.equals(newMediaId))
this.mediaId = newMediaId;
onContentChanged();
}
}
What's this? onContentChanged
is a method of the abstract Loader
superclass, which will force a background load to occur if our Loader
is currently in the "started" state. If we're currently "stopped", a flag will be set and a background load will be triggered next time the Loader
is started. Either way, when the background load completes, onLoadFinished
will be triggered with the new data.
We need to make a few changes to our onStartLoading
method to correctly handle the case where we were "stopped" when onContentChanged
was invoked. Let's remind ourselves of what it used to look like:
@Override protected void onStartLoading() { if (data != null) { deliverResult(data); } else { forceLoad(); } }
Now let's update onStartLoading
to check if we need to reload our data, and respond appropriately:
@Override
protected void onStartLoading() {
if (data != null)
deliverResult(data);
if (takeContentChanged() || data == null)
forceLoad();
}
The updated onStartLoading
still delivers its data immediately—if it has any. It then calls takeContentChanged
to see if we need to discard our cached Bitmap
and load a new one. If takeContentChanged
returns true
, we invoke forceLoad
to trigger a background load and redelivery.
Now we can cause our ThumbnailLoader
to load and cache a different image, but a single ThumbnailLoader
can only load and cache one image at a time, so we're going to need more than one active instance.
Let's walk through the process of modifying MediaCursorAdapter
to initialize a ThumbnailLoader
for each cell in the GridView
, and to use those Loaders to asynchronously load the images and display them.
First, we can no longer rely on SimpleCursorAdapter
, so we'll need to subclass CursorAdapter
instead. We'll also need LoaderManager
, which we'll pass to the constructor, and we'll grab LayoutInflater
from the Context
to use later when we create the View
objects:
public class MediaCursorAdapter extends CursorAdapter { private LoaderManager mgr; private LayoutInflater inf; private int count; private List<Integer> ids; public MediaCursorAdapter(Context ctx, LoaderManager mgr) { super(ctx.getApplicationContext(), null, true); this.mgr = mgr; inf = (LayoutInflater) ctx. getSystemService(Context.LAYOUT_INFLATER_SERVICE); ids = new ArrayList<Integer>(); } @Override public View newView( Context ctx, Cursor crsr, ViewGroup parent) { return null; // todo } @Override public void bindView(View view, Context ctx, Cursor crsr) { // todo } }
We have two methods to implement—newView
and bindView
. GridView
will invoke newView
until it has enough View
objects to fill all of its visible cells, and from then on it will recycle these same View
objects by passing them to bindView
to be repopulated with data for a different cell as the grid scrolls. As a view scrolls out of sight, it becomes available for rebinding.
What this means for us is that we have a convenient method in which to initialize our ThumbnailLoaders
—newView
, and another convenient method in which to retask Loader
to load a new thumbnail—bindView
.
newView
first inflates ImageView
for the cell and gives it a unique ID, then passes it to a ThumbnailCallbacks
class, which we'll meet in a moment. ThumbnailCallbacks
is in turn used to initialize a new Loader
, which shares the ID of the View
. In this way we are initializing a new Loader
for each visible cell in the grid:
@Override public View newView(final Context ctx, Cursor crsr, ViewGroup vg){ ImageView view = (ImageView) inf.inflate(R.layout.example3_cell, vg, false); view.setId( MediaCursorAdapter.class.hashCode() + count++); mgr.initLoader( view.getId(), null, new ThumbnailCallbacks(ctx, view)); ids.add(view.getId()); return view; }
In bindView
, we are recycling each existing View
to update the image being displayed by that View
. So the first thing we do is clear out the old Bitmap
.
Next we look up the correct Loader
by ID, extract the ID of the next image to load from the Cursor
, and load it by passing the ID to a new method of ThumbnailLoader
—setMediaId
.
@Override public void bindView(View view, final Context ctx, Cursor crsr) { ((ImageView)view).setImageBitmap(null); ThumbnailLoader loader = (ThumbnailLoader) mgr.getLoader(view.getId()); loader.setMediaId(crsr.getInt( crsr.getColumnIndex(MediaStore.Images.Media._ID))); }
We need to add one more method to our Adapter
so that we can clean up ThumbnaiLoaders when we no longer need them. We'll call these ourselves when we no longer need these Loaders—for example, when our Activity
is finishing:
public void destroyLoaders() () { for (Integer id : ids) mgr.destroyLoader(id); }
That's our completed Adapter
. Next, let's look at ThumbnailCallbacks
which, as you probably guessed, is just an implementation of LoaderCallbacks
:
public class ThumbnailCallbacks implements LoaderManager.LoaderCallbacks<Bitmap> { private Context context; private ImageView image; public ThumbnailCallbacks(Context context, ImageView image) { this.context = context.getApplicationContext(); this.image = image; } @Override public Loader<Bitmap> onCreateLoader(int i, Bundle bundle) { return new ThumbnailLoader(context); } @Override public void onLoadFinished(Loader<Bitmap> loader, Bitmap b) { image.setImageBitmap(b); } @Override public void onLoaderReset(Loader<Bitmap> loader) { } }
The only interesting things ThumbnailCallbacks
does is create an instance of ThumbnailLoader
, and set a loaded Bitmap
to its ImageView
.
Our Activity
is almost unchanged—we need to pass an extra parameter when instantiating MediaCursorAdapter
and, to avoid leaking the Loaders it creates, we need to invoke the destroyLoaders
method of MediaCursorAdapter
in onPause
or onStop
, if the Activity
is finishing:
@Override protected void onStop() { super.onStop(); if (isFinishing()) { getSupportLoaderManager().destroyLoader(MS_LOADER); adapter.destroyLoaders(); } }
The full source code is available from the Packt Publishing website. Take a look at the complete source code to appreciate how little there actually is, and run it on a device to get a feel for just how much functionality Loaders give you for a relatively small effort!
3.146.255.87