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>The details {@link android.app.Fragment} holds the {@link android.widget.ImageView} children:</p> 107 108 <pre> 109 public 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 150 resources 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 152 the UI Thread</a> lesson, its straightforward to move image loading and processing to a background 153 thread:</p> 154 155 <pre> 156 public 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 168 public 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 184 in the <a href="process-bitmap.html#BitmapWorkerTask">{@code BitmapWorkerTask}</a> without affecting 185 responsiveness of the main UI. If the background thread is doing more than just loading an image 186 directly from disk, it can also be beneficial to add a memory and/or disk cache as described in the 187 lesson <a href="cache-bitmap.html#memory-cache">Caching Bitmaps</a>. Here's the additional 188 modifications for a memory cache:</p> 189 190 <pre> 191 public 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 219 android.support.v4.view.ViewPager} implementation with minimal image loading latency and the ability 220 to 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 225 useful for showing image data sets and can be implemented using a {@link android.widget.GridView} 226 component in which many images can be on-screen at any one time and many more need to be ready to 227 appear if the user scrolls up or down. When implementing this type of control, you must ensure the 228 UI remains fluid, memory usage remains under control and concurrency is handled correctly (due to 229 the 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 232 android.widget.ImageView} children placed inside a {@link android.app.Fragment}:</p> 233 234 <pre> 235 public 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. 312 While this may work for small, simple images (due to system resource loading and caching), if any 313 additional 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 316 here. However, you also need to wary of concurrency issues as the {@link android.widget.GridView} 317 recycles its children views. To handle this, use the techniques discussed in the <a 318 href="process-bitmap.html#concurrency">Processing Bitmaps Off the UI Thread</a> lesson. Here is the 319 updated 320 solution:</p> 321 322 <pre> 323 public 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 394 android.widget.ListView} as well.</p> 395 396 <p>This implementation allows for flexibility in how the images are processed and loaded without 397 impeding the smoothness of the UI. In the background task you can load images from the network or 398 resize 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 401 sample application.</p> 402