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 
      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">
     27   <a href="{@docRoot}shareables/training/BitmapFun.zip" class="button">Download the sample</a>
     28   <p class="filename">BitmapFun.zip</p>
     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>Heres 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     &#64;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         &#64;Override
     92         public int getCount() {
     93             return mSize;
     94         }
     95 
     96         &#64;Override
     97         public Fragment getItem(int position) {
     98             return ImageDetailFragment.newInstance(position);
     99         }
    100     }
    101 }
    102 </pre>
    103 
    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>
    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     &#64;Override
    125     public void onCreate(Bundle savedInstanceState) {
    126         super.onCreate(savedInstanceState);
    127         mImageNum = getArguments() != null ? getArguments().getInt(IMAGE_DATA_EXTRA) : -1;
    128     }
    129 
    130     &#64;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     &#64;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 
    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, its straightforward to move image loading and processing to a
    152 background thread:</p>
    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     &#64;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     ...
    192     private LruCache&lt;String, Bitmap&gt; mMemoryCache;
    193 
    194     &#64;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
    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>
    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&lt;?&gt; 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&lt;BitmapWorkerTask&gt; bitmapWorkerTaskReference;
    349 
    350         public AsyncDrawable(Resources res, Bitmap bitmap,
    351                 BitmapWorkerTask bitmapWorkerTask) {
    352             super(res, bitmap);
    353             bitmapWorkerTaskReference =
    354                 new WeakReference&lt;BitmapWorkerTask&gt;(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