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