am 153f8fe4: docs: Android U: Displaying Bitmaps Efficiently
* commit '153f8fe420506a7e1b7a8f6b4d07db798867746e': docs: Android U: Displaying Bitmaps Efficiently
This commit is contained in:
@@ -299,6 +299,31 @@ class="new"> new!</span></span>
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li class="toggle-list">
|
||||||
|
<div><a href="<?cs var:toroot ?>training/displaying-bitmaps/index.html">
|
||||||
|
<span class="en">Displaying Bitmaps Efficiently<span class="new"> new!</span></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
<li><a href="<?cs var:toroot ?>training/displaying-bitmaps/load-bitmap.html">
|
||||||
|
<span class="en">Loading Large Bitmaps Efficiently</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li><a href="<?cs var:toroot ?>training/displaying-bitmaps/process-bitmap.html">
|
||||||
|
<span class="en">Processing Bitmaps Off the UI Thread</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li><a href="<?cs var:toroot ?>training/displaying-bitmaps/cache-bitmap.html">
|
||||||
|
<span class="en">Caching Bitmaps</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li><a href="<?cs var:toroot ?>training/displaying-bitmaps/display-bitmap.html">
|
||||||
|
<span class="en">Displaying Bitmaps in Your UI</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li class="toggle-list">
|
<li class="toggle-list">
|
||||||
<div><a href="<?cs var:toroot ?>training/accessibility/index.html">
|
<div><a href="<?cs var:toroot ?>training/accessibility/index.html">
|
||||||
|
|||||||
BIN
docs/html/shareables/training/BitmapFun.zip
Normal file
BIN
docs/html/shareables/training/BitmapFun.zip
Normal file
Binary file not shown.
337
docs/html/training/displaying-bitmaps/cache-bitmap.jd
Normal file
337
docs/html/training/displaying-bitmaps/cache-bitmap.jd
Normal file
@@ -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
|
||||||
|
|
||||||
|
<div id="tb-wrapper">
|
||||||
|
<div id="tb">
|
||||||
|
|
||||||
|
<h2>This lesson teaches you to</h2>
|
||||||
|
<ol>
|
||||||
|
<li><a href="#memory-cache">Use a Memory Cache</a></li>
|
||||||
|
<li><a href="#disk-cache">Use a Disk Cache</a></li>
|
||||||
|
<li><a href="#config-changes">Handle Configuration Changes</a></li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>You should also read</h2>
|
||||||
|
<ul>
|
||||||
|
<li><a href="{@docRoot}guide/topics/resources/runtime-changes.html">Handling Runtime Changes</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>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.</p>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
|
<h2 id="memory-cache">Use a Memory Cache</h2>
|
||||||
|
|
||||||
|
<p>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 <a
|
||||||
|
href="{@docRoot}reference/android/support/v4/util/LruCache.html">Support Library</a> 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.</p>
|
||||||
|
|
||||||
|
<p class="note"><strong>Note:</strong> 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.</p>
|
||||||
|
|
||||||
|
<p>In order to choose a suitable size for a {@link android.util.LruCache}, a number of factors
|
||||||
|
should be taken into consideration, for example:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>How memory intensive is the rest of your activity and/or application?</li>
|
||||||
|
<li>How many images will be on-screen at once? How many need to be available ready to come
|
||||||
|
on-screen?</li>
|
||||||
|
<li>What is the screen size and density of the device? An extra high density screen (xhdpi) device
|
||||||
|
like <a href="http://www.android.com/devices/detail/galaxy-nexus">Galaxy Nexus</a> will need a
|
||||||
|
larger cache to hold the same number of images in memory compared to a device like <a
|
||||||
|
href="http://www.android.com/devices/detail/nexus-s">Nexus S</a> (hdpi).</li>
|
||||||
|
<li>What dimensions and configuration are the bitmaps and therefore how much memory will each take
|
||||||
|
up?</li>
|
||||||
|
<li>How frequently will the images be accessed? Will some be accessed more frequently than others?
|
||||||
|
If so, perhaps you may want to keep certain items always in memory or even have multiple {@link
|
||||||
|
android.util.LruCache} objects for different groups of bitmaps.</li>
|
||||||
|
<li>Can you balance quality against quantity? Sometimes it can be more useful to store a larger
|
||||||
|
number of lower quality bitmaps, potentially loading a higher quality version in another
|
||||||
|
background task.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
|
<p>Here’s an example of setting up a {@link android.util.LruCache} for bitmaps:</p>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
private LruCache<String, Bitmap> 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<String, Bitmap>(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);
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<p class="note"><strong>Note:</strong> 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.</p>
|
||||||
|
|
||||||
|
<p>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:</p>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<p>The <a href="process-bitmap.html#BitmapWorkerTask">{@code BitmapWorkerTask}</a> also needs to be
|
||||||
|
updated to add entries to the memory cache:</p>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
|
||||||
|
...
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
...
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<h2 id="disk-cache">Use a Disk Cache</h2>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
|
<p class="note"><strong>Note:</strong> 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.</p>
|
||||||
|
|
||||||
|
<p>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 <a
|
||||||
|
href="http://www.google.com/search?q=disklrucache">quick search</a> shows others who have already
|
||||||
|
implemented this solution).</p>
|
||||||
|
|
||||||
|
<p>Here’s updated example code that uses the simple {@code DiskLruCache} included in the sample
|
||||||
|
application of this class:</p>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
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<Integer, Void, Bitmap> {
|
||||||
|
...
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
|
<h2 id="config-changes">Handle Configuration Changes</h2>
|
||||||
|
|
||||||
|
<p>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 <a href="{@docRoot}guide/topics/resources/runtime-changes.html">Handling Runtime Changes</a>).
|
||||||
|
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.</p>
|
||||||
|
|
||||||
|
<p>Luckily, you have a nice memory cache of bitmaps that you built in the <a
|
||||||
|
href="#memory-cache">Use a Memory Cache</a> 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.</p>
|
||||||
|
|
||||||
|
<p>Here’s an example of retaining a {@link android.util.LruCache} object across configuration
|
||||||
|
changes using a {@link android.app.Fragment}:</p>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
private LruCache<String, Bitmap> mMemoryCache;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
...
|
||||||
|
RetainFragment mRetainFragment =
|
||||||
|
RetainFragment.findOrCreateRetainFragment(getFragmentManager());
|
||||||
|
mMemoryCache = RetainFragment.mRetainedCache;
|
||||||
|
if (mMemoryCache == null) {
|
||||||
|
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
|
||||||
|
... // Initialize cache here as usual
|
||||||
|
}
|
||||||
|
mRetainFragment.mRetainedCache = mMemoryCache;
|
||||||
|
}
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
class RetainFragment extends Fragment {
|
||||||
|
private static final String TAG = "RetainFragment";
|
||||||
|
public LruCache<String, Bitmap> 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);
|
||||||
|
<strong>setRetainInstance(true);</strong>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
400
docs/html/training/displaying-bitmaps/display-bitmap.jd
Normal file
400
docs/html/training/displaying-bitmaps/display-bitmap.jd
Normal file
@@ -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>
|
||||||
78
docs/html/training/displaying-bitmaps/index.jd
Normal file
78
docs/html/training/displaying-bitmaps/index.jd
Normal file
@@ -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
|
||||||
|
|
||||||
|
<div id="tb-wrapper">
|
||||||
|
<div id="tb">
|
||||||
|
|
||||||
|
<h2>Dependencies and prerequisites</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Android 2.1 (API Level 7) or higher</li>
|
||||||
|
<li><a href="{@docRoot}sdk/compatibility-library.html">Support Library</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>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:<br />{@code java.lang.OutofMemoryError: bitmap size exceeds VM budget}.</p>
|
||||||
|
|
||||||
|
<p>There are a number of reasons why loading bitmaps in your Android application is tricky:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>Mobile devices typically have constrained system resources. Android devices can have as little
|
||||||
|
as 16MB of memory available to a single application. The <a
|
||||||
|
href="http://source.android.com/compatibility/downloads.html">Android Compatibility Definition
|
||||||
|
Document</a> (CDD), <i>Section 3.7. Virtual Machine Compatibility</i> gives the required minimum
|
||||||
|
application memory for various screen sizes and densities. Applications should be optimized to
|
||||||
|
perform under this minimum memory limit. However, keep in mind many devices are configured with
|
||||||
|
higher limits.</li>
|
||||||
|
<li>Bitmaps take up a lot of memory, especially for rich images like photographs. For example, the
|
||||||
|
camera on the <a href="http://www.google.com/nexus/">Galaxy Nexus</a> takes photos up to 2592x1936
|
||||||
|
pixels (5 megapixels). If the bitmap configuration used is {@link
|
||||||
|
android.graphics.Bitmap.Config ARGB_8888} (the default from the Android 2.3 onward) then loading
|
||||||
|
this image into memory takes about 19MB of memory (2592*1936*4 bytes), immediately exhausting the
|
||||||
|
per-app limit on some devices.</li>
|
||||||
|
<li>Android app UI’s frequently require several bitmaps to be loaded at once. Components such as
|
||||||
|
{@link android.widget.ListView}, {@link android.widget.GridView} and {@link
|
||||||
|
android.support.v4.view.ViewPager} commonly include multiple bitmaps on-screen at once with many
|
||||||
|
more potentially off-screen ready to show at the flick of a finger.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Lessons</h2>
|
||||||
|
|
||||||
|
<dl>
|
||||||
|
<dt><b><a href="load-bitmap.html">Loading Large Bitmaps Efficiently</a></b></dt>
|
||||||
|
<dd>This lesson walks you through decoding large bitmaps without exceeding the per application
|
||||||
|
memory limit.</dd>
|
||||||
|
|
||||||
|
<dt><b><a href="process-bitmap.html">Processing Bitmaps Off the UI Thread</a></b></dt>
|
||||||
|
<dd>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.</dd>
|
||||||
|
|
||||||
|
<dt><b><a href="cache-bitmap.html">Caching Bitmaps</a></b></dt>
|
||||||
|
<dd>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.</dd>
|
||||||
|
|
||||||
|
<dt><b><a href="display-bitmap.html">Displaying Bitmaps in Your UI</a></b></dt>
|
||||||
|
<dd>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.</dd>
|
||||||
|
|
||||||
|
</dl>
|
||||||
165
docs/html/training/displaying-bitmaps/load-bitmap.jd
Normal file
165
docs/html/training/displaying-bitmaps/load-bitmap.jd
Normal file
@@ -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
|
||||||
|
|
||||||
|
<div id="tb-wrapper">
|
||||||
|
<div id="tb">
|
||||||
|
|
||||||
|
<h2>This lesson teaches you to</h2>
|
||||||
|
<ol>
|
||||||
|
<li><a href="#read-bitmap">Read Bitmap Dimensions and Type</a></li>
|
||||||
|
<li><a href="#load-bitmap">Load a Scaled Down Version into Memory</a></li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<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>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.</p>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
|
<p>This lesson walks you through decoding large bitmaps without exceeding the per application
|
||||||
|
memory limit by loading a smaller subsampled version in memory.</p>
|
||||||
|
|
||||||
|
<h2 id="read-bitmap">Read Bitmap Dimensions and Type</h2>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
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;
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
|
<h2 id="load-bitmap">Load a Scaled Down Version into Memory</h2>
|
||||||
|
|
||||||
|
<p>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:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>Estimated memory usage of loading the full image in memory.</li>
|
||||||
|
<li>Amount of memory you are willing to commit to loading this image given any other memory
|
||||||
|
requirements of your application.</li>
|
||||||
|
<li>Dimensions of the target {@link android.widget.ImageView} or UI component that the image
|
||||||
|
is to be loaded into.</li>
|
||||||
|
<li>Screen size and density of the current device.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>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}.</p>
|
||||||
|
|
||||||
|
<p>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:</p>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<p class="note"><strong>Note:</strong> 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.</p>
|
||||||
|
|
||||||
|
<p>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}:</p>
|
||||||
|
|
||||||
|
<a name="decodeSampledBitmapFromResource"></a>
|
||||||
|
<pre>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<p>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:</p>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
mImageView.setImageBitmap(
|
||||||
|
decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
239
docs/html/training/displaying-bitmaps/process-bitmap.jd
Normal file
239
docs/html/training/displaying-bitmaps/process-bitmap.jd
Normal file
@@ -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
|
||||||
|
|
||||||
|
<div id="tb-wrapper">
|
||||||
|
<div id="tb">
|
||||||
|
|
||||||
|
<h2>This lesson teaches you to</h2>
|
||||||
|
<ol>
|
||||||
|
<li><a href="#async-task">Use an AsyncTask</a></li>
|
||||||
|
<li><a href="#concurrency">Handle Concurrency</a></li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>You should also read</h2>
|
||||||
|
<ul>
|
||||||
|
<li><a href="{@docRoot}guide/practices/design/responsiveness.html">Designing for Responsiveness</a></li>
|
||||||
|
<li><a
|
||||||
|
href="http://android-developers.blogspot.com/2010/07/multithreading-for-performance.html">Multithreading
|
||||||
|
for Performance</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>The {@link
|
||||||
|
android.graphics.BitmapFactory#decodeByteArray(byte[],int,int,android.graphics.BitmapFactory.Options)
|
||||||
|
BitmapFactory.decode*} methods, discussed in the <a href="load-bitmap.html">Load Large Bitmaps
|
||||||
|
Efficiently</a> 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 <a
|
||||||
|
href="{@docRoot}guide/practices/design/responsiveness.html">Designing for Responsiveness</a> for
|
||||||
|
more information).</p>
|
||||||
|
|
||||||
|
<p>This lesson walks you through processing bitmaps in a background thread using
|
||||||
|
{@link android.os.AsyncTask} and shows you how to handle concurrency issues.</p>
|
||||||
|
|
||||||
|
<h2 id="async-task">Use an AsyncTask</h2>
|
||||||
|
|
||||||
|
<p>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 <a
|
||||||
|
href="load-bitmap.html#decodeSampledBitmapFromResource">{@code
|
||||||
|
decodeSampledBitmapFromResource()}</a>: </p>
|
||||||
|
|
||||||
|
<a name="BitmapWorkerTask"></a>
|
||||||
|
<pre>
|
||||||
|
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
|
||||||
|
private final WeakReference<ImageView> imageViewReference;
|
||||||
|
private int data = 0;
|
||||||
|
|
||||||
|
public BitmapWorkerTask(ImageView imageView) {
|
||||||
|
// Use a WeakReference to ensure the ImageView can be garbage collected
|
||||||
|
imageViewReference = new WeakReference<ImageView>(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
|
<p>To start loading the bitmap asynchronously, simply create a new task and execute it:</p>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
public void loadBitmap(int resId, ImageView imageView) {
|
||||||
|
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
|
||||||
|
task.execute(resId);
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<h2 id="concurrency">Handle Concurrency</h2>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
|
<p>The blog post <a
|
||||||
|
href="http://android-developers.blogspot.com/2010/07/multithreading-for-performance.html">Multithreading
|
||||||
|
for Performance</a> 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.</p>
|
||||||
|
|
||||||
|
<p>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:</p>
|
||||||
|
|
||||||
|
<a name="AsyncDrawable"></a>
|
||||||
|
<pre>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<p>Before executing the <a href="#BitmapWorkerTask">{@code BitmapWorkerTask}</a>, you create an <a
|
||||||
|
href="#AsyncDrawable">{@code AsyncDrawable}</a> and bind it to the target {@link
|
||||||
|
android.widget.ImageView}:</p>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<p>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}:</p>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<p>A helper method, {@code getBitmapWorkerTask()}, is used above to retrieve the task associated
|
||||||
|
with a particular {@link android.widget.ImageView}:</p>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<p>The last step is updating {@code onPostExecute()} in <a href="#BitmapWorkerTask">{@code
|
||||||
|
BitmapWorkerTask}</a> so that it checks if the task is cancelled and if the current task matches the
|
||||||
|
one associated with the {@link android.widget.ImageView}:</p>
|
||||||
|
|
||||||
|
<a name="BitmapWorkerTaskUpdated"></a>
|
||||||
|
<pre>
|
||||||
|
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
|
||||||
|
...
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Bitmap bitmap) {
|
||||||
|
<strong>if (isCancelled()) {
|
||||||
|
bitmap = null;
|
||||||
|
}</strong>
|
||||||
|
|
||||||
|
if (imageViewReference != null && bitmap != null) {
|
||||||
|
final ImageView imageView = imageViewReference.get();
|
||||||
|
<strong>final BitmapWorkerTask bitmapWorkerTask =
|
||||||
|
getBitmapWorkerTask(imageView);</strong>
|
||||||
|
if (<strong>this == bitmapWorkerTask &&</strong> imageView != null) {
|
||||||
|
imageView.setImageBitmap(bitmap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
Reference in New Issue
Block a user