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 @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 @Override 94 public int getCount() { 95 return mSize; 96 } 97 98 @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 @Override 126 public void onCreate(Bundle savedInstanceState) { 127 super.onCreate(savedInstanceState); 128 mImageNum = getArguments() != null ? getArguments().getInt(IMAGE_DATA_EXTRA) : -1; 129 } 130 131 @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 @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 @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 @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 @Override 248 public void onCreate(Bundle savedInstanceState) { 249 super.onCreate(savedInstanceState); 250 mAdapter = new ImageAdapter(getActivity()); 251 } 252 253 @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 @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 @Override 279 public int getCount() { 280 return imageResIds.length; 281 } 282 283 @Override 284 public Object getItem(int position) { 285 return imageResIds[position]; 286 } 287 288 @Override 289 public long getItemId(int position) { 290 return position; 291 } 292 293 @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 @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