An overview of the Drawable
If you need to display static images within your application, you can use the Drawable class and its subclasses to draw shapes and images. A Drawable is a general abstraction of a Drawable object. Different subclasses can be used for specific image scenes, which can be extended to define drawable objects that behave in a unique way.
Definition and instantiation of Drawable
Drawable can be defined and instantiated in one of three ways:
-
The constructor
Use existing Drawable subclasses, such as ShapeDrawable, to draw basic physics graphics; ColorDrawable, used to draw a specific color; BitmapDrawable, used to draw a specific bitmap, etc.
You can also directly inherit the Drawable and customize the drawing behavior:
// This example is a Drawable to draw the largest circle of the region. Class MyDrawable:Drawable() { private val redPaint: Paint = Paint().apply { setARGB(255, 255, 0, 0)} override fun draw(canvas: canvas) { Int = bounds.width() val height: Int = bounds.height() val radius: Float = math.min (width, height).tofloat () / 2f // Draw a circle from the center of canvas. DrawCircle ((width / 2).tofloat (), (height / 2).toFloat(), radius, redPaint) } override funsetAlpha(Alpha: Int) {// Must override method, handle transparency} override funsetColorFilter(colorFilter: ColorFilter?) {// Must override method to handle color filter} Override fun getOpacity(): Int = // Must override the method that returns the opacity of this Drawable/transparency // must return the following values: / / PixelFormat. UNKNOWN / / PixelFormat. TRANSLUCENT only draw place under the cover of contents / / PixelFormat. TRANSPARENT TRANSPARENT, Pixelformat.opaque} pixelformat.opaque}Copy the code
-
Create drawable objects from resource images
The most direct way is to store specific types of image files such as PNG, JPG, GIF, etc in the resources directory.
It is worth noting that image resources in the res/drawable/ directory can be automatically optimized for lossless image compression by the AAPT tool during the build process. However, apPT does not modify images in the res/raw/ folder.
If multiple Drawable objects are instantiated by the same resource and properties (such as transparency) of one of them are changed, the other objects will be affected as well.
val myImage1: Drawable = ResourcesCompat.getDrawable(context.resources, R.drawable.my_image, null) val myImage2: Drawable = ResourcesCompat.getDrawable(context.resources, R.drawable.my_image, Null) myImage1.setalpha (1)//myImage2 will also be affectedCopy the code
-
Create drawable objects from XML resources
For example TransitionDrawable:
<transition xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/image_expand">
<item android:drawable="@drawable/image_collapse">
</transition>
Copy the code
VectorDrawable
A VectorDrawable is a vector graph defined in an XML file as a set of points, lines, and curves and their associated color information. The main advantage of using vector-drawable objects is that the image can be scaled. Images can be scaled without degrading display quality, that is, the same file can be resized for different screen densities without degrading image quality. This not only reduces APK file size, but also reduces developer maintenance. You can also use vector images for animations by using multiple XML files for various display resolutions instead of multiple images.
A VectorDrawable defines statically drawable objects. Similar to the SVG format, each vector graph is defined as a tree hierarchy consisting of path and Group objects. Each path contains the geometry of the object’s outline, while the group contains the details of the transformation. All paths are drawn in the order they appear in the XML file.
Vector Asset Studio makes it easy to add Vector graphics as XML files to your project.
For vector graphics, there is another class, AnimatedVectorDrawable, which animates the properties of the vector graphics.
Android5.0 began to support the use of vector graphics, if you want to use in a lower version, so can be done in VectorDrawableCompat and AnimatedVectorDrawableCompat compatible. In a control, such as ImageView, you can use the srcCompat property to reference vector graphics.
Bitmap
Bitmap is a display independent Bitmap digital image file format. BMP files are usually uncompressed, so they are usually much larger than compressed image file formats for the same image.
The core of Bitmap storage lies in the information of the image. How many pixels are there in width, how many pixels are there in height, and then specific information about each pixel.
For each pixel, typically stored color depths are 2 (1 bit), 16 (4 bit), 256 (8 bit), 65536 (16 bit) and 16.7 million (24 bit) colors.
The more pixels in the Bitmap, the more colors can be expressed by each point, and the clearer and richer the image will be.
Available quality types when instantiating a Bitmap in Android:
- ALPHA_8 (8/8 = 1) bytes/pixel
Save only transparency information, no color information
- RGB_565 (Red, green and Blue 5+6+5=16 16/8 = 2) bytes/pixel
Save red, green and blue information without transparency
- ARGB_4444 (Transparency, red, green and blue 4+4+4+4=16 16/8 = 2) bytes/pixel
Transparency and red, green and blue are available, but the number of colors you can use is small
- ARGB_8888 (Transparency, red, green and blue 8+8+8+8=32 32/8 = 4) bytes/pixel
Transparency and red, green and blue are available, but you can use a larger number of colors
- RGBA_F16 (Transparency, Red green blue 16+16+16+16 +16=48 48/8 = 6) bytes/pixel
Transparency and red, green and blue are available, but you can use more colors
Bitmaps use in-memory calculations
Calculation formula (for in-memory Bitmap) :
Memory used = Horizontal pixels * vertical pixels * bytes per pixelCopy the code
For example, for a Bitmap with 1024 * 1024 pixels and ARGB_8888 mass, the memory required to load it into memory is:
1024 * 1024 * 4 = 4MB
Loading bitmaps into An Android app can be complicated for a number of reasons:
-
Bitmaps can easily drain your application’s memory budget.
-
Loading bitmaps in interface threads can degrade application performance, slow down response times, and even cause ANR messages to be displayed in the system. Therefore, thread processing must be managed properly when using bitmaps.
-
If your application loads multiple bitmaps into memory, you need to skillfully manage memory and disk caches. Otherwise, the responsiveness and smoothness of the application interface may be affected.
Efficient loading of large images
Since bitmaps are real image data and consume a lot of memory, what happens when you have to load a large image in some scenes (such as loading a large high-resolution image from an album)?
You can follow the following steps to efficiently load large images:
-
inJustDecodeBounds
When loading a Bitmap from BitmapFactory, pass in the Config parameter and set it to true to inJustDecodeBounds. BitmapFactory does not automatically allocate memory for it during loading, but instead reads the size and type of the Bitmap.
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
Copy the code
-
inSampleSize
InJustDecodeBounds lets you know the size of the bitmap, which can then be used to determine if you want to lower the sample rate of the image in several other dimensions:
1. Load the estimated memory usage of the complete image in memory.
2. The amount of memory to load this image can be allocated based on any other memory requirements of the application.
3. The size of the target ImageView or interface component to which the image will be loaded. For example, if a 1024×768 pixel image ends up showing up as a 128×96 pixel thumbnail in ImageView, it’s not worth loading it into memory.
4. Screen size and density of the current device.
For example, an image with a resolution of 2048*1536 decoded with 4 as inSampleSize will generate a bitmap of approximately (2048/4) 512 * (1536/4) 384. Loading this image into memory takes 0.75MB instead of the 12MB required for the full image (assuming the bitmap is configured to ARGB_8888).
The value of inSampleSize should be a power of 2. Namely 1, 2, 4, 8…
So, the next thing to do is to calculate the actual adoption rate according to the actual bitmap size and the actual required size:
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Val (height: Int, width: Int) = options.run {outHeight to outWidth} varinSampleSize = 1
if(height > reqHeight | | width > reqWidth) {val halfHeight: Int = height / 2 val halfWidth: Int = width / 2 / / calculate the biggest of allinSampleSize value, which is a power of 2 and makes both height and width greater than the requested height and width.while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
inSampleSize *= 2
}
}
return inSampleSize
}
Copy the code
To sum up, when loading a large image, the overall process looks like this:
Fun decodeSampledBitmapFromResource (res: Resources, resId: Int, reqWidth: Int, reqHeight: Int) : Bitmap {/ / image size information firstreturn BitmapFactory.Options().run {
inJustDecodeBounds = trueBitmapFactory. DecodeResource (res, resId, this) / / calculationinSampleSize
inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight) // Finally load the actual bitmap according to the calculated sampling rateinJustDecodeBounds = false
BitmapFactory.decodeResource(res, resId, this)
}
}
Copy the code
Caching bitmaps
Loading a single bitmap into the interface is simple, but it can get complicated if you need to load many images at once. In many cases (such as a ListView, GridView, or ViewPager), there is an infinite number of images on the screen combined with images that might soon scroll onto the screen.
For such components, the system limits memory usage by recycling the off-screen subviews. The garbage collector also frees loaded bitmaps, but when the user slides back to the previously reclaimed item, memory and disk caching allow the component to quickly reload the processed image.
- Memory cache — LruCache
Set parameters related to LruCahce initialization
private lateinit var memoryCache: LruCache<String, Bitmap> override fun onCreate(savedInstanceState: Bundle?) {... // Get the maximum available VM memory (in KB), exceeding which an OutOfMemory exception will be thrown. Val maxMemory = (Runtime.geTruntime ().maxMemory() / 1024).toint () // use 1/8 of the available memory as the memory cache. val cacheSize = maxMemory / 8 memoryCache = object : LruCache<String, Bitmap>(cacheSize) { override fun sizeOf(key: String, bitmap: bitmap): Int {// The cache size is in KBreturn bitmap.byteCount / 1024
}
}
...
}
Copy the code
Use memory cache:
Fun loadBitmap(resId: Int, imageView: imageView) {val imageKey: String = resid.toString (); Bitmap? = getBitmapFromMemCache(imageKey)? .also { mImageView.setImageBitmap(it) } ? : Run {// Memory cache does not have, So the asynchronous loading mImageView. SetImageResource (R.d rawable. Image_placeholder) val task = BitmapWorkerTask () task. The execute (resId) is null }}Copy the code
When loading images asynchronously, bitmap should be stored in memory cache in time:
private inner class BitmapWorkerTask : AsyncTask<Int, Unit, Bitmap>() { ... // Asynchronously load the bitmap, and immediately put the bitmap into the memory cache. Override FundoInBackground(vararg params: Int?) : Bitmap? {returnparams[0]? .let { imageId -> decodeSampledBitmapFromResource(resources, imageId, 100, 100)? .also { bitmap -> addBitmapToMemoryCache(imageId.toString(), bitmap) } } } ... }Copy the code
- Disk cache
An in-memory cache helps speed up access to recently viewed bitmaps, but you cannot rely on images retained in this cache. Components with large data sets like the GridView can easily fill up the memory cache. The application may be interrupted by other tasks, such as phone calls, and in the background, the application may be terminated and the memory cache destroyed.
In these cases, disk caching can be used to hold the processed bitmap and help reduce load time when the image is no longer in the in-memory cache. Of course, getting images from disk is slower than loading them from memory, and should be done in background threads because disk read times are unpredictable.
Complete picture access:
Private const val DISK_CACHE_SIZE = 1024 * 1024 * 10 // 10MB disk cache space private const val DISK_CACHE_SUBDIR ="thumbnails". private var diskLruCache: DiskLruCache? = null private val diskCacheLock = ReentrantLock() // Even initializing disk cache requires disk operations and should not be performed on the main thread. // However, this also means that the cache may be accessed before initialization. // To solve this problem, a lock object is used to ensure that the application does not read data from the disk cache until it is initialized. private val diskCacheLockCondition: Condition = diskCacheLock.newCondition() private var diskCacheStarting =trueoverride fun onCreate(savedInstanceState: Bundle?) {... // Initialize the memory cache... Val cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR) InitDiskCacheTask().execute(cacheDir)... } internal inner class InitDiskCacheTask : AsyncTask<File, Void, Void>() { override fundoInBackground(vararg params: File): Void? {
diskCacheLock.withLock {
val cacheDir = params[0]
diskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE)
diskCacheStarting = false/ / finish initializing diskCacheLockCondition signalAll threads that are waiting () / / wake up}returnnull } } internal inner class BitmapWorkerTask : AsyncTask<Int, Unit, Bitmap>() { ... // Asynchronously decode the image override fundoInBackground(vararg params: Int?) : Bitmap? {val imageKey = params[0].toString() // Check disk cache asynchronouslyreturngetBitmapFromDiskCache(imageKey) ? : / / hard disk was not found in cache decodeSampledBitmapFromResource (resources, params [0], 100, 100)? AddBitmapToCache (imageKey, it)}}} Fun addBitmapToCache(key: String, bitmap: Bitmap) {// Check whether there is any available cache in the memory cache, if not, put it into the memory cacheif(getBitmapFromMemCache(key) == null) { memoryCache.put(key, Synchronized (diskCacheLock) {diskCacheLock (diskCacheLock) {diskcacrucache? .apply {if(! containsKey(key)) { put(key, bitmap) } } } } fun getBitmapFromDiskCache(key: String): Bitmap? = diskCacheLock.withLock {while (diskCacheStarting) {
try {
diskCacheLockCondition.await()
} catch (e: InterruptedException) {
}
}
returndiskLruCache? .get(key)} // creates a unique subdirectory of the specified application cache directory. Try to use it externally, but if not installed, fall back to internal storage. fun getDiskCacheDir(context: Context, uniqueName: String): File {// Check if media is installed or storage is built in, if so, try using an external cache directory, otherwise use an internal cache directory val cachePath =if(Environment.MEDIA_MOUNTED == Environment.getExternalStorageState() || ! isExternalStorageRemovable()) { context.externalCacheDir.path }else {
context.cacheDir.path
}
return File(cachePath + File.separator + uniqueName)
}
Copy the code
Manage Bitmap memory
For different Versions of Android, bitmap memory management changes as follows:
-
On Android 2.2 (API level 8) and below, the application thread stops when garbage collection occurs. This leads to latency, which reduces performance. Concurrent garbage collection has been added to Android 2.3, which means that memory is reclaimed soon after the system stops referencing bitmaps.
-
On Android 2.3.3 (API level 10) and below, bitmap pixel data is stored in Native memory. It is separate from the bitmap itself stored in the Dalvik heap. Pixel data in Native memory is not released in a predictable way, which can cause an application to temporarily exceed its memory limit and crash.
-
From Android 3.0 (API level 11) to Android 7.1 (API level 25), pixel data is stored on the Dalvik heap along with the associated bitmap.
-
In Android 8.0 (API level 26) and later, bitmap pixel data is stored in the native heap.
Therefore, different management schemes should be adopted in different Android versions:
-
On Android 2.3.3 (API level 10) and later versions, you are advised to use recycle() to recycle memory as soon as possible.
-
Android 3.0 (API level 11) BitmapFactory is introduced. The Options. InBitmap field. If this option is set, decoding methods that take the Options object will try to reuse the existing bitmap when loading content. This means that bitmap memory is reused, which improves performance, while removing memory allocation and unallocation. However, there are limitations to how inBitmap can be used, requiring that the bitmaps that need to be reused be mutable. Especially prior to Android 4.4 (API level 19), the system only supported bitmaps of the same size (with the same number of pixels and a sampling rate of 1).
About BitmapFactory. The Options
This parameter is very powerful, it can set the Bitmap sampling rate, by changing the width, height, zoom, etc., to reduce the image pixels. In general, by setting this value, you can have better control, display, and use bitmaps.
Here are the attributes and their meanings:
attribute | type | meaning |
---|---|---|
inJustDecodeBounds | boolean | Whether to parse only picture information |
inSampleSize | int | Sampling rate (how many samples are sampled every time as the result, for example, 4, which means that there are no 4 pixels, take 1 as the result and return it, the width and height are 1/4 of the original, and the total is 1/16 of the original) |
inScaled | boolean | Whether to scale the current file when needed. False does not scale; True or not, dynamically zooms based on folder resolution and screen resolution |
inDensity | int | Set the screen resolution of the resource folder where the file resides |
inTargetDensity | int | Said true display screen resolution, zoom ratio = inTargetDensity/inDensity |
inScreenDensity | int | Is the pixel density of the actual screen being used currently useless |
inPreferredConfig | enum | Sets the format for storing pixels. RGB_565 ARGB_8888 etc |
inMutable | boolean | If set to true, the decoding method will always return a bitmap that is mutable (pixel information can be modified), rather than a bitmap that is immutable (non-modifiable). |
inBitmap | Bitmap | Reuse this Bitmap, which needs to be modifiable |
outConfig | Config | If known, decode bitmap will have configuration |
outHeight | int | The final height of the bitmap |
outWidth | int | Final width of the bitmap |
inDither | boolean | Whether to dither, if set to true, the decoder will attempt to dither the decoded image. For example, the image was originally 100px200px when you need 150px300px. After setting this parameter, the original 100 pixels will be tiled, and the extra white space will be used by two adjacent colors to create a “middle color” for transition. |
About mutable and immutable bitmaps
For mutable bitmaps, setPixel(int x,int y,int color) and other functions can be used to set the pixel values, while immutable bitmaps using these methods will report errors.
So when is a generated Bitmap mutable and when is it immutable? The answer is: Bitmaps loaded through the BitmapFactory are immutable; Only bitmaps created by a few functions in a Bitmap are pixel-variable. The functions are:
Copy (Config Config, Boolean isMutable)//isMutable true 2. CreateBitmap (@nullable DisplayMetrics display, int width, int height, @NonNull Config config, boolean hasAlpha, @NonNull ColorSpace colorSpace) 3.createScaledBitmap(@NonNull Bitmap src, int dstWidth, int dstHeight, boolean filter)Copy the code