Scott Main | 153f8fe | 2012-04-04 17:45:24 -0700 | [diff] [blame] | 1 | page.title=Displaying Bitmaps in Your UI |
| 2 | parent.title=Displaying Bitmaps Efficiently |
| 3 | parent.link=index.html |
| 4 | |
| 5 | trainingnavtop=true |
Scott Main | 153f8fe | 2012-04-04 17:45:24 -0700 | [diff] [blame] | 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"> |
Adam Koch | 21e3372 | 2014-02-26 14:00:20 +1100 | [diff] [blame] | 27 | <a href="{@docRoot}downloads/samples/DisplayingBitmaps.zip" class="button">Download the sample</a> |
| 28 | <p class="filename">DisplayingBitmaps.zip</p> |
Scott Main | 153f8fe | 2012-04-04 17:45:24 -0700 | [diff] [blame] | 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 |
| 37 | bitmaps into {@link android.support.v4.view.ViewPager} and {@link android.widget.GridView} |
| 38 | components using a background thread and bitmap cache, while dealing with concurrency and |
| 39 | configuration 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 |
| 44 | way to navigate the detail view of an image gallery. You can implement this pattern using a {@link |
| 45 | android.support.v4.view.ViewPager} component backed by a {@link |
| 46 | android.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 |
| 48 | state of the {@link android.app.Fragment Fragments} in the {@link android.support.v4.view.ViewPager} |
| 49 | as 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 |
| 52 | all fit within the application memory limit, then using a regular {@link |
| 53 | android.support.v4.view.PagerAdapter} or {@link android.support.v4.app.FragmentPagerAdapter} might |
| 54 | be more appropriate.</p> |
| 55 | |
| 56 | <p>Here’s an implementation of a {@link android.support.v4.view.ViewPager} with {@link |
| 57 | android.widget.ImageView} children. The main activity holds the {@link |
| 58 | android.support.v4.view.ViewPager} and the adapter:</p> |
| 59 | |
| 60 | <pre> |
| 61 | public 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 | |
Adam Koch | 9977ddd | 2012-08-14 14:53:42 -0400 | [diff] [blame] | 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 |
| 105 | you see the drawbacks of this implementation? How could it be improved?</p> |
Scott Main | 153f8fe | 2012-04-04 17:45:24 -0700 | [diff] [blame] | 106 | |
| 107 | <pre> |
| 108 | public 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 | |
Adam Koch | 9977ddd | 2012-08-14 14:53:42 -0400 | [diff] [blame] | 148 | <p>Hopefully you noticed the issue: the images are being read from resources on the UI thread, |
| 149 | which 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 |
| 151 | Off the UI Thread</a> lesson, it’s straightforward to move image loading and processing to a |
| 152 | background thread:</p> |
Scott Main | 153f8fe | 2012-04-04 17:45:24 -0700 | [diff] [blame] | 153 | |
| 154 | <pre> |
| 155 | public 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 | |
| 167 | public 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 |
| 183 | in the <a href="process-bitmap.html#BitmapWorkerTask">{@code BitmapWorkerTask}</a> without affecting |
| 184 | responsiveness of the main UI. If the background thread is doing more than just loading an image |
| 185 | directly from disk, it can also be beneficial to add a memory and/or disk cache as described in the |
| 186 | lesson <a href="cache-bitmap.html#memory-cache">Caching Bitmaps</a>. Here's the additional |
| 187 | modifications for a memory cache:</p> |
| 188 | |
| 189 | <pre> |
| 190 | public class ImageDetailActivity extends FragmentActivity { |
| 191 | ... |
Adam Koch | 9977ddd | 2012-08-14 14:53:42 -0400 | [diff] [blame] | 192 | private LruCache<String, Bitmap> mMemoryCache; |
Scott Main | 153f8fe | 2012-04-04 17:45:24 -0700 | [diff] [blame] | 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 |
| 218 | android.support.v4.view.ViewPager} implementation with minimal image loading latency and the ability |
| 219 | to 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 |
| 224 | useful for showing image data sets and can be implemented using a {@link android.widget.GridView} |
| 225 | component in which many images can be on-screen at any one time and many more need to be ready to |
| 226 | appear if the user scrolls up or down. When implementing this type of control, you must ensure the |
| 227 | UI remains fluid, memory usage remains under control and concurrency is handled correctly (due to |
| 228 | the 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 Koch | 9977ddd | 2012-08-14 14:53:42 -0400 | [diff] [blame] | 231 | android.widget.ImageView} children placed inside a {@link android.app.Fragment}. Again, this might |
| 232 | seem like a perfectly reasonable approach, but what would make it better?</p> |
Scott Main | 153f8fe | 2012-04-04 17:45:24 -0700 | [diff] [blame] | 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 |
Adam Koch | 9977ddd | 2012-08-14 14:53:42 -0400 | [diff] [blame] | 264 | public void onItemClick(AdapterView<?> parent, View v, int position, long id) { |
Scott Main | 153f8fe | 2012-04-04 17:45:24 -0700 | [diff] [blame] | 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 |
Scott Main | f90f4ed | 2012-04-20 11:53:32 -0700 | [diff] [blame] | 318 | href="process-bitmap.html#concurrency">Processing Bitmaps Off the UI Thread</a> lesson. Here is the |
| 319 | updated |
Scott Main | 153f8fe | 2012-04-04 17:45:24 -0700 | [diff] [blame] | 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 { |
Adam Koch | 9977ddd | 2012-08-14 14:53:42 -0400 | [diff] [blame] | 348 | private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference; |
Scott Main | 153f8fe | 2012-04-04 17:45:24 -0700 | [diff] [blame] | 349 | |
| 350 | public AsyncDrawable(Resources res, Bitmap bitmap, |
| 351 | BitmapWorkerTask bitmapWorkerTask) { |
| 352 | super(res, bitmap); |
| 353 | bitmapWorkerTaskReference = |
Adam Koch | 9977ddd | 2012-08-14 14:53:42 -0400 | [diff] [blame] | 354 | new WeakReference<BitmapWorkerTask>(bitmapWorkerTask); |
Scott Main | 153f8fe | 2012-04-04 17:45:24 -0700 | [diff] [blame] | 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> |