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! + +
  • +
    + Displaying Bitmaps Efficiently new! + +
    + +
  • diff --git a/docs/html/shareables/training/BitmapFun.zip b/docs/html/shareables/training/BitmapFun.zip new file mode 100644 index 0000000000000..e7e71f9f85a09 Binary files /dev/null and b/docs/html/shareables/training/BitmapFun.zip differ diff --git a/docs/html/training/displaying-bitmaps/cache-bitmap.jd b/docs/html/training/displaying-bitmaps/cache-bitmap.jd new file mode 100644 index 0000000000000..94abe2186561b --- /dev/null +++ b/docs/html/training/displaying-bitmaps/cache-bitmap.jd @@ -0,0 +1,337 @@ +page.title=Caching Bitmaps +parent.title=Displaying Bitmaps Efficiently +parent.link=index.html + +trainingnavtop=true +next.title=Displaying Bitmaps in Your UI +next.link=display-bitmap.html +previous.title=Processing Bitmaps Off the UI Thread +previous.link=process-bitmap.html + +@jd:body + + + +

    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.

    + +

    Use a Memory Cache

    + +

    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;
    +    }
    +    ...
    +}
    +
    + +

    Use a Disk Cache

    + +

    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.

    + +

    Handle Configuration Changes

    + +

    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 + +
    +
    + +

    This lesson teaches you to

    +
      +
    1. Load Bitmaps into a ViewPager Implementation
    2. +
    3. Load Bitmaps into a GridView Implementation
    4. +
    + +

    You should also read

    + + +

    Try it out

    + +
    + Download the sample +

    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.

    + +

    Load Bitmaps into a ViewPager Implementation

    + +

    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.

    + +

    Load Bitmaps into a GridView Implementation

    + +

    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 + +
    +
    + +

    Dependencies and prerequisites

    + + +

    Try it out

    + +
    + Download the sample +

    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:

    + + + +

    Lessons

    + +
    +
    Loading Large Bitmaps Efficiently
    +
    This lesson walks you through decoding large bitmaps without exceeding the per application + memory limit.
    + +
    Processing Bitmaps Off the UI Thread
    +
    Bitmap processing (resizing, downloading from a remote source, etc.) should never take place + on the main UI thread. This lesson walks you through processing bitmaps in a background thread + using {@link android.os.AsyncTask} and explains how to handle concurrency issues.
    + +
    Caching Bitmaps
    +
    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.
    + +
    Displaying Bitmaps in Your UI
    +
    This lesson brings everything together, showing you how to load multiple bitmaps into + components like {@link android.support.v4.view.ViewPager} and {@link android.widget.GridView} + using a background thread and bitmap cache.
    + +
    \ No newline at end of file diff --git a/docs/html/training/displaying-bitmaps/load-bitmap.jd b/docs/html/training/displaying-bitmaps/load-bitmap.jd new file mode 100644 index 0000000000000..c0a5709d41ccb --- /dev/null +++ b/docs/html/training/displaying-bitmaps/load-bitmap.jd @@ -0,0 +1,165 @@ +page.title=Loading Large Bitmaps Efficiently +parent.title=Displaying Bitmaps Efficiently +parent.link=index.html + +trainingnavtop=true +next.title=Processing Bitmaps Off the UI Thread +next.link=process-bitmap.html + +@jd:body + +
    +
    + +

    This lesson teaches you to

    +
      +
    1. Read Bitmap Dimensions and Type
    2. +
    3. Load a Scaled Down Version into Memory
    4. +
    + +

    Try it out

    + +
    + Download the sample +

    BitmapFun.zip

    +
    + +
    +
    + +

    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.

    + +

    Read Bitmap Dimensions and Type

    + +

    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.

    + +

    Load a Scaled Down Version into 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 + +
    +
    + +

    This lesson teaches you to

    +
      +
    1. Use an AsyncTask
    2. +
    3. Handle Concurrency
    4. +
    + +

    You should also read

    + + +

    Try it out

    + +
    + Download the sample +

    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.

    + +

    Use an AsyncTask

    + +

    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);
    +}
    +
    + +

    Handle Concurrency

    + +

    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