blob: 4147f83a9954bf35b67591c7e404d0f90a3341c0 [file] [log] [blame]
page.title=Displaying Bitmaps in Your UI
parent.title=Displaying Bitmaps Efficiently
parent.link=index.html
trainingnavtop=true
@jd:body
<div id="tb-wrapper">
<div id="tb">
<h2>This lesson teaches you to</h2>
<ol>
<li><a href="#viewpager">Load Bitmaps into a ViewPager Implementation</a></li>
<li><a href="#gridview">Load Bitmaps into a GridView Implementation</a></li>
</ol>
<h2>You should also read</h2>
<ul>
<li><a href="{@docRoot}design/patterns/swipe-views.html">Android Design: Swipe Views</a></li>
<li><a href="{@docRoot}design/building-blocks/grid-lists.html">Android Design: Grid Lists</a></li>
</ul>
<h2>Try it out</h2>
<div class="download-box">
<a href="{@docRoot}downloads/samples/DisplayingBitmaps.zip" class="button">Download the sample</a>
<p class="filename">DisplayingBitmaps.zip</p>
</div>
</div>
</div>
<p></p>
<p>This lesson brings together everything from previous lessons, showing you how to load multiple
bitmaps into {@link android.support.v4.view.ViewPager} and {@link android.widget.GridView}
components using a background thread and bitmap cache, while dealing with concurrency and
configuration changes.</p>
<h2 id="viewpager">Load Bitmaps into a ViewPager Implementation</h2>
<p>The <a href="{@docRoot}design/patterns/swipe-views.html">swipe view pattern</a> is an excellent
way to navigate the detail view of an image gallery. You can implement this pattern using a {@link
android.support.v4.view.ViewPager} component backed by a {@link
android.support.v4.view.PagerAdapter}. However, a more suitable backing adapter is the subclass
{@link android.support.v4.app.FragmentStatePagerAdapter} which automatically destroys and saves
state of the {@link android.app.Fragment Fragments} in the {@link android.support.v4.view.ViewPager}
as they disappear off-screen, keeping memory usage down.</p>
<p class="note"><strong>Note:</strong> If you have a smaller number of images and are confident they
all fit within the application memory limit, then using a regular {@link
android.support.v4.view.PagerAdapter} or {@link android.support.v4.app.FragmentPagerAdapter} might
be more appropriate.</p>
<p>Here’s an implementation of a {@link android.support.v4.view.ViewPager} with {@link
android.widget.ImageView} children. The main activity holds the {@link
android.support.v4.view.ViewPager} and the adapter:</p>
<pre>
public class ImageDetailActivity extends FragmentActivity {
public static final String EXTRA_IMAGE = "extra_image";
private ImagePagerAdapter mAdapter;
private ViewPager mPager;
// A static dataset to back the ViewPager adapter
public final static Integer[] imageResIds = new Integer[] {
R.drawable.sample_image_1, R.drawable.sample_image_2, R.drawable.sample_image_3,
R.drawable.sample_image_4, R.drawable.sample_image_5, R.drawable.sample_image_6,
R.drawable.sample_image_7, R.drawable.sample_image_8, R.drawable.sample_image_9};
&#64;Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.image_detail_pager); // Contains just a ViewPager
mAdapter = new ImagePagerAdapter(getSupportFragmentManager(), imageResIds.length);
mPager = (ViewPager) findViewById(R.id.pager);
mPager.setAdapter(mAdapter);
}
public static class ImagePagerAdapter extends FragmentStatePagerAdapter {
private final int mSize;
public ImagePagerAdapter(FragmentManager fm, int size) {
super(fm);
mSize = size;
}
&#64;Override
public int getCount() {
return mSize;
}
&#64;Override
public Fragment getItem(int position) {
return ImageDetailFragment.newInstance(position);
}
}
}
</pre>
<p>Here is an implementation of the details {@link android.app.Fragment} which holds the {@link android.widget.ImageView} children. This might seem like a perfectly reasonable approach, but can
you see the drawbacks of this implementation? How could it be improved?</p>
<pre>
public class ImageDetailFragment extends Fragment {
private static final String IMAGE_DATA_EXTRA = "resId";
private int mImageNum;
private ImageView mImageView;
static ImageDetailFragment newInstance(int imageNum) {
final ImageDetailFragment f = new ImageDetailFragment();
final Bundle args = new Bundle();
args.putInt(IMAGE_DATA_EXTRA, imageNum);
f.setArguments(args);
return f;
}
// Empty constructor, required as per Fragment docs
public ImageDetailFragment() {}
&#64;Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mImageNum = getArguments() != null ? getArguments().getInt(IMAGE_DATA_EXTRA) : -1;
}
&#64;Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// image_detail_fragment.xml contains just an ImageView
final View v = inflater.inflate(R.layout.image_detail_fragment, container, false);
mImageView = (ImageView) v.findViewById(R.id.imageView);
return v;
}
&#64;Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
final int resId = ImageDetailActivity.imageResIds[mImageNum];
<strong>mImageView.setImageResource(resId);</strong> // Load image into ImageView
}
}
</pre>
<p>Hopefully you noticed the issue: the images are being read from resources on the UI thread,
which can lead to an application hanging and being force closed. Using an
{@link android.os.AsyncTask} as described in the <a href="process-bitmap.html">Processing Bitmaps
Off the UI Thread</a> lesson, it’s straightforward to move image loading and processing to a
background thread:</p>
<pre>
public class ImageDetailActivity extends FragmentActivity {
...
public void loadBitmap(int resId, ImageView imageView) {
mImageView.setImageResource(R.drawable.image_placeholder);
BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
task.execute(resId);
}
... // include <a href="process-bitmap.html#BitmapWorkerTask">{@code BitmapWorkerTask}</a> class
}
public class ImageDetailFragment extends Fragment {
...
&#64;Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (ImageDetailActivity.class.isInstance(getActivity())) {
final int resId = ImageDetailActivity.imageResIds[mImageNum];
// Call out to ImageDetailActivity to load the bitmap in a background thread
((ImageDetailActivity) getActivity()).loadBitmap(resId, mImageView);
}
}
}
</pre>
<p>Any additional processing (such as resizing or fetching images from the network) can take place
in the <a href="process-bitmap.html#BitmapWorkerTask">{@code BitmapWorkerTask}</a> without affecting
responsiveness of the main UI. If the background thread is doing more than just loading an image
directly from disk, it can also be beneficial to add a memory and/or disk cache as described in the
lesson <a href="cache-bitmap.html#memory-cache">Caching Bitmaps</a>. Here's the additional
modifications for a memory cache:</p>
<pre>
public class ImageDetailActivity extends FragmentActivity {
...
private LruCache&lt;String, Bitmap&gt; mMemoryCache;
&#64;Override
public void onCreate(Bundle savedInstanceState) {
...
// initialize LruCache as per <a href="cache-bitmap.html#memory-cache">Use a Memory Cache</a> section
}
public void loadBitmap(int resId, ImageView imageView) {
final String imageKey = String.valueOf(resId);
final Bitmap bitmap = mMemoryCache.get(imageKey);
if (bitmap != null) {
mImageView.setImageBitmap(bitmap);
} else {
mImageView.setImageResource(R.drawable.image_placeholder);
BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
task.execute(resId);
}
}
... // include updated BitmapWorkerTask from <a href="cache-bitmap.html#memory-cache">Use a Memory Cache</a> section
}
</pre>
<p>Putting all these pieces together gives you a responsive {@link
android.support.v4.view.ViewPager} implementation with minimal image loading latency and the ability
to do as much or as little background processing on your images as needed.</p>
<h2 id="gridview">Load Bitmaps into a GridView Implementation</h2>
<p>The <a href="{@docRoot}design/building-blocks/grid-lists.html">grid list building block</a> is
useful for showing image data sets and can be implemented using a {@link android.widget.GridView}
component in which many images can be on-screen at any one time and many more need to be ready to
appear if the user scrolls up or down. When implementing this type of control, you must ensure the
UI remains fluid, memory usage remains under control and concurrency is handled correctly (due to
the way {@link android.widget.GridView} recycles its children views).</p>
<p>To start with, here is a standard {@link android.widget.GridView} implementation with {@link
android.widget.ImageView} children placed inside a {@link android.app.Fragment}. Again, this might
seem like a perfectly reasonable approach, but what would make it better?</p>
<pre>
public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
private ImageAdapter mAdapter;
// A static dataset to back the GridView adapter
public final static Integer[] imageResIds = new Integer[] {
R.drawable.sample_image_1, R.drawable.sample_image_2, R.drawable.sample_image_3,
R.drawable.sample_image_4, R.drawable.sample_image_5, R.drawable.sample_image_6,
R.drawable.sample_image_7, R.drawable.sample_image_8, R.drawable.sample_image_9};
// Empty constructor as per Fragment docs
public ImageGridFragment() {}
&#64;Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mAdapter = new ImageAdapter(getActivity());
}
&#64;Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View v = inflater.inflate(R.layout.image_grid_fragment, container, false);
final GridView mGridView = (GridView) v.findViewById(R.id.gridView);
mGridView.setAdapter(mAdapter);
mGridView.setOnItemClickListener(this);
return v;
}
&#64;Override
public void onItemClick(AdapterView&lt;?&gt; parent, View v, int position, long id) {
final Intent i = new Intent(getActivity(), ImageDetailActivity.class);
i.putExtra(ImageDetailActivity.EXTRA_IMAGE, position);
startActivity(i);
}
private class ImageAdapter extends BaseAdapter {
private final Context mContext;
public ImageAdapter(Context context) {
super();
mContext = context;
}
&#64;Override
public int getCount() {
return imageResIds.length;
}
&#64;Override
public Object getItem(int position) {
return imageResIds[position];
}
&#64;Override
public long getItemId(int position) {
return position;
}
&#64;Override
public View getView(int position, View convertView, ViewGroup container) {
ImageView imageView;
if (convertView == null) { // if it's not recycled, initialize some attributes
imageView = new ImageView(mContext);
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
imageView.setLayoutParams(new GridView.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
} else {
imageView = (ImageView) convertView;
}
<strong>imageView.setImageResource(imageResIds[position]);</strong> // Load image into ImageView
return imageView;
}
}
}
</pre>
<p>Once again, the problem with this implementation is that the image is being set in the UI thread.
While this may work for small, simple images (due to system resource loading and caching), if any
additional processing needs to be done, your UI grinds to a halt.</p>
<p>The same asynchronous processing and caching methods from the previous section can be implemented
here. However, you also need to wary of concurrency issues as the {@link android.widget.GridView}
recycles its children views. To handle this, use the techniques discussed in the <a
href="process-bitmap.html#concurrency">Processing Bitmaps Off the UI Thread</a> lesson. Here is the
updated
solution:</p>
<pre>
public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
...
private class ImageAdapter extends BaseAdapter {
...
&#64;Override
public View getView(int position, View convertView, ViewGroup container) {
...
<strong>loadBitmap(imageResIds[position], imageView)</strong>
return imageView;
}
}
public void loadBitmap(int resId, ImageView imageView) {
if (cancelPotentialWork(resId, imageView)) {
final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
final AsyncDrawable asyncDrawable =
new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
imageView.setImageDrawable(asyncDrawable);
task.execute(resId);
}
}
static class AsyncDrawable extends BitmapDrawable {
private final WeakReference&lt;BitmapWorkerTask&gt; bitmapWorkerTaskReference;
public AsyncDrawable(Resources res, Bitmap bitmap,
BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskReference =
new WeakReference&lt;BitmapWorkerTask&gt;(bitmapWorkerTask);
}
public BitmapWorkerTask getBitmapWorkerTask() {
return bitmapWorkerTaskReference.get();
}
}
public static boolean cancelPotentialWork(int data, ImageView imageView) {
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (bitmapWorkerTask != null) {
final int bitmapData = bitmapWorkerTask.data;
if (bitmapData != data) {
// Cancel previous task
bitmapWorkerTask.cancel(true);
} else {
// The same work is already in progress
return false;
}
}
// No task associated with the ImageView, or an existing task was cancelled
return true;
}
private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
if (imageView != null) {
final Drawable drawable = imageView.getDrawable();
if (drawable instanceof AsyncDrawable) {
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
return asyncDrawable.getBitmapWorkerTask();
}
}
return null;
}
... // include updated <a href="process-bitmap.html#BitmapWorkerTaskUpdated">{@code BitmapWorkerTask}</a> class
</pre>
<p class="note"><strong>Note:</strong> The same code can easily be adapted to work with {@link
android.widget.ListView} as well.</p>
<p>This implementation allows for flexibility in how the images are processed and loaded without
impeding the smoothness of the UI. In the background task you can load images from the network or
resize large digital camera photos and the images appear as the tasks finish processing.</p>
<p>For a full example of this and other concepts discussed in this lesson, please see the included
sample application.</p>