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>
|
||||
</ul>
|
||||
</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">
|
||||
<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