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