Home | History | Annotate | Download | only in displaying-bitmaps
      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     &#64;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         &#64;Override
     94         public int getCount() {
     95             return mSize;
     96         }
     97 
     98         &#64;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     &#64;Override
    127     public void onCreate(Bundle savedInstanceState) {
    128         super.onCreate(savedInstanceState);
    129         mImageNum = getArguments() != null ? getArguments().getInt(IMAGE_DATA_EXTRA) : -1;
    130     }
    131 
    132     &#64;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     &#64;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     &#64;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&lt;String, Bitmap&gt; mMemoryCache;
    195 
    196     &#64;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     &#64;Override
    250     public void onCreate(Bundle savedInstanceState) {
    251         super.onCreate(savedInstanceState);
    252         mAdapter = new ImageAdapter(getActivity());
    253     }
    254 
    255     &#64;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     &#64;Override
    266     public void onItemClick(AdapterView&lt;?&gt; 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         &#64;Override
    281         public int getCount() {
    282             return imageResIds.length;
    283         }
    284 
    285         &#64;Override
    286         public Object getItem(int position) {
    287             return imageResIds[position];
    288         }
    289 
    290         &#64;Override
    291         public long getItemId(int position) {
    292             return position;
    293         }
    294 
    295         &#64;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         &#64;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&lt;BitmapWorkerTask&gt; bitmapWorkerTaskReference;
    351 
    352         public AsyncDrawable(Resources res, Bitmap bitmap,
    353                 BitmapWorkerTask bitmapWorkerTask) {
    354             super(res, bitmap);
    355             bitmapWorkerTaskReference =
    356                 new WeakReference&lt;BitmapWorkerTask&gt;(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