diff --git a/docs/html/resources/resources_toc.cs b/docs/html/resources/resources_toc.cs index bbbe6fb119fa3..5297c23c82cdc 100644 --- a/docs/html/resources/resources_toc.cs +++ b/docs/html/resources/resources_toc.cs @@ -299,6 +299,31 @@ class="new"> new! + +
BitmapFun.zip
+Loading a single bitmap into your user interface (UI) is straightforward, however things get more +complicated if you need to load a larger set of images at once. In many cases (such as with +components like {@link android.widget.ListView}, {@link android.widget.GridView} or {@link +android.support.v4.view.ViewPager }), the total number of images on-screen combined with images that +might soon scroll onto the screen are essentially unlimited.
+ +Memory usage is kept down with components like this by recycling the child views as they move +off-screen. The garbage collector also frees up your loaded bitmaps, assuming you don't keep any +long lived references. This is all good and well, but in order to keep a fluid and fast-loading UI +you want to avoid continually processing these images each time they come back on-screen. A memory +and disk cache can often help here, allowing components to quickly reload processed images.
+ +This lesson walks you through using a memory and disk bitmap cache to improve the responsiveness +and fluidity of your UI when loading multiple bitmaps.
+ +A memory cache offers fast access to bitmaps at the cost of taking up valuable application +memory. The {@link android.util.LruCache} class (also available in the Support Library for use back +to API Level 4) is particularly well suited to the task of caching bitmaps, keeping recently +referenced objects in a strong referenced {@link java.util.LinkedHashMap} and evicting the least +recently used member before the cache exceeds its designated size.
+ +Note: In the past, a popular memory cache implementation was a +{@link java.lang.ref.SoftReference} or {@link java.lang.ref.WeakReference} bitmap cache, however +this is not recommended. Starting from Android 2.3 (API Level 9) the garbage collector is more +aggressive with collecting soft/weak references which makes them fairly ineffective. In addition, +prior to Android 3.0 (API Level 11), the backing data of a bitmap was stored in native memory which +is not released in a predictable manner, potentially causing an application to briefly exceed its +memory limits and crash.
+ +In order to choose a suitable size for a {@link android.util.LruCache}, a number of factors +should be taken into consideration, for example:
+ +There is no specific size or formula that suits all applications, it's up to you to analyze your +usage and come up with a suitable solution. A cache that is too small causes additional overhead with +no benefit, a cache that is too large can once again cause {@code java.lang.OutOfMemory} exceptions +and leave the rest of your app little memory to work with.
+ +Here’s an example of setting up a {@link android.util.LruCache} for bitmaps:
+ ++private LruCache+ +mMemoryCache; + +@Override +protected void onCreate(Bundle savedInstanceState) { + ... + // Get memory class of this device, exceeding this amount will throw an + // OutOfMemory exception. + final int memClass = ((ActivityManager) context.getSystemService( + Context.ACTIVITY_SERVICE)).getMemoryClass(); + + // Use 1/8th of the available memory for this memory cache. + final int cacheSize = 1024 * 1024 * memClass / 8; + + mMemoryCache = new LruCache (cacheSize) { + @Override + protected int sizeOf(String key, Bitmap bitmap) { + // The cache size will be measured in bytes rather than number of items. + return bitmap.getByteCount(); + } + }; + ... +} + +public void addBitmapToMemoryCache(String key, Bitmap bitmap) { + if (getBitmapFromMemCache(key) == null) { + mMemoryCache.put(key, bitmap); + } +} + +public Bitmap getBitmapFromMemCache(String key) { + return mMemoryCache.get(key); +} +
Note: In this example, one eighth of the application memory is +allocated for our cache. On a normal/hdpi device this is a minimum of around 4MB (32/8). A full +screen {@link android.widget.GridView} filled with images on a device with 800x480 resolution would +use around 1.5MB (800*480*4 bytes), so this would cache a minimum of around 2.5 pages of images in +memory.
+ +When loading a bitmap into an {@link android.widget.ImageView}, the {@link android.util.LruCache} +is checked first. If an entry is found, it is used immediately to update the {@link +android.widget.ImageView}, otherwise a background thread is spawned to process the image:
+ +
+public void loadBitmap(int resId, ImageView imageView) {
+ final String imageKey = String.valueOf(resId);
+
+ final Bitmap bitmap = getBitmapFromMemCache(imageKey);
+ if (bitmap != null) {
+ mImageView.setImageBitmap(bitmap);
+ } else {
+ mImageView.setImageResource(R.drawable.image_placeholder);
+ BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
+ task.execute(resId);
+ }
+}
+
+
+The {@code BitmapWorkerTask} also needs to be +updated to add entries to the memory cache:
+ ++class BitmapWorkerTask extends AsyncTask+ +{ + ... + // Decode image in background. + @Override + protected Bitmap doInBackground(Integer... params) { + final Bitmap bitmap = decodeSampledBitmapFromResource( + getResources(), params[0], 100, 100)); + addBitmapToMemoryCache(String.valueOf(params[0]), bitmap); + return bitmap; + } + ... +} +
A memory cache is useful in speeding up access to recently viewed bitmaps, however you cannot +rely on images being available in this cache. Components like {@link android.widget.GridView} with +larger datasets can easily fill up a memory cache. Your application could be interrupted by another +task like a phone call, and while in the background it might be killed and the memory cache +destroyed. Once the user resumes, your application it has to process each image again.
+ +A disk cache can be used in these cases to persist processed bitmaps and help decrease loading +times where images are no longer available in a memory cache. Of course, fetching images from disk +is slower than loading from memory and should be done in a background thread, as disk read times can +be unpredictable.
+ +Note: A {@link android.content.ContentProvider} might be a more +appropriate place to store cached images if they are accessed more frequently, for example in an +image gallery application.
+ +Included in the sample code of this class is a basic {@code DiskLruCache} implementation. +However, a more robust and recommended {@code DiskLruCache} solution is included in the Android 4.0 +source code ({@code libcore/luni/src/main/java/libcore/io/DiskLruCache.java}). Back-porting this +class for use on previous Android releases should be fairly straightforward (a quick search shows others who have already +implemented this solution).
+ +Here’s updated example code that uses the simple {@code DiskLruCache} included in the sample +application of this class:
+ +
+private DiskLruCache mDiskCache;
+private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
+private static final String DISK_CACHE_SUBDIR = "thumbnails";
+
+@Override
+protected void onCreate(Bundle savedInstanceState) {
+ ...
+ // Initialize memory cache
+ ...
+ File cacheDir = getCacheDir(this, DISK_CACHE_SUBDIR);
+ mDiskCache = DiskLruCache.openCache(this, cacheDir, DISK_CACHE_SIZE);
+ ...
+}
+
+class BitmapWorkerTask extends AsyncTask {
+ ...
+ // Decode image in background.
+ @Override
+ protected Bitmap doInBackground(Integer... params) {
+ final String imageKey = String.valueOf(params[0]);
+
+ // Check disk cache in background thread
+ Bitmap bitmap = getBitmapFromDiskCache(imageKey);
+
+ if (bitmap == null) { // Not found in disk cache
+ // Process as normal
+ final Bitmap bitmap = decodeSampledBitmapFromResource(
+ getResources(), params[0], 100, 100));
+ }
+
+ // Add final bitmap to caches
+ addBitmapToCache(String.valueOf(imageKey, bitmap);
+
+ return bitmap;
+ }
+ ...
+}
+
+public void addBitmapToCache(String key, Bitmap bitmap) {
+ // Add to memory cache as before
+ if (getBitmapFromMemCache(key) == null) {
+ mMemoryCache.put(key, bitmap);
+ }
+
+ // Also add to disk cache
+ if (!mDiskCache.containsKey(key)) {
+ mDiskCache.put(key, bitmap);
+ }
+}
+
+public Bitmap getBitmapFromDiskCache(String key) {
+ return mDiskCache.get(key);
+}
+
+// Creates a unique subdirectory of the designated app cache directory. Tries to use external
+// but if not mounted, falls back on internal storage.
+public static File getCacheDir(Context context, String uniqueName) {
+ // Check if media is mounted or storage is built-in, if so, try and use external cache dir
+ // otherwise use internal cache dir
+ final String cachePath = Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED
+ || !Environment.isExternalStorageRemovable() ?
+ context.getExternalCacheDir().getPath() : context.getCacheDir().getPath();
+
+ return new File(cachePath + File.separator + uniqueName);
+}
+
+
+While the memory cache is checked in the UI thread, the disk cache is checked in the background +thread. Disk operations should never take place on the UI thread. When image processing is +complete, the final bitmap is added to both the memory and disk cache for future use.
+ +Runtime configuration changes, such as a screen orientation change, cause Android to destroy and +restart the running activity with the new configuration (For more information about this behavior, +see Handling Runtime Changes). +You want to avoid having to process all your images again so the user has a smooth and fast +experience when a configuration change occurs.
+ +Luckily, you have a nice memory cache of bitmaps that you built in the Use a Memory Cache section. This cache can be passed through to the new +activity instance using a {@link android.app.Fragment} which is preserved by calling {@link +android.app.Fragment#setRetainInstance setRetainInstance(true)}). After the activity has been +recreated, this retained {@link android.app.Fragment} is reattached and you gain access to the +existing cache object, allowing images to be quickly fetched and re-populated into the {@link +android.widget.ImageView} objects.
+ +Here’s an example of retaining a {@link android.util.LruCache} object across configuration +changes using a {@link android.app.Fragment}:
+ ++private LruCache+ +mMemoryCache; + +@Override +protected void onCreate(Bundle savedInstanceState) { + ... + RetainFragment mRetainFragment = + RetainFragment.findOrCreateRetainFragment(getFragmentManager()); + mMemoryCache = RetainFragment.mRetainedCache; + if (mMemoryCache == null) { + mMemoryCache = new LruCache (cacheSize) { + ... // Initialize cache here as usual + } + mRetainFragment.mRetainedCache = mMemoryCache; + } + ... +} + +class RetainFragment extends Fragment { + private static final String TAG = "RetainFragment"; + public LruCache mRetainedCache; + + public RetainFragment() {} + + public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) { + RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG); + if (fragment == null) { + fragment = new RetainFragment(); + } + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRetainInstance(true); + } +} +
To test this out, try rotating a device both with and without retaining the {@link +android.app.Fragment}. You should notice little to no lag as the images populate the activity almost +instantly from memory when you retain the cache. Any images not found in the memory cache are +hopefully available in the disk cache, if not, they are processed as usual.
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 0000000000000..7a93313cb9984 --- /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 + +BitmapFun.zip
+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.
+ +The swipe view pattern 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.
+ +Note: 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.
+ +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:
+ +
+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);
+ }
+ }
+}
+
+
+The details {@link android.app.Fragment} holds the {@link android.widget.ImageView} children:
+ +
+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];
+ mImageView.setImageResource(resId); // Load image into ImageView
+ }
+}
+
+
+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 Processing Bitmaps Off +the UI Thread lesson, it’s straightforward to move image loading and processing to a background +thread:
+ +
+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 {@code BitmapWorkerTask} 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);
+ }
+ }
+}
+
+
+Any additional processing (such as resizing or fetching images from the network) can take place +in the {@code BitmapWorkerTask} 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 Caching Bitmaps. Here's the additional +modifications for a memory cache:
+ +
+public class ImageDetailActivity extends FragmentActivity {
+ ...
+ private LruCache mMemoryCache;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ ...
+ // initialize LruCache as per Use a Memory Cache 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 Use a Memory Cache section
+}
+
+
+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.
+ +The grid list building block 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).
+ +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}:
+ +
+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;
+ }
+ imageView.setImageResource(imageResIds[position]); // Load image into ImageView
+ return imageView;
+ }
+ }
+}
+
+
+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.
+ +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 Processing Bitmaps Off the UI Thread lesson. Here is the updated +solution:
+ +
+public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
+ ...
+
+ private class ImageAdapter extends BaseAdapter {
+ ...
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup container) {
+ ...
+ loadBitmap(imageResIds[position], imageView)
+ 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 bitmapWorkerTaskReference;
+
+ public AsyncDrawable(Resources res, Bitmap bitmap,
+ BitmapWorkerTask bitmapWorkerTask) {
+ super(res, bitmap);
+ bitmapWorkerTaskReference =
+ new WeakReference(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 {@code BitmapWorkerTask} class
+
+
+Note: The same code can easily be adapted to work with {@link +android.widget.ListView} as well.
+ +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.
+ +For a full example of this and other concepts discussed in this lesson, please see the included +sample application.
diff --git a/docs/html/training/displaying-bitmaps/index.jd b/docs/html/training/displaying-bitmaps/index.jd new file mode 100644 index 0000000000000..6755c24495aeb --- /dev/null +++ b/docs/html/training/displaying-bitmaps/index.jd @@ -0,0 +1,78 @@ +page.title=Displaying Bitmaps Efficiently + +trainingnavtop=true +startpage=true +next.title=Loading Large Bitmaps Efficiently +next.link=load-bitmap.html + +@jd:body + +BitmapFun.zip
+This class covers some common techniques for processing and loading {@link
+android.graphics.Bitmap} objects in a way that keeps your user interface (UI) components responsive
+and avoids exceeding your application memory limit. If you're not careful, bitmaps can quickly
+consume your available memory budget leading to an application crash due to the dreaded
+exception:
{@code java.lang.OutofMemoryError: bitmap size exceeds VM budget}.
There are a number of reasons why loading bitmaps in your Android application is tricky:
+ +Images come in all shapes and sizes. In many cases they are larger than required for a typical +application user interface (UI). For example, the system Gallery application displays photos taken +using your Android devices's camera which are typically much higher resolution than the screen +density of your device.
+ +Given that you are working with limited memory, ideally you only want to load a lower resolution +version in memory. The lower resolution version should match the size of the UI component that +displays it. An image with a higher resolution does not provide any visible benefit, but still takes +up precious memory and incurs additional performance overhead due to additional on the fly +scaling.
+ +This lesson walks you through decoding large bitmaps without exceeding the per application +memory limit by loading a smaller subsampled version in memory.
+ +The {@link android.graphics.BitmapFactory} class provides several decoding methods ({@link +android.graphics.BitmapFactory#decodeByteArray(byte[],int,int,android.graphics.BitmapFactory.Options) +decodeByteArray()}, {@link +android.graphics.BitmapFactory#decodeFile(java.lang.String,android.graphics.BitmapFactory.Options) +decodeFile()}, {@link +android.graphics.BitmapFactory#decodeResource(android.content.res.Resources,int,android.graphics.BitmapFactory.Options) +decodeResource()}, etc.) for creating a {@link android.graphics.Bitmap} from various sources. Choose +the most appropriate decode method based on your image data source. These methods attempt to +allocate memory for the constructed bitmap and therefore can easily result in an {@code OutOfMemory} +exception. Each type of decode method has additional signatures that let you specify decoding +options via the {@link android.graphics.BitmapFactory.Options} class. Setting the {@link +android.graphics.BitmapFactory.Options#inJustDecodeBounds} property to {@code true} while decoding +avoids memory allocation, returning {@code null} for the bitmap object but setting {@link +android.graphics.BitmapFactory.Options#outWidth}, {@link +android.graphics.BitmapFactory.Options#outHeight} and {@link +android.graphics.BitmapFactory.Options#outMimeType}. This technique allows you to read the +dimensions and type of the image data prior to construction (and memory allocation) of the +bitmap.
+ ++BitmapFactory.Options options = new BitmapFactory.Options(); +options.inJustDecodeBounds = true; +BitmapFactory.decodeResource(getResources(), R.id.myimage, options); +int imageHeight = options.outHeight; +int imageWidth = options.outWidth; +String imageType = options.outMimeType; ++ +
To avoid {@code java.lang.OutOfMemory} exceptions, check the dimensions of a bitmap before +decoding it, unless you absolutely trust the source to provide you with predictably sized image data +that comfortably fits within the available memory.
+ +Now that the image dimensions are known, they can be used to decide if the full image should be +loaded into memory or if a subsampled version should be loaded instead. Here are some factors to +consider:
+ +For example, it’s not worth loading a 1024x768 pixel image into memory if it will eventually be +displayed in a 128x96 pixel thumbnail in an {@link android.widget.ImageView}.
+ +To tell the decoder to subsample the image, loading a smaller version into memory, set {@link +android.graphics.BitmapFactory.Options#inSampleSize} to {@code true} in your {@link +android.graphics.BitmapFactory.Options} object. For example, an image with resolution 2048x1536 that +is decoded with an {@link android.graphics.BitmapFactory.Options#inSampleSize} of 4 produces a +bitmap of approximately 512x384. Loading this into memory uses 0.75MB rather than 12MB for the full +image (assuming a bitmap configuration of {@link android.graphics.Bitmap.Config ARGB_8888}). Here’s +a method to calculate a the sample size value based on a target width and height:
+ +
+public static int calculateInSampleSize(
+ BitmapFactory.Options options, int reqWidth, int reqHeight) {
+ // Raw height and width of image
+ final int height = options.outHeight;
+ final int width = options.outWidth;
+ int inSampleSize = 1;
+
+ if (height > reqHeight || width > reqWidth) {
+ if (width > height) {
+ inSampleSize = Math.round((float)height / (float)reqHeight);
+ } else {
+ inSampleSize = Math.round((float)width / (float)reqWidth);
+ }
+ }
+ return inSampleSize;
+}
+
+
+Note: Using powers of 2 for {@link +android.graphics.BitmapFactory.Options#inSampleSize} values is faster and more efficient for the +decoder. However, if you plan to cache the resized versions in memory or on disk, it’s usually still +worth decoding to the most appropriate image dimensions to save space.
+ +To use this method, first decode with {@link +android.graphics.BitmapFactory.Options#inJustDecodeBounds} set to {@code true}, pass the options +through and then decode again using the new {@link +android.graphics.BitmapFactory.Options#inSampleSize} value and {@link +android.graphics.BitmapFactory.Options#inJustDecodeBounds} set to {@code false}:
+ + +
+public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
+ int reqWidth, int reqHeight) {
+
+ // First decode with inJustDecodeBounds=true to check dimensions
+ final BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeResource(res, resId, options);
+
+ // Calculate inSampleSize
+ options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
+
+ // Decode bitmap with inSampleSize set
+ options.inJustDecodeBounds = false;
+ return BitmapFactory.decodeResource(res, resId, options);
+}
+
+
+This method makes it easy to load a bitmap of arbitrarily large size into an {@link +android.widget.ImageView} that displays a 100x100 pixel thumbnail, as shown in the following example +code:
+ ++mImageView.setImageBitmap( + decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100)); ++ +
You can follow a similar process to decode bitmaps from other sources, by substituting the +appropriate {@link +android.graphics.BitmapFactory#decodeByteArray(byte[],int,int,android.graphics.BitmapFactory.Options) +BitmapFactory.decode*} method as needed.
\ No newline at end of file diff --git a/docs/html/training/displaying-bitmaps/process-bitmap.jd b/docs/html/training/displaying-bitmaps/process-bitmap.jd new file mode 100644 index 0000000000000..c1450b4a8850d --- /dev/null +++ b/docs/html/training/displaying-bitmaps/process-bitmap.jd @@ -0,0 +1,239 @@ +page.title=Processing Bitmaps Off the UI Thread +parent.title=Displaying Bitmaps Efficiently +parent.link=index.html + +trainingnavtop=true +next.title=Caching Bitmaps +next.link=cache-bitmap.html +previous.title=Loading Large Bitmaps Efficiently +previous.link=load-bitmap.html + +@jd:body + +BitmapFun.zip
+The {@link +android.graphics.BitmapFactory#decodeByteArray(byte[],int,int,android.graphics.BitmapFactory.Options) +BitmapFactory.decode*} methods, discussed in the Load Large Bitmaps +Efficiently lesson, should not be executed on the main UI thread if the source data is read from +disk or a network location (or really any source other than memory). The time this data takes to +load is unpredictable and depends on a variety of factors (speed of reading from disk or network, +size of image, power of CPU, etc.). If one of these tasks blocks the UI thread, the system flags +your application as non-responsive and the user has the option of closing it (see Designing for Responsiveness for +more information).
+ +This lesson walks you through processing bitmaps in a background thread using +{@link android.os.AsyncTask} and shows you how to handle concurrency issues.
+ +The {@link android.os.AsyncTask} class provides an easy way to execute some work in a background +thread and publish the results back on the UI thread. To use it, create a subclass and override the +provided methods. Here’s an example of loading a large image into an {@link +android.widget.ImageView} using {@link android.os.AsyncTask} and {@code +decodeSampledBitmapFromResource()}:
+ + ++class BitmapWorkerTask extends AsyncTask+ +{ + private final WeakReference imageViewReference; + private int data = 0; + + public BitmapWorkerTask(ImageView imageView) { + // Use a WeakReference to ensure the ImageView can be garbage collected + imageViewReference = new WeakReference (imageView); + } + + // Decode image in background. + @Override + protected Bitmap doInBackground(Integer... params) { + data = params[0]; + return decodeSampledBitmapFromResource(getResources(), data, 100, 100)); + } + + // Once complete, see if ImageView is still around and set bitmap. + @Override + protected void onPostExecute(Bitmap bitmap) { + if (imageViewReference != null && bitmap != null) { + final ImageView imageView = imageViewReference.get(); + if (imageView != null) { + imageView.setImageBitmap(bitmap); + } + } + } +} +
The {@link java.lang.ref.WeakReference} to the {@link android.widget.ImageView} ensures that the +{@link android.os.AsyncTask} does not prevent the {@link android.widget.ImageView} and anything it +references from being garbage collected. There’s no guarantee the {@link android.widget.ImageView} +is still around when the task finishes, so you must also check the reference in {@link +android.os.AsyncTask#onPostExecute(Result) onPostExecute()}. The {@link android.widget.ImageView} +may no longer exist, if for example, the user navigates away from the activity or if a +configuration change happens before the task finishes.
+ +To start loading the bitmap asynchronously, simply create a new task and execute it:
+ +
+public void loadBitmap(int resId, ImageView imageView) {
+ BitmapWorkerTask task = new BitmapWorkerTask(imageView);
+ task.execute(resId);
+}
+
+
+Common view components such as {@link android.widget.ListView} and {@link +android.widget.GridView} introduce another issue when used in conjunction with the {@link +android.os.AsyncTask} as demonstrated in the previous section. In order to be efficient with memory, +these components recycle child views as the user scrolls. If each child view triggers an {@link +android.os.AsyncTask}, there is no guarantee that when it completes, the associated view has not +already been recycled for use in another child view. Furthermore, there is no guarantee that the +order in which asynchronous tasks are started is the order that they complete.
+ +The blog post Multithreading +for Performance further discusses dealing with concurrency, and offers a solution where the +{@link android.widget.ImageView} stores a reference to the most recent {@link android.os.AsyncTask} +which can later be checked when the task completes. Using a similar method, the {@link +android.os.AsyncTask} from the previous section can be extended to follow a similar pattern.
+ +Create a dedicated {@link android.graphics.drawable.Drawable} subclass to store a reference +back to the worker task. In this case, a {@link android.graphics.drawable.BitmapDrawable} is used so +that a placeholder image can be displayed in the {@link android.widget.ImageView} while the task +completes:
+ + +
+static class AsyncDrawable extends BitmapDrawable {
+ private final WeakReference bitmapWorkerTaskReference;
+
+ public AsyncDrawable(Resources res, Bitmap bitmap,
+ BitmapWorkerTask bitmapWorkerTask) {
+ super(res, bitmap);
+ bitmapWorkerTaskReference =
+ new WeakReference(bitmapWorkerTask);
+ }
+
+ public BitmapWorkerTask getBitmapWorkerTask() {
+ return bitmapWorkerTaskReference.get();
+ }
+}
+
+
+Before executing the {@code BitmapWorkerTask}, you create an {@code AsyncDrawable} and bind it to the target {@link +android.widget.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);
+ }
+}
+
+
+The {@code cancelPotentialWork} method referenced in the code sample above checks if another +running task is already associated with the {@link android.widget.ImageView}. If so, it attempts to +cancel the previous task by calling {@link android.os.AsyncTask#cancel cancel()}. In a small number +of cases, the new task data matches the existing task and nothing further needs to happen. Here is +the implementation of {@code cancelPotentialWork}:
+ +
+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;
+}
+
+
+A helper method, {@code getBitmapWorkerTask()}, is used above to retrieve the task associated +with a particular {@link android.widget.ImageView}:
+ +
+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;
+}
+
+
+The last step is updating {@code onPostExecute()} in {@code +BitmapWorkerTask} so that it checks if the task is cancelled and if the current task matches the +one associated with the {@link android.widget.ImageView}:
+ + ++class BitmapWorkerTask extends AsyncTask+ +{ + ... + + @Override + protected void onPostExecute(Bitmap bitmap) { + if (isCancelled()) { + bitmap = null; + } + + if (imageViewReference != null && bitmap != null) { + final ImageView imageView = imageViewReference.get(); + final BitmapWorkerTask bitmapWorkerTask = + getBitmapWorkerTask(imageView); + if (this == bitmapWorkerTask && imageView != null) { + imageView.setImageBitmap(bitmap); + } + } + } +} +
This implementation is now suitable for use in {@link android.widget.ListView} and {@link +android.widget.GridView} components as well as any other components that recycle their child +views. Simply call {@code loadBitmap} where you normally set an image to your {@link +android.widget.ImageView}. For example, in a {@link android.widget.GridView} implementation this +would be in the {@link android.widget.Adapter#getView getView()} method of the backing adapter.
\ No newline at end of file