diff options
Diffstat (limited to 'docs/html/training/displaying-bitmaps/display-bitmap.jd')
-rw-r--r-- | docs/html/training/displaying-bitmaps/display-bitmap.jd | 400 |
1 files changed, 400 insertions, 0 deletions
diff --git a/docs/html/training/displaying-bitmaps/display-bitmap.jd b/docs/html/training/displaying-bitmaps/display-bitmap.jd new file mode 100644 index 0000000..7a93313 --- /dev/null +++ b/docs/html/training/displaying-bitmaps/display-bitmap.jd @@ -0,0 +1,400 @@ +page.title=Displaying Bitmaps in Your UI +parent.title=Displaying Bitmaps Efficiently +parent.link=index.html + +trainingnavtop=true +previous.title=Caching Bitmaps +previous.link=cache-bitmap.html + +@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>The details {@link android.app.Fragment} holds the {@link android.widget.ImageView} children:</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 with this implementation; 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}:</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#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> |