Drawable is a general abstraction for "something that can be drawn."
Bitmap represents a raw image data. Before Android 5 it is recommended to recycle the unused bitmap with method recycle().
BitmapFactory class allows to creates Bitmap objects from various sources, including files, streams, and byte-arrays. Supported formats are jpeg, png (argb8888, rgb888), webp (Android 4.4+ withot loss less compression). You can use it
to read header to get width and height of image
to load scaled image (thumbnail) to avoid the OutOfMemory exception
to load image with reusing other Bitmap object
ImageDecoder class (API 28+) allows to convert encoded images (like PNG, JPEG, WEBP, GIF, or HEIF) into Drawable or Bitmap objects. It is more powerful then BitmapFactory. It is support animated gif, post processing and etc.
Canvas class allows to draw in mutable Bitmap object.
ThumbnailUtils is utility class for generating visual thumbnails from files.
Most apps use libraries like Glide which allow to load remote images, save them in disk/mem cache and assign to an ImageView.
read size and type
To avoid 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.
Read image size and type
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeResource(resources, R.id.myimage, options)
val imageHeight: Int = options.outHeight
val imageWidth: Int = options.outWidth
val imageType: String = options.outMimeType
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;
down scale image
To tell the decoder to subsample the image, loading a smaller version into memory, set inSampleSize in your BitmapFactory.Options object. For example, an image with resolution 2048x1536 that is decoded with an inSampleSize of 4 produces a bitmap of approximately 512x384.
Calculate sample size
fun calculateInSampleSize(
options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
// Raw height and width of image
val (height: Int, width: Int) = options.run { outHeight to outWidth }
var inSampleSize = 1
if (height > reqHeight || width > reqWidth) {
val halfHeight: Int = height / 2
val halfWidth: Int = 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
}
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;
}
Decode sample image
fun decodeSampledBitmapFromResource(
res: Resources,
resId: Int,
reqWidth: Int,
reqHeight: Int
): Bitmap {
// First decode with inJustDecodeBounds=true to check dimensions
return BitmapFactory.Options().run {
inJustDecodeBounds = true
BitmapFactory.decodeResource(res, resId, this)
// Calculate inSampleSize
inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)
// Decode bitmap with inSampleSize set
inJustDecodeBounds = false
BitmapFactory.decodeResource(res, resId, this)
}
}
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);
}
reusing bitmaps
You can decode a new image to the specified Bitmap object.
A destination bitmap must be mutable and satisfies the size. Prior Android 4.4 size must be exactly same.
Check image size
private fun canUseForInBitmap(candidate: Bitmap, targetOptions: BitmapFactory.Options): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
val width: Int = targetOptions.outWidth / targetOptions.inSampleSize
val height: Int = targetOptions.outHeight / targetOptions.inSampleSize
val byteCount: Int = width * height * getBytesPerPixel(candidate.config)
byteCount <= candidate.allocationByteCount
} else {
// On earlier versions, the dimensions must match exactly and the inSampleSize must be 1
candidate.width == targetOptions.outWidth
&& candidate.height == targetOptions.outHeight
&& targetOptions.inSampleSize == 1
}
}
/**
* A helper function to return the byte usage per pixel
* of a bitmap based on its configuration.
*/
private fun getBytesPerPixel(config: Bitmap.Config): Int {
return when (config) {
Bitmap.Config.ARGB_8888 -> 4
Bitmap.Config.RGB_565, Bitmap.Config.ARGB_4444 -> 2
Bitmap.Config.ALPHA_8 -> 1
else -> 1
}
}
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;
}
You can use set of SoftReference to store images for reusing.
For example, let images are cached in the LRU cache. When the image is removed from the cache, you can store it in a set.
Save image for reusing
var reusableBitmaps: MutableSet<SoftReference<Bitmap>>? = null
private lateinit var memoryCache: LruCache<String, BitmapDrawable>
// If you're running on Honeycomb or newer, create a
// synchronized HashSet of references to reusable bitmaps.
if (Utils.hasHoneycomb()) {
reusableBitmaps = Collections.synchronizedSet(HashSet<SoftReference<Bitmap>>())
}
memoryCache = object : LruCache<String, BitmapDrawable>(cacheParams.memCacheSize) {
// Notify the removed entry that is no longer being cached.
override fun entryRemoved(
evicted: Boolean,
key: String,
oldValue: BitmapDrawable,
newValue: BitmapDrawable
) {
if (oldValue is RecyclingBitmapDrawable) {
// The removed entry is a recycling drawable, so notify it
// that it has been removed from the memory cache.
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.
reusableBitmaps?.add(SoftReference(oldValue.bitmap))
}
}
}
}
Set<SoftReference<Bitmap>> reusableBitmaps;
private LruCache<String, BitmapDrawable> memoryCache;
// If you're running on Honeycomb or newer, create a
// synchronized HashSet of references to reusable bitmaps.
if (Utils.hasHoneycomb()) {
reusableBitmaps =
Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>());
}
memoryCache = new LruCache<String, BitmapDrawable>(cacheParams.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.
reusableBitmaps.add
(new SoftReference<Bitmap>(oldValue.getBitmap()));
}
}
}
....
}
Search reusable image in set
// This method iterates through the reusable bitmaps, looking for one
// to use for inBitmap:
fun getBitmapFromReusableSet(options: BitmapFactory.Options): Bitmap? {
mReusableBitmaps?.takeIf { it.isNotEmpty() }?.let { reusableBitmaps ->
synchronized(reusableBitmaps) {
val iterator: MutableIterator<SoftReference<Bitmap>> = reusableBitmaps.iterator()
while (iterator.hasNext()) {
iterator.next().get()?.let { item ->
if (item.isMutable) {
// Check to see it the item can be used for inBitmap.
if (canUseForInBitmap(item, options)) {
// Remove from reusable set so it can't be used again.
iterator.remove()
return item
}
} else {
// Remove from the set if the reference has been cleared.
iterator.remove()
}
}
}
}
}
return null
}
// This method iterates through the reusable bitmaps, looking for one
// to use for inBitmap:
protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
Bitmap bitmap = null;
if (reusableBitmaps != null && !reusableBitmaps.isEmpty()) {
synchronized (reusableBitmaps) {
final Iterator<SoftReference<Bitmap>> iterator
= reusableBitmaps.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;
}
A destination bitmap is specified in inBitmap field of BitmapFactory.Options class.
private fun addInBitmapOptions(options: BitmapFactory.Options, cache: ImageCache?) {
// inBitmap only works with mutable bitmaps, so force the decoder to
// return mutable bitmaps.
options.inMutable = true
// Try to find a bitmap to use for inBitmap.
cache?.getBitmapFromReusableSet(options)?.also { inBitmap ->
// If a suitable bitmap has been found, set it as the value of
// inBitmap.
options.inBitmap = inBitmap
}
}
draw in Bitmap
You can draw in mutable Bitmap object using Canvas class.
Crop bitmap
/**
* Extracts a part of a Bitmap defined by copyRect.
*/
fun getSubimage(b: Bitmap, copyRect: Rect): Bitmap? {
val subImage = Bitmap.createBitmap(
copyRect.width(),
copyRect.height(), Bitmap.Config.ARGB_8888
)
Canvas(subImage).apply {
drawBitmap(
b, copyRect,
Rect(0, 0, copyRect.width(), copyRect.height()), null
)
}
return subImage
}