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 @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 @Override 92 public int getCount() { 93 return mSize; 94 } 95 96 @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 @Override 125 public void onCreate(Bundle savedInstanceState) { 126 super.onCreate(savedInstanceState); 127 mImageNum = getArguments() != null ? getArguments().getInt(IMAGE_DATA_EXTRA) : -1; 128 } 129 130 @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 @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 @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<String, Bitmap> mMemoryCache; 193 194 @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 @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.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 @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<BitmapWorkerTask> bitmapWorkerTaskReference; 349 350 public AsyncDrawable(Resources res, Bitmap bitmap, 351 BitmapWorkerTask bitmapWorkerTask) { 352 super(res, bitmap); 353 bitmapWorkerTaskReference = 354 new WeakReference<BitmapWorkerTask>(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