display-bitmap.jd revision 153f8fe420506a7e1b7a8f6b4d07db798867746e
1page.title=Displaying Bitmaps in Your UI
2parent.title=Displaying Bitmaps Efficiently
3parent.link=index.html
4
5trainingnavtop=true
6previous.title=Caching Bitmaps
7previous.link=cache-bitmap.html
8
9@jd:body
10
11<div id="tb-wrapper">
12<div id="tb">
13
14<h2>This lesson teaches you to</h2>
15<ol>
16  <li><a href="#viewpager">Load Bitmaps into a ViewPager Implementation</a></li>
17  <li><a href="#gridview">Load Bitmaps into a GridView Implementation</a></li>
18</ol>
19
20<h2>You should also read</h2>
21<ul>
22  <li><a href="{@docRoot}design/patterns/swipe-views.html">Android Design: Swipe Views</a></li>
23  <li><a href="{@docRoot}design/building-blocks/grid-lists.html">Android Design: Grid Lists</a></li>
24</ul>
25
26<h2>Try it out</h2>
27
28<div class="download-box">
29  <a href="{@docRoot}shareables/training/BitmapFun.zip" class="button">Download the sample</a>
30  <p class="filename">BitmapFun.zip</p>
31</div>
32
33</div>
34</div>
35
36<p></p>
37
38<p>This lesson brings together everything from previous lessons, showing you how to load multiple
39bitmaps into {@link android.support.v4.view.ViewPager} and {@link android.widget.GridView}
40components using a background thread and bitmap cache, while dealing with concurrency and
41configuration changes.</p>
42
43<h2 id="viewpager">Load Bitmaps into a ViewPager Implementation</h2>
44
45<p>The <a href="{@docRoot}design/patterns/swipe-views.html">swipe view pattern</a> is an excellent
46way to navigate the detail view of an image gallery. You can implement this pattern using a {@link
47android.support.v4.view.ViewPager} component backed by a {@link
48android.support.v4.view.PagerAdapter}. However, a more suitable backing adapter is the subclass
49{@link android.support.v4.app.FragmentStatePagerAdapter} which automatically destroys and saves
50state of the {@link android.app.Fragment Fragments} in the {@link android.support.v4.view.ViewPager}
51as they disappear off-screen, keeping memory usage down.</p>
52
53<p class="note"><strong>Note:</strong> If you have a smaller number of images and are confident they
54all fit within the application memory limit, then using a regular {@link
55android.support.v4.view.PagerAdapter} or {@link android.support.v4.app.FragmentPagerAdapter} might
56be more appropriate.</p>
57
58<p>Here’s an implementation of a {@link android.support.v4.view.ViewPager} with {@link
59android.widget.ImageView} children. The main activity holds the {@link
60android.support.v4.view.ViewPager} and the adapter:</p>
61
62<pre>
63public class ImageDetailActivity extends FragmentActivity {
64    public static final String EXTRA_IMAGE = "extra_image";
65
66    private ImagePagerAdapter mAdapter;
67    private ViewPager mPager;
68
69    // A static dataset to back the ViewPager adapter
70    public final static Integer[] imageResIds = new Integer[] {
71            R.drawable.sample_image_1, R.drawable.sample_image_2, R.drawable.sample_image_3,
72            R.drawable.sample_image_4, R.drawable.sample_image_5, R.drawable.sample_image_6,
73            R.drawable.sample_image_7, R.drawable.sample_image_8, R.drawable.sample_image_9};
74
75    &#64;Override
76    public void onCreate(Bundle savedInstanceState) {
77        super.onCreate(savedInstanceState);
78        setContentView(R.layout.image_detail_pager); // Contains just a ViewPager
79
80        mAdapter = new ImagePagerAdapter(getSupportFragmentManager(), imageResIds.length);
81        mPager = (ViewPager) findViewById(R.id.pager);
82        mPager.setAdapter(mAdapter);
83    }
84
85    public static class ImagePagerAdapter extends FragmentStatePagerAdapter {
86        private final int mSize;
87
88        public ImagePagerAdapter(FragmentManager fm, int size) {
89            super(fm);
90            mSize = size;
91        }
92
93        &#64;Override
94        public int getCount() {
95            return mSize;
96        }
97
98        &#64;Override
99        public Fragment getItem(int position) {
100            return ImageDetailFragment.newInstance(position);
101        }
102    }
103}
104</pre>
105
106<p>The details {@link android.app.Fragment} holds the {@link android.widget.ImageView} children:</p>
107
108<pre>
109public class ImageDetailFragment extends Fragment {
110    private static final String IMAGE_DATA_EXTRA = "resId";
111    private int mImageNum;
112    private ImageView mImageView;
113
114    static ImageDetailFragment newInstance(int imageNum) {
115        final ImageDetailFragment f = new ImageDetailFragment();
116        final Bundle args = new Bundle();
117        args.putInt(IMAGE_DATA_EXTRA, imageNum);
118        f.setArguments(args);
119        return f;
120    }
121
122    // Empty constructor, required as per Fragment docs
123    public ImageDetailFragment() {}
124
125    &#64;Override
126    public void onCreate(Bundle savedInstanceState) {
127        super.onCreate(savedInstanceState);
128        mImageNum = getArguments() != null ? getArguments().getInt(IMAGE_DATA_EXTRA) : -1;
129    }
130
131    &#64;Override
132    public View onCreateView(LayoutInflater inflater, ViewGroup container,
133            Bundle savedInstanceState) {
134        // image_detail_fragment.xml contains just an ImageView
135        final View v = inflater.inflate(R.layout.image_detail_fragment, container, false);
136        mImageView = (ImageView) v.findViewById(R.id.imageView);
137        return v;
138    }
139
140    &#64;Override
141    public void onActivityCreated(Bundle savedInstanceState) {
142        super.onActivityCreated(savedInstanceState);
143        final int resId = ImageDetailActivity.imageResIds[mImageNum];
144        <strong>mImageView.setImageResource(resId);</strong> // Load image into ImageView
145    }
146}
147</pre>
148
149<p>Hopefully you noticed the issue with this implementation; The images are being read from
150resources on the UI thread which can lead to an application hanging and being force closed. Using an
151{@link android.os.AsyncTask} as described in the <a href="process-bitmap.html">Processing Bitmaps Off
152the UI Thread</a> lesson, it’s straightforward to move image loading and processing to a background
153thread:</p>
154
155<pre>
156public class ImageDetailActivity extends FragmentActivity {
157    ...
158
159    public void loadBitmap(int resId, ImageView imageView) {
160        mImageView.setImageResource(R.drawable.image_placeholder);
161        BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
162        task.execute(resId);
163    }
164
165    ... // include <a href="process-bitmap.html#BitmapWorkerTask">{@code BitmapWorkerTask}</a> class
166}
167
168public class ImageDetailFragment extends Fragment {
169    ...
170
171    &#64;Override
172    public void onActivityCreated(Bundle savedInstanceState) {
173        super.onActivityCreated(savedInstanceState);
174        if (ImageDetailActivity.class.isInstance(getActivity())) {
175            final int resId = ImageDetailActivity.imageResIds[mImageNum];
176            // Call out to ImageDetailActivity to load the bitmap in a background thread
177            ((ImageDetailActivity) getActivity()).loadBitmap(resId, mImageView);
178        }
179    }
180}
181</pre>
182
183<p>Any additional processing (such as resizing or fetching images from the network) can take place
184in the <a href="process-bitmap.html#BitmapWorkerTask">{@code BitmapWorkerTask}</a> without affecting
185responsiveness of the main UI. If the background thread is doing more than just loading an image
186directly from disk, it can also be beneficial to add a memory and/or disk cache as described in the
187lesson <a href="cache-bitmap.html#memory-cache">Caching Bitmaps</a>. Here's the additional
188modifications for a memory cache:</p>
189
190<pre>
191public class ImageDetailActivity extends FragmentActivity {
192    ...
193    private LruCache<String, Bitmap> mMemoryCache;
194
195    &#64;Override
196    public void onCreate(Bundle savedInstanceState) {
197        ...
198        // initialize LruCache as per <a href="cache-bitmap.html#memory-cache">Use a Memory Cache</a> section
199    }
200
201    public void loadBitmap(int resId, ImageView imageView) {
202        final String imageKey = String.valueOf(resId);
203
204        final Bitmap bitmap = mMemoryCache.get(imageKey);
205        if (bitmap != null) {
206            mImageView.setImageBitmap(bitmap);
207        } else {
208            mImageView.setImageResource(R.drawable.image_placeholder);
209            BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
210            task.execute(resId);
211        }
212    }
213
214    ... // include updated BitmapWorkerTask from <a href="cache-bitmap.html#memory-cache">Use a Memory Cache</a> section
215}
216</pre>
217
218<p>Putting all these pieces together gives you a responsive {@link
219android.support.v4.view.ViewPager} implementation with minimal image loading latency and the ability
220to do as much or as little background processing on your images as needed.</p>
221
222<h2 id="gridview">Load Bitmaps into a GridView Implementation</h2>
223
224<p>The <a href="{@docRoot}design/building-blocks/grid-lists.html">grid list building block</a> is
225useful for showing image data sets and can be implemented using a {@link android.widget.GridView}
226component in which many images can be on-screen at any one time and many more need to be ready to
227appear if the user scrolls up or down. When implementing this type of control, you must ensure the
228UI remains fluid, memory usage remains under control and concurrency is handled correctly (due to
229the way {@link android.widget.GridView} recycles its children views).</p>
230
231<p>To start with, here is a standard {@link android.widget.GridView} implementation with {@link
232android.widget.ImageView} children placed inside a {@link android.app.Fragment}:</p>
233
234<pre>
235public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
236    private ImageAdapter mAdapter;
237
238    // A static dataset to back the GridView adapter
239    public final static Integer[] imageResIds = new Integer[] {
240            R.drawable.sample_image_1, R.drawable.sample_image_2, R.drawable.sample_image_3,
241            R.drawable.sample_image_4, R.drawable.sample_image_5, R.drawable.sample_image_6,
242            R.drawable.sample_image_7, R.drawable.sample_image_8, R.drawable.sample_image_9};
243
244    // Empty constructor as per Fragment docs
245    public ImageGridFragment() {}
246
247    &#64;Override
248    public void onCreate(Bundle savedInstanceState) {
249        super.onCreate(savedInstanceState);
250        mAdapter = new ImageAdapter(getActivity());
251    }
252
253    &#64;Override
254    public View onCreateView(
255            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
256        final View v = inflater.inflate(R.layout.image_grid_fragment, container, false);
257        final GridView mGridView = (GridView) v.findViewById(R.id.gridView);
258        mGridView.setAdapter(mAdapter);
259        mGridView.setOnItemClickListener(this);
260        return v;
261    }
262
263    &#64;Override
264    public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
265        final Intent i = new Intent(getActivity(), ImageDetailActivity.class);
266        i.putExtra(ImageDetailActivity.EXTRA_IMAGE, position);
267        startActivity(i);
268    }
269
270    private class ImageAdapter extends BaseAdapter {
271        private final Context mContext;
272
273        public ImageAdapter(Context context) {
274            super();
275            mContext = context;
276        }
277
278        &#64;Override
279        public int getCount() {
280            return imageResIds.length;
281        }
282
283        &#64;Override
284        public Object getItem(int position) {
285            return imageResIds[position];
286        }
287
288        &#64;Override
289        public long getItemId(int position) {
290            return position;
291        }
292
293        &#64;Override
294        public View getView(int position, View convertView, ViewGroup container) {
295            ImageView imageView;
296            if (convertView == null) { // if it's not recycled, initialize some attributes
297                imageView = new ImageView(mContext);
298                imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
299                imageView.setLayoutParams(new GridView.LayoutParams(
300                        LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
301            } else {
302                imageView = (ImageView) convertView;
303            }
304            <strong>imageView.setImageResource(imageResIds[position]);</strong> // Load image into ImageView
305            return imageView;
306        }
307    }
308}
309</pre>
310
311<p>Once again, the problem with this implementation is that the image is being set in the UI thread.
312While this may work for small, simple images (due to system resource loading and caching), if any
313additional processing needs to be done, your UI grinds to a halt.</p>
314
315<p>The same asynchronous processing and caching methods from the previous section can be implemented
316here. However, you also need to wary of concurrency issues as the {@link android.widget.GridView}
317recycles its children views. To handle this, use the techniques discussed in the <a
318href="process-bitmap#concurrency">Processing Bitmaps Off the UI Thread</a> lesson. Here is the updated
319solution:</p>
320
321<pre>
322public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
323    ...
324
325    private class ImageAdapter extends BaseAdapter {
326        ...
327
328        &#64;Override
329        public View getView(int position, View convertView, ViewGroup container) {
330            ...
331            <strong>loadBitmap(imageResIds[position], imageView)</strong>
332            return imageView;
333        }
334    }
335
336    public void loadBitmap(int resId, ImageView imageView) {
337        if (cancelPotentialWork(resId, imageView)) {
338            final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
339            final AsyncDrawable asyncDrawable =
340                    new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
341            imageView.setImageDrawable(asyncDrawable);
342            task.execute(resId);
343        }
344    }
345
346    static class AsyncDrawable extends BitmapDrawable {
347        private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
348
349        public AsyncDrawable(Resources res, Bitmap bitmap,
350                BitmapWorkerTask bitmapWorkerTask) {
351            super(res, bitmap);
352            bitmapWorkerTaskReference =
353                new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
354        }
355
356        public BitmapWorkerTask getBitmapWorkerTask() {
357            return bitmapWorkerTaskReference.get();
358        }
359    }
360
361    public static boolean cancelPotentialWork(int data, ImageView imageView) {
362        final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
363
364        if (bitmapWorkerTask != null) {
365            final int bitmapData = bitmapWorkerTask.data;
366            if (bitmapData != data) {
367                // Cancel previous task
368                bitmapWorkerTask.cancel(true);
369            } else {
370                // The same work is already in progress
371                return false;
372            }
373        }
374        // No task associated with the ImageView, or an existing task was cancelled
375        return true;
376    }
377
378    private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
379       if (imageView != null) {
380           final Drawable drawable = imageView.getDrawable();
381           if (drawable instanceof AsyncDrawable) {
382               final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
383               return asyncDrawable.getBitmapWorkerTask();
384           }
385        }
386        return null;
387    }
388
389    ... // include updated <a href="process-bitmap.html#BitmapWorkerTaskUpdated">{@code BitmapWorkerTask}</a> class
390</pre>
391
392<p class="note"><strong>Note:</strong> The same code can easily be adapted to work with {@link
393android.widget.ListView} as well.</p>
394
395<p>This implementation allows for flexibility in how the images are processed and loaded without
396impeding the smoothness of the UI. In the background task you can load images from the network or
397resize large digital camera photos and the images appear as the tasks finish processing.</p>
398
399<p>For a full example of this and other concepts discussed in this lesson, please see the included
400sample application.</p>
401