blob: ed1836ce05d105f699062d1e7f6df6d049f56cae [file] [log] [blame]
Scott Main153f8fe2012-04-04 17:45:24 -07001page.title=Displaying Bitmaps in Your UI
2parent.title=Displaying Bitmaps Efficiently
3parent.link=index.html
4
5trainingnavtop=true
Scott Main153f8fe2012-04-04 17:45:24 -07006
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
Adam Koch9977ddd2012-08-14 14:53:42 -0400104<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>
Scott Main153f8fe2012-04-04 17:45:24 -0700106
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
Adam Koch9977ddd2012-08-14 14:53:42 -0400148<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>
Scott Main153f8fe2012-04-04 17:45:24 -0700153
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 ...
Adam Koch9977ddd2012-08-14 14:53:42 -0400192 private LruCache&lt;String, Bitmap&gt; mMemoryCache;
Scott Main153f8fe2012-04-04 17:45:24 -0700193
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
Adam Koch9977ddd2012-08-14 14:53:42 -0400231android.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>
Scott Main153f8fe2012-04-04 17:45:24 -0700233
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
Adam Koch9977ddd2012-08-14 14:53:42 -0400264 public void onItemClick(AdapterView&lt;?&gt; parent, View v, int position, long id) {
Scott Main153f8fe2012-04-04 17:45:24 -0700265 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
Scott Mainf90f4ed2012-04-20 11:53:32 -0700318href="process-bitmap.html#concurrency">Processing Bitmaps Off the UI Thread</a> lesson. Here is the
319updated
Scott Main153f8fe2012-04-04 17:45:24 -0700320solution:</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 {
Adam Koch9977ddd2012-08-14 14:53:42 -0400348 private final WeakReference&lt;BitmapWorkerTask&gt; bitmapWorkerTaskReference;
Scott Main153f8fe2012-04-04 17:45:24 -0700349
350 public AsyncDrawable(Resources res, Bitmap bitmap,
351 BitmapWorkerTask bitmapWorkerTask) {
352 super(res, bitmap);
353 bitmapWorkerTaskReference =
Adam Koch9977ddd2012-08-14 14:53:42 -0400354 new WeakReference&lt;BitmapWorkerTask&gt;(bitmapWorkerTask);
Scott Main153f8fe2012-04-04 17:45:24 -0700355 }
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>