| page.title=Displaying Bitmaps in Your UI |
| parent.title=Displaying Bitmaps Efficiently |
| parent.link=index.html |
| |
| trainingnavtop=true |
| |
| @jd:body |
| |
| <div id="tb-wrapper"> |
| <div id="tb"> |
| |
| <h2>This lesson teaches you to</h2> |
| <ol> |
| <li><a href="#viewpager">Load Bitmaps into a ViewPager Implementation</a></li> |
| <li><a href="#gridview">Load Bitmaps into a GridView Implementation</a></li> |
| </ol> |
| |
| <h2>You should also read</h2> |
| <ul> |
| <li><a href="{@docRoot}design/patterns/swipe-views.html">Android Design: Swipe Views</a></li> |
| <li><a href="{@docRoot}design/building-blocks/grid-lists.html">Android Design: Grid Lists</a></li> |
| </ul> |
| |
| <h2>Try it out</h2> |
| |
| <div class="download-box"> |
| <a href="{@docRoot}shareables/training/BitmapFun.zip" class="button">Download the sample</a> |
| <p class="filename">BitmapFun.zip</p> |
| </div> |
| |
| </div> |
| </div> |
| |
| <p></p> |
| |
| <p>This lesson brings together everything from previous lessons, showing you how to load multiple |
| bitmaps into {@link android.support.v4.view.ViewPager} and {@link android.widget.GridView} |
| components using a background thread and bitmap cache, while dealing with concurrency and |
| configuration changes.</p> |
| |
| <h2 id="viewpager">Load Bitmaps into a ViewPager Implementation</h2> |
| |
| <p>The <a href="{@docRoot}design/patterns/swipe-views.html">swipe view pattern</a> is an excellent |
| way to navigate the detail view of an image gallery. You can implement this pattern using a {@link |
| android.support.v4.view.ViewPager} component backed by a {@link |
| android.support.v4.view.PagerAdapter}. However, a more suitable backing adapter is the subclass |
| {@link android.support.v4.app.FragmentStatePagerAdapter} which automatically destroys and saves |
| state of the {@link android.app.Fragment Fragments} in the {@link android.support.v4.view.ViewPager} |
| as they disappear off-screen, keeping memory usage down.</p> |
| |
| <p class="note"><strong>Note:</strong> If you have a smaller number of images and are confident they |
| all fit within the application memory limit, then using a regular {@link |
| android.support.v4.view.PagerAdapter} or {@link android.support.v4.app.FragmentPagerAdapter} might |
| be more appropriate.</p> |
| |
| <p>Here’s an implementation of a {@link android.support.v4.view.ViewPager} with {@link |
| android.widget.ImageView} children. The main activity holds the {@link |
| android.support.v4.view.ViewPager} and the adapter:</p> |
| |
| <pre> |
| public class ImageDetailActivity extends FragmentActivity { |
| public static final String EXTRA_IMAGE = "extra_image"; |
| |
| private ImagePagerAdapter mAdapter; |
| private ViewPager mPager; |
| |
| // A static dataset to back the ViewPager adapter |
| public final static Integer[] imageResIds = new Integer[] { |
| R.drawable.sample_image_1, R.drawable.sample_image_2, R.drawable.sample_image_3, |
| R.drawable.sample_image_4, R.drawable.sample_image_5, R.drawable.sample_image_6, |
| R.drawable.sample_image_7, R.drawable.sample_image_8, R.drawable.sample_image_9}; |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| setContentView(R.layout.image_detail_pager); // Contains just a ViewPager |
| |
| mAdapter = new ImagePagerAdapter(getSupportFragmentManager(), imageResIds.length); |
| mPager = (ViewPager) findViewById(R.id.pager); |
| mPager.setAdapter(mAdapter); |
| } |
| |
| public static class ImagePagerAdapter extends FragmentStatePagerAdapter { |
| private final int mSize; |
| |
| public ImagePagerAdapter(FragmentManager fm, int size) { |
| super(fm); |
| mSize = size; |
| } |
| |
| @Override |
| public int getCount() { |
| return mSize; |
| } |
| |
| @Override |
| public Fragment getItem(int position) { |
| return ImageDetailFragment.newInstance(position); |
| } |
| } |
| } |
| </pre> |
| |
| <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 |
| you see the drawbacks of this implementation? How could it be improved?</p> |
| |
| <pre> |
| public class ImageDetailFragment extends Fragment { |
| private static final String IMAGE_DATA_EXTRA = "resId"; |
| private int mImageNum; |
| private ImageView mImageView; |
| |
| static ImageDetailFragment newInstance(int imageNum) { |
| final ImageDetailFragment f = new ImageDetailFragment(); |
| final Bundle args = new Bundle(); |
| args.putInt(IMAGE_DATA_EXTRA, imageNum); |
| f.setArguments(args); |
| return f; |
| } |
| |
| // Empty constructor, required as per Fragment docs |
| public ImageDetailFragment() {} |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| mImageNum = getArguments() != null ? getArguments().getInt(IMAGE_DATA_EXTRA) : -1; |
| } |
| |
| @Override |
| public View onCreateView(LayoutInflater inflater, ViewGroup container, |
| Bundle savedInstanceState) { |
| // image_detail_fragment.xml contains just an ImageView |
| final View v = inflater.inflate(R.layout.image_detail_fragment, container, false); |
| mImageView = (ImageView) v.findViewById(R.id.imageView); |
| return v; |
| } |
| |
| @Override |
| public void onActivityCreated(Bundle savedInstanceState) { |
| super.onActivityCreated(savedInstanceState); |
| final int resId = ImageDetailActivity.imageResIds[mImageNum]; |
| <strong>mImageView.setImageResource(resId);</strong> // Load image into ImageView |
| } |
| } |
| </pre> |
| |
| <p>Hopefully you noticed the issue: the images are being read from resources on the UI thread, |
| which can lead to an application hanging and being force closed. Using an |
| {@link android.os.AsyncTask} as described in the <a href="process-bitmap.html">Processing Bitmaps |
| Off the UI Thread</a> lesson, it’s straightforward to move image loading and processing to a |
| background thread:</p> |
| |
| <pre> |
| public class ImageDetailActivity extends FragmentActivity { |
| ... |
| |
| public void loadBitmap(int resId, ImageView imageView) { |
| mImageView.setImageResource(R.drawable.image_placeholder); |
| BitmapWorkerTask task = new BitmapWorkerTask(mImageView); |
| task.execute(resId); |
| } |
| |
| ... // include <a href="process-bitmap.html#BitmapWorkerTask">{@code BitmapWorkerTask}</a> class |
| } |
| |
| public class ImageDetailFragment extends Fragment { |
| ... |
| |
| @Override |
| public void onActivityCreated(Bundle savedInstanceState) { |
| super.onActivityCreated(savedInstanceState); |
| if (ImageDetailActivity.class.isInstance(getActivity())) { |
| final int resId = ImageDetailActivity.imageResIds[mImageNum]; |
| // Call out to ImageDetailActivity to load the bitmap in a background thread |
| ((ImageDetailActivity) getActivity()).loadBitmap(resId, mImageView); |
| } |
| } |
| } |
| </pre> |
| |
| <p>Any additional processing (such as resizing or fetching images from the network) can take place |
| in the <a href="process-bitmap.html#BitmapWorkerTask">{@code BitmapWorkerTask}</a> without affecting |
| responsiveness of the main UI. If the background thread is doing more than just loading an image |
| directly from disk, it can also be beneficial to add a memory and/or disk cache as described in the |
| lesson <a href="cache-bitmap.html#memory-cache">Caching Bitmaps</a>. Here's the additional |
| modifications for a memory cache:</p> |
| |
| <pre> |
| public class ImageDetailActivity extends FragmentActivity { |
| ... |
| private LruCache<String, Bitmap> mMemoryCache; |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| ... |
| // initialize LruCache as per <a href="cache-bitmap.html#memory-cache">Use a Memory Cache</a> section |
| } |
| |
| public void loadBitmap(int resId, ImageView imageView) { |
| final String imageKey = String.valueOf(resId); |
| |
| final Bitmap bitmap = mMemoryCache.get(imageKey); |
| if (bitmap != null) { |
| mImageView.setImageBitmap(bitmap); |
| } else { |
| mImageView.setImageResource(R.drawable.image_placeholder); |
| BitmapWorkerTask task = new BitmapWorkerTask(mImageView); |
| task.execute(resId); |
| } |
| } |
| |
| ... // include updated BitmapWorkerTask from <a href="cache-bitmap.html#memory-cache">Use a Memory Cache</a> section |
| } |
| </pre> |
| |
| <p>Putting all these pieces together gives you a responsive {@link |
| android.support.v4.view.ViewPager} implementation with minimal image loading latency and the ability |
| to do as much or as little background processing on your images as needed.</p> |
| |
| <h2 id="gridview">Load Bitmaps into a GridView Implementation</h2> |
| |
| <p>The <a href="{@docRoot}design/building-blocks/grid-lists.html">grid list building block</a> is |
| useful for showing image data sets and can be implemented using a {@link android.widget.GridView} |
| component in which many images can be on-screen at any one time and many more need to be ready to |
| appear if the user scrolls up or down. When implementing this type of control, you must ensure the |
| UI remains fluid, memory usage remains under control and concurrency is handled correctly (due to |
| the way {@link android.widget.GridView} recycles its children views).</p> |
| |
| <p>To start with, here is a standard {@link android.widget.GridView} implementation with {@link |
| android.widget.ImageView} children placed inside a {@link android.app.Fragment}. Again, this might |
| seem like a perfectly reasonable approach, but what would make it better?</p> |
| |
| <pre> |
| public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener { |
| private ImageAdapter mAdapter; |
| |
| // A static dataset to back the GridView adapter |
| public final static Integer[] imageResIds = new Integer[] { |
| R.drawable.sample_image_1, R.drawable.sample_image_2, R.drawable.sample_image_3, |
| R.drawable.sample_image_4, R.drawable.sample_image_5, R.drawable.sample_image_6, |
| R.drawable.sample_image_7, R.drawable.sample_image_8, R.drawable.sample_image_9}; |
| |
| // Empty constructor as per Fragment docs |
| public ImageGridFragment() {} |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| mAdapter = new ImageAdapter(getActivity()); |
| } |
| |
| @Override |
| public View onCreateView( |
| LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { |
| final View v = inflater.inflate(R.layout.image_grid_fragment, container, false); |
| final GridView mGridView = (GridView) v.findViewById(R.id.gridView); |
| mGridView.setAdapter(mAdapter); |
| mGridView.setOnItemClickListener(this); |
| return v; |
| } |
| |
| @Override |
| public void onItemClick(AdapterView<?> parent, View v, int position, long id) { |
| final Intent i = new Intent(getActivity(), ImageDetailActivity.class); |
| i.putExtra(ImageDetailActivity.EXTRA_IMAGE, position); |
| startActivity(i); |
| } |
| |
| private class ImageAdapter extends BaseAdapter { |
| private final Context mContext; |
| |
| public ImageAdapter(Context context) { |
| super(); |
| mContext = context; |
| } |
| |
| @Override |
| public int getCount() { |
| return imageResIds.length; |
| } |
| |
| @Override |
| public Object getItem(int position) { |
| return imageResIds[position]; |
| } |
| |
| @Override |
| public long getItemId(int position) { |
| return position; |
| } |
| |
| @Override |
| public View getView(int position, View convertView, ViewGroup container) { |
| ImageView imageView; |
| if (convertView == null) { // if it's not recycled, initialize some attributes |
| imageView = new ImageView(mContext); |
| imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); |
| imageView.setLayoutParams(new GridView.LayoutParams( |
| LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); |
| } else { |
| imageView = (ImageView) convertView; |
| } |
| <strong>imageView.setImageResource(imageResIds[position]);</strong> // Load image into ImageView |
| return imageView; |
| } |
| } |
| } |
| </pre> |
| |
| <p>Once again, the problem with this implementation is that the image is being set in the UI thread. |
| While this may work for small, simple images (due to system resource loading and caching), if any |
| additional processing needs to be done, your UI grinds to a halt.</p> |
| |
| <p>The same asynchronous processing and caching methods from the previous section can be implemented |
| here. However, you also need to wary of concurrency issues as the {@link android.widget.GridView} |
| recycles its children views. To handle this, use the techniques discussed in the <a |
| href="process-bitmap.html#concurrency">Processing Bitmaps Off the UI Thread</a> lesson. Here is the |
| updated |
| solution:</p> |
| |
| <pre> |
| public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener { |
| ... |
| |
| private class ImageAdapter extends BaseAdapter { |
| ... |
| |
| @Override |
| public View getView(int position, View convertView, ViewGroup container) { |
| ... |
| <strong>loadBitmap(imageResIds[position], imageView)</strong> |
| return imageView; |
| } |
| } |
| |
| public void loadBitmap(int resId, ImageView imageView) { |
| if (cancelPotentialWork(resId, imageView)) { |
| final BitmapWorkerTask task = new BitmapWorkerTask(imageView); |
| final AsyncDrawable asyncDrawable = |
| new AsyncDrawable(getResources(), mPlaceHolderBitmap, task); |
| imageView.setImageDrawable(asyncDrawable); |
| task.execute(resId); |
| } |
| } |
| |
| static class AsyncDrawable extends BitmapDrawable { |
| private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference; |
| |
| public AsyncDrawable(Resources res, Bitmap bitmap, |
| BitmapWorkerTask bitmapWorkerTask) { |
| super(res, bitmap); |
| bitmapWorkerTaskReference = |
| new WeakReference<BitmapWorkerTask>(bitmapWorkerTask); |
| } |
| |
| public BitmapWorkerTask getBitmapWorkerTask() { |
| return bitmapWorkerTaskReference.get(); |
| } |
| } |
| |
| public static boolean cancelPotentialWork(int data, ImageView imageView) { |
| final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); |
| |
| if (bitmapWorkerTask != null) { |
| final int bitmapData = bitmapWorkerTask.data; |
| if (bitmapData != data) { |
| // Cancel previous task |
| bitmapWorkerTask.cancel(true); |
| } else { |
| // The same work is already in progress |
| return false; |
| } |
| } |
| // No task associated with the ImageView, or an existing task was cancelled |
| return true; |
| } |
| |
| private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { |
| if (imageView != null) { |
| final Drawable drawable = imageView.getDrawable(); |
| if (drawable instanceof AsyncDrawable) { |
| final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; |
| return asyncDrawable.getBitmapWorkerTask(); |
| } |
| } |
| return null; |
| } |
| |
| ... // include updated <a href="process-bitmap.html#BitmapWorkerTaskUpdated">{@code BitmapWorkerTask}</a> class |
| </pre> |
| |
| <p class="note"><strong>Note:</strong> The same code can easily be adapted to work with {@link |
| android.widget.ListView} as well.</p> |
| |
| <p>This implementation allows for flexibility in how the images are processed and loaded without |
| impeding the smoothness of the UI. In the background task you can load images from the network or |
| resize large digital camera photos and the images appear as the tasks finish processing.</p> |
| |
| <p>For a full example of this and other concepts discussed in this lesson, please see the included |
| sample application.</p> |