Android-Displaying Bitmaps Efficiently
2015-12-30 20:45
651 查看
Learn how to use common techniques to process and load
in a way that keeps your user interface (UI) components responsive and avoids exceeding your application memory limit.
There are a number of reasons why loading bitmaps in your Android application is tricky:
Mobile devices typically have constrained system resources. Android devices can have as little as 16MB of memory available to a single application. The Android
Compatibility Definition Document (CDD), Section 3.7. Virtual Machine Compatibility 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.
Bitmaps take up a lot of memory, especially for rich images like photographs. For example, the camera on theGalaxy
Nexus takes photos up to 2592x1936 pixels (5 megapixels). If the bitmap configuration used is
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.
Android app UI’s frequently require several bitmaps to be loaded at once. Components such as
include multiple bitmaps on-screen at once with many more potentially off-screen ready to show at the flick of a finger.
Images come in all shapes and sizes. In many cases they are larger than required for a typical application user interface (UI).
The
provides several decoding methods (
etc.) for creating a
various sources.
》Here are some factors to consider:
Estimated memory usage of loading the full image in memory.
Amount of memory you are willing to commit to loading this image given any other memory requirements of your application.
Dimensions of the target
the image is to be loaded into.
Screen size and density of the current device.
》 Here’s a method to calculate a sample size value that is a power of two based on a target width and height:
Note: A power of two value is calculated because the decoder uses a final value by rounding down to the nearest power of two, as per the
To use this method, first decode with
to
and
This method makes it easy to load a bitmap of arbitrarily large size into an
displays a 100x100 pixel thumbnail, as shown in the following example code:
》The
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
Create a dedicated
task. In this case, a
can be displayed in the
Before executing the
bind it to the target
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
he
(also available in the Support
Library for use back to API Level 4) is particularly well suited to the task of caching bitmaps, keeping recently referenced objects in a strong referenced
evicting the least recently used member before the cache exceeds its designated size.
》 Note: In
the past, a popular memory cache implementation was a
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.
Here’s an example of setting up a
Note: A
store cached images if they are accessed more frequently, for example in an image gallery application.
The sample code of this class uses a
source. Here’s updated example code that adds a disk cache in addition to the existing memory cache:
》 While the memory cache is checked in the UI thread, the disk cache is checked in the background thread. Disk operations should never take place on the UI thread. When image processing
is complete, the final bitmap is added to both the memory and disk cache for future use.
Runtime configuration changes, such as a screen orientation change, cause Android to destroy and restart
the running activity with the new configuration (For more information about this behavior, see Handling
Runtime Changes). You want to avoid having to process all your images again so the user has a smooth and fast experience when a configuration change occurs.
》Here’s an example of retaining a
across configuration changes using a
To set the stage for this lesson, here is how Android's management of bitmap memory has evolved:
On Android Android 2.2 (API level 8) and lower, when garbage collection occurs, your app's threads get stopped. This causes a lag that can degrade performance. Android 2.3 adds concurrent garbage collection, which means
that the memory is reclaimed soon after a bitmap is no longer referenced.
On Android 2.3.3 (API level 10) and lower, the backing pixel data for a bitmap is stored in native memory. It is separate from the bitmap itself, which is stored in the Dalvik heap. The pixel data in native memory is not released
in a predictable manner, potentially causing an application to briefly exceed its memory limits and crash. As of Android 3.0 (API level 11), the pixel data is stored on the Dalvik heap along with the associated bitmap.
The code recycles the bitmap when these conditions are met:
The reference count for both
The bitmap is not
》 Android 3.0 (API level 11) introduces the
If this option is set, decode methods that take the
will attempt to reuse an existing bitmap when loading content. This means that the bitmap's memory is reused, resulting in improved performance, and removing both memory allocation and de-allocation. However, there are certain restrictions with how
be used. In particular, before Android 4.4 (API level 19), only equal sized bitmaps are supported. For details, please see the
The following snippet demonstrates how an existing bitmap is stored for possible later use in the sample app. When an app is running on Android 3.0 or higher and a bitmap is evicted from the
a soft reference to the bitmap is placed in a
In the running app, decoder methods check to see if there is an existing bitmap they can use. For example:
The next snippet shows the
the above snippet. It looks for an existing bitmap to set as the value for
Note that this method only sets a value for
it finds a suitable match (your code should never assume that a match will be found):
Finally, this method determines whether a candidate bitmap satisfies the size criteria to be used for
》 Here’s an implementation of a
The main activity holds the
the adapter:
》To start with, here is a standard
with
placed inside a
Again, this might seem like a perfectly reasonable approach, but what would make it better?
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
its children views. To handle this, use the techniques discussed in the Processing Bitmaps
Off the UI Thread lesson. Here is the updated solution:
Note: The same code can easily be adapted to work with
Bitmapobjects
in a way that keeps your user interface (UI) components responsive and avoids exceeding your application memory limit.
There are a number of reasons why loading bitmaps in your Android application is tricky:
Mobile devices typically have constrained system resources. Android devices can have as little as 16MB of memory available to a single application. The Android
Compatibility Definition Document (CDD), Section 3.7. Virtual Machine Compatibility 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.
Bitmaps take up a lot of memory, especially for rich images like photographs. For example, the camera on theGalaxy
Nexus takes photos up to 2592x1936 pixels (5 megapixels). If the bitmap configuration used is
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.
Android app UI’s frequently require several bitmaps to be loaded at once. Components such as
ListView,
GridViewand
ViewPagercommonly
include multiple bitmaps on-screen at once with many more potentially off-screen ready to show at the flick of a finger.
Images come in all shapes and sizes. In many cases they are larger than required for a typical application user interface (UI).
The
BitmapFactoryclass
provides several decoding methods (
, int, int, android.graphics.BitmapFactory.Options)]decodeByteArray(),
decodeFile(),
decodeResource(),
etc.) for creating a
Bitmapfrom
various sources.
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;
》Here are some factors to consider:
Estimated memory usage of loading the full image in memory.
Amount of memory you are willing to commit to loading this image given any other memory requirements of your application.
Dimensions of the target
ImageViewor UI component that
the image is to be loaded into.
Screen size and density of the current device.
》 Here’s a method to calculate a sample size value that is a power of two based on a target width and height:
public static int calculateInSampleSize( BitmapFactory.Options options, int reqWidth, int reqHeight) { // Raw height and width of image final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { final int halfHeight = height / 2; final int halfWidth = width / 2; // Calculate the largest inSampleSize value that is a power of 2 and keeps both // height and width larger than the requested height and width. while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) { inSampleSize *= 2; } } return inSampleSize; }
Note: A power of two value is calculated because the decoder uses a final value by rounding down to the nearest power of two, as per the
inSampleSizedocumentation.
To use this method, first decode with
inJustDecodeBoundsset
to
true, pass the options through and then decode again using the new
inSampleSizevalue
and
inJustDecodeBoundsset to
false:
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) { // First decode with inJustDecodeBounds=true to check dimensions final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(res, resId, options); // Calculate inSampleSize options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // Decode bitmap with inSampleSize set options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res, resId, options); }
This method makes it easy to load a bitmap of arbitrarily large size into an
ImageViewthat
displays a 100x100 pixel thumbnail, as shown in the following example code:
mImageView.setImageBitmap( decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
》The
AsyncTaskclass
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
ImageViewusing
AsyncTaskand
decodeSampledBitmapFromResource():
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); } } } }
public void loadBitmap(int resId, ImageView imageView) { BitmapWorkerTask task = new BitmapWorkerTask(imageView); task.execute(resId); }
Create a dedicated
Drawablesubclass to store a reference back to the worker
task. In this case, a
BitmapDrawableis used so that a placeholder image
can be displayed in the
ImageViewwhile the task completes:
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(); } }
Before executing the
BitmapWorkerTask, you create an
AsyncDrawableand
bind it to the target
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); } }
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
cancelPotentialWork:
public static boolean cancelPotentialWork(int data, ImageView imageView) { final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); if (bitmapWorkerTask != null) { final int bitmapData = bitmapWorkerTask.data; // If bitmapData is not yet set or it differs from the new data if (bitmapData == 0 || 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; }
he
LruCacheclass
(also available in the Support
Library for use back to API Level 4) is particularly well suited to the task of caching bitmaps, keeping recently referenced objects in a strong referenced
LinkedHashMapand
evicting the least recently used member before the cache exceeds its designated size.
》 Note: In
the past, a popular memory cache implementation was a
SoftReferenceor
WeakReferencebitmap
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.
Here’s an example of setting up a
LruCachefor bitmaps:
private LruCache<String, Bitmap> mMemoryCache; @Override protected void onCreate(Bundle savedInstanceState) { ... // Get max available VM memory, exceeding this amount will throw an // OutOfMemory exception. Stored in kilobytes as LruCache takes an // int in its constructor. final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); // Use 1/8th of the available memory for this memory cache. final int cacheSize = maxMemory / 8; mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { // The cache size will be measured in kilobytes rather than // number of items. return bitmap.getByteCount() / 1024; } }; ... } public void addBitmapToMemoryCache(String key, Bitmap bitmap) { if (getBitmapFromMemCache(key) == null) { mMemoryCache.put(key, bitmap); } } public Bitmap getBitmapFromMemCache(String key) { return mMemoryCache.get(key); }
Note: A
ContentProvidermight be a more appropriate place to
store cached images if they are accessed more frequently, for example in an image gallery application.
The sample code of this class uses a
DiskLruCacheimplementation that is pulled from the Android
source. Here’s updated example code that adds a disk cache in addition to the existing memory cache:
private DiskLruCache mDiskLruCache; private final Object mDiskCacheLock = new Object(); private boolean mDiskCacheStarting = true; 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 ... // Initialize disk cache on background thread File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR); new InitDiskCacheTask().execute(cacheDir); ... } class InitDiskCacheTask extends AsyncTask<File, Void, Void> { @Override protected Void doInBackground(File... params) { synchronized (mDiskCacheLock) { File cacheDir = params[0]; mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE); mDiskCacheStarting = false; // Finished initialization mDiskCacheLock.notifyAll(); // Wake any waiting threads } return null; } } 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(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 synchronized (mDiskCacheLock) { if (mDiskLruCache != null && mDiskLruCache.get(key) == null) { mDiskLruCache.put(key, bitmap); } } } public Bitmap getBitmapFromDiskCache(String key) { synchronized (mDiskCacheLock) { // Wait while disk cache is started from background thread while (mDiskCacheStarting) { try { mDiskCacheLock.wait(); } catch (InterruptedException e) {} } if (mDiskLruCache != null) { return mDiskLruCache.get(key); } } return null; } // 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 getDiskCacheDir(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.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() : context.getCacheDir().getPath(); return new File(cachePath + File.separator + uniqueName); }
》 While the memory cache is checked in the UI thread, the disk cache is checked in the background thread. Disk operations should never take place on the UI thread. When image processing
is complete, the final bitmap is added to both the memory and disk cache for future use.
Runtime configuration changes, such as a screen orientation change, cause Android to destroy and restart
the running activity with the new configuration (For more information about this behavior, see Handling
Runtime Changes). You want to avoid having to process all your images again so the user has a smooth and fast experience when a configuration change occurs.
》Here’s an example of retaining a
LruCacheobject
across configuration changes using a
Fragment:
private LruCache<String, Bitmap> mMemoryCache; @Override protected void onCreate(Bundle savedInstanceState) { ... RetainFragment retainFragment = RetainFragment.findOrCreateRetainFragment(getFragmentManager()); mMemoryCache = retainFragment.mRetainedCache; if (mMemoryCache == null) { mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { ... // Initialize cache here as usual } retainFragment.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(); fm.beginTransaction().add(fragment, TAG).commit(); } return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); } }
To set the stage for this lesson, here is how Android's management of bitmap memory has evolved:
On Android Android 2.2 (API level 8) and lower, when garbage collection occurs, your app's threads get stopped. This causes a lag that can degrade performance. Android 2.3 adds concurrent garbage collection, which means
that the memory is reclaimed soon after a bitmap is no longer referenced.
On Android 2.3.3 (API level 10) and lower, the backing pixel data for a bitmap is stored in native memory. It is separate from the bitmap itself, which is stored in the Dalvik heap. The pixel data in native memory is not released
in a predictable manner, potentially causing an application to briefly exceed its memory limits and crash. As of Android 3.0 (API level 11), the pixel data is stored on the Dalvik heap along with the associated bitmap.
The code recycles the bitmap when these conditions are met:
The reference count for both
mDisplayRefCountand
mCacheRefCountis 0.
The bitmap is not
null, and it hasn't been recycled yet.
private int mCacheRefCount = 0; private int mDisplayRefCount = 0; ... // Notify the drawable that the displayed state has changed. // Keep a count to determine when the drawable is no longer displayed. public void setIsDisplayed(boolean isDisplayed) { synchronized (this) { if (isDisplayed) { mDisplayRefCount++; mHasBeenDisplayed = true; } else { mDisplayRefCount--; } } // Check to see if recycle() can be called. checkState(); } // Notify the drawable that the cache state has changed. // Keep a count to determine when the drawable is no longer being cached. public void setIsCached(boolean isCached) { synchronized (this) { if (isCached) { mCacheRefCount++; } else { mCacheRefCount--; } } // Check to see if recycle() can be called. checkState(); } private synchronized void checkState() { // If the drawable cache and display ref counts = 0, and this drawable // has been displayed, then recycle. if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed && hasValidBitmap()) { getBitmap().recycle(); } } private synchronized boolean hasValidBitmap() { Bitmap bitmap = getBitmap(); return bitmap != null && !bitmap.isRecycled(); }
》 Android 3.0 (API level 11) introduces the
BitmapFactory.Options.inBitmapfield.
If this option is set, decode methods that take the
Optionsobject
will attempt to reuse an existing bitmap when loading content. This means that the bitmap's memory is reused, resulting in improved performance, and removing both memory allocation and de-allocation. However, there are certain restrictions with how
inBitmapcan
be used. In particular, before Android 4.4 (API level 19), only equal sized bitmaps are supported. For details, please see the
inBitmapdocumentation.
Save a bitmap for later use
The following snippet demonstrates how an existing bitmap is stored for possible later use in the sample app. When an app is running on Android 3.0 or higher and a bitmap is evicted from the LruCache,
a soft reference to the bitmap is placed in a
HashSet, for possible reuse later with
inBitmap:
Set<SoftReference<Bitmap>> mReusableBitmaps; private LruCache<String, BitmapDrawable> mMemoryCache; // If you're running on Honeycomb or newer, create a // synchronized HashSet of references to reusable bitmaps. if (Utils.hasHoneycomb()) { mReusableBitmaps = Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>()); } mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) { // Notify the removed entry that is no longer being cached. @Override protected void entryRemoved(boolean evicted, String key, BitmapDrawable oldValue, BitmapDrawable newValue) { if (RecyclingBitmapDrawable.class.isInstance(oldValue)) { // The removed entry is a recycling drawable, so notify it // that it has been removed from the memory cache. ((RecyclingBitmapDrawable) oldValue).setIsCached(false); } else { // The removed entry is a standard BitmapDrawable. if (Utils.hasHoneycomb()) { // We're running on Honeycomb or later, so add the bitmap // to a SoftReference set for possible use with inBitmap later. mReusableBitmaps.add (new SoftReference<Bitmap>(oldValue.getBitmap())); } } } .... }
Use an existing bitmap
In the running app, decoder methods check to see if there is an existing bitmap they can use. For example:public static Bitmap decodeSampledBitmapFromFile(String filename, int reqWidth, int reqHeight, ImageCache cache) { final BitmapFactory.Options options = new BitmapFactory.Options(); ... BitmapFactory.decodeFile(filename, options); ... // If we're running on Honeycomb or newer, try to use inBitmap. if (Utils.hasHoneycomb()) { addInBitmapOptions(options, cache); } ... return BitmapFactory.decodeFile(filename, options); }
The next snippet shows the
addInBitmapOptions()method that is called in
the above snippet. It looks for an existing bitmap to set as the value for
inBitmap.
Note that this method only sets a value for
inBitmapif
it finds a suitable match (your code should never assume that a match will be found):
private static void addInBitmapOptions(BitmapFactory.Options options, ImageCache cache) { // inBitmap only works with mutable bitmaps, so force the decoder to // return mutable bitmaps. options.inMutable = true; if (cache != null) { // Try to find a bitmap to use for inBitmap. Bitmap inBitmap = cache.getBitmapFromReusableSet(options); if (inBitmap != null) { // If a suitable bitmap has been found, set it as the value of // inBitmap. options.inBitmap = inBitmap; } } } // This method iterates through the reusable bitmaps, looking for one // to use for inBitmap: protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) { Bitmap bitmap = null; if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) { synchronized (mReusableBitmaps) { final Iterator<SoftReference<Bitmap>> iterator = mReusableBitmaps.iterator(); Bitmap item; while (iterator.hasNext()) { item = iterator.next().get(); if (null != item && item.isMutable()) { // Check to see it the item can be used for inBitmap. if (canUseForInBitmap(item, options)) { bitmap = item; // Remove from reusable set so it can't be used again. iterator.remove(); break; } } else { // Remove from the set if the reference has been cleared. iterator.remove(); } } } } return bitmap; }
Finally, this method determines whether a candidate bitmap satisfies the size criteria to be used for
inBitmap:
static boolean canUseForInBitmap( Bitmap candidate, BitmapFactory.Options targetOptions) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // From Android 4.4 (KitKat) onward we can re-use if the byte size of // the new bitmap is smaller than the reusable bitmap candidate // allocation byte count. int width = targetOptions.outWidth / targetOptions.inSampleSize; int height = targetOptions.outHeight / targetOptions.inSampleSize; int byteCount = width * height * getBytesPerPixel(candidate.getConfig()); return byteCount <= candidate.getAllocationByteCount(); } // On earlier versions, the dimensions must match exactly and the inSampleSize must be 1 return candidate.getWidth() == targetOptions.outWidth && candidate.getHeight() == targetOptions.outHeight && targetOptions.inSampleSize == 1; } /** * A helper function to return the byte usage per pixel of a bitmap based on its configuration. */ static int getBytesPerPixel(Config config) { if (config == Config.ARGB_8888) { return 4; } else if (config == Config.RGB_565) { return 2; } else if (config == Config.ARGB_4444) { return 2; } else if (config == Config.ALPHA_8) { return 1; } return 1; }
》 Here’s an implementation of a
ViewPagerwith
ImageViewchildren.
The main activity holds the
ViewPagerand
the adapter:
public class ImageDetailActivity extends FragmentActivity { public static final String EXTRA_IMAGE = "extra_image"; private ImagePagerAdapter mAdapter; private ViewPager mPager; // A static dataset to back the ViewPager adapter public final static Integer[] imageResIds = new Integer[] { R.drawable.sample_image_1, R.drawable.sample_image_2, R.drawable.sample_image_3, R.drawable.sample_image_4, R.drawable.sample_image_5, R.drawable.sample_image_6, R.drawable.sample_image_7, R.drawable.sample_image_8, R.drawable.sample_image_9}; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.image_detail_pager); // Contains just a ViewPager mAdapter = new ImagePagerAdapter(getSupportFragmentManager(), imageResIds.length); mPager = (ViewPager) findViewById(R.id.pager); mPager.setAdapter(mAdapter); } public static class ImagePagerAdapter extends FragmentStatePagerAdapter { private final int mSize; public ImagePagerAdapter(FragmentManager fm, int size) { super(fm); mSize = size; } @Override public int getCount() { return mSize; } @Override public Fragment getItem(int position) { return ImageDetailFragment.newInstance(position); } } }
》To start with, here is a standard
GridViewimplementation
with
ImageViewchildren
placed inside a
Fragment.
Again, this might seem like a perfectly reasonable approach, but what would make it better?
public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener { private ImageAdapter mAdapter; // A static dataset to back the GridView adapter public final static Integer[] imageResIds = new Integer[] { R.drawable.sample_image_1, R.drawable.sample_image_2, R.drawable.sample_image_3, R.drawable.sample_image_4, R.drawable.sample_image_5, R.drawable.sample_image_6, R.drawable.sample_image_7, R.drawable.sample_image_8, R.drawable.sample_image_9}; // Empty constructor as per Fragment docs public ImageGridFragment() {} @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mAdapter = new ImageAdapter(getActivity()); } @Override public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View v = inflater.inflate(R.layout.image_grid_fragment, container, false); final GridView mGridView = (GridView) v.findViewById(R.id.gridView); mGridView.setAdapter(mAdapter); mGridView.setOnItemClickListener(this); return v; } @Override public void onItemClick(AdapterView<?> parent, View v, int position, long id) { final Intent i = new Intent(getActivity(), ImageDetailActivity.class); i.putExtra(ImageDetailActivity.EXTRA_IMAGE, position); startActivity(i); } private class ImageAdapter extends BaseAdapter { private final Context mContext; public ImageAdapter(Context context) { super(); mContext = context; } @Override public int getCount() { return imageResIds.length; } @Override public Object getItem(int position) { return imageResIds[position]; } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup container) { ImageView imageView; if (convertView == null) { // if it's not recycled, initialize some attributes imageView = new ImageView(mContext); imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); imageView.setLayoutParams(new GridView.LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); } else { imageView = (ImageView) convertView; } imageView.setImageResource(imageResIds[position]); // Load image into ImageView return imageView; } } }
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
GridViewrecycles
its children views. To handle this, use the techniques discussed in the Processing Bitmaps
Off the UI Thread lesson. Here is the updated solution:
public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
...
private class ImageAdapter extends BaseAdapter {
...
@Override
public View getView(int position, View convertView, ViewGroup container) {
...
loadBitmap(imageResIds[position], imageView)
return imageView;
}
}
public void loadBitmap(int resId, ImageView imageView) { if (cancelPotentialWork(resId, imageView)) { final BitmapWorkerTask task = new BitmapWorkerTask(imageView); final AsyncDrawable asyncDrawable = new AsyncDrawable(getResources(), mPlaceHolderBitmap, task); imageView.setImageDrawable(asyncDrawable); task.execute(resId); } }
static class AsyncDrawable extends BitmapDrawable { private final WeakReference<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 BitmapWorkerTask class
Note: The same code can easily be adapted to work with
ListViewas well.
相关文章推荐
- Android 获取手机系统的声音设置管理通知提醒的声音
- 【android学习1】:安装MySQL启动服务失败解决方法
- Android Toast防止重复显示的方法
- Android 滑动页面的实现,ViewPager使用详解
- android阅读器里的 txt 文本处理分页功能的实现:
- Android 启动 Activity和一键退出应用的的最佳方法
- android RecyclerView
- Android Support Design Library之NavigationView
- android:clipToPadding和android:clipChildren
- Android学习之多线程编程(handler篇)
- 学习Android从0开始之背景篇-Android系统介绍
- 《Monkey Android》第12课ImageView
- Launcher和LauncherModel之间的数据交互
- 用Android Studio通过Java代码调用C代码的JNI流程
- Android中开启进程的方法
- Android 自定义布局—瀑布流
- 学习Android从0开始之开发工具篇-Android studio详解
- Android Studio很实用的调试技巧
- Android动画
- material design学习之杂记