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>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     &#64;Override
    126     public void onCreate(Bundle savedInstanceState) {
    127         super.onCreate(savedInstanceState);
    128         mImageNum = getArguments() != null ? getArguments().getInt(IMAGE_DATA_EXTRA) : -1;
    129     }
    130 
    131     &#64;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     &#64;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     &#64;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     &#64;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     &#64;Override
    248     public void onCreate(Bundle savedInstanceState) {
    249         super.onCreate(savedInstanceState);
    250         mAdapter = new ImageAdapter(getActivity());
    251     }
    252 
    253     &#64;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     &#64;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         &#64;Override
    279         public int getCount() {
    280             return imageResIds.length;
    281         }
    282 
    283         &#64;Override
    284         public Object getItem(int position) {
    285             return imageResIds[position];
    286         }
    287 
    288         &#64;Override
    289         public long getItemId(int position) {
    290             return position;
    291         }
    292 
    293         &#64;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         &#64;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