1 package com.android.systemui.util.drawable 2 3 import android.content.res.Resources 4 import android.graphics.Bitmap 5 import android.graphics.Canvas 6 import android.graphics.drawable.Animatable 7 import android.graphics.drawable.Animatable2 8 import android.graphics.drawable.AnimatedImageDrawable 9 import android.graphics.drawable.AnimatedRotateDrawable 10 import android.graphics.drawable.AnimatedStateListDrawable 11 import android.graphics.drawable.AnimatedVectorDrawable 12 import android.graphics.drawable.BitmapDrawable 13 import android.graphics.drawable.Drawable 14 import android.graphics.drawable.LayerDrawable 15 import android.util.Log 16 import androidx.annotation.Px 17 import com.android.app.tracing.traceSection 18 19 class DrawableSize { 20 21 companion object { 22 23 const val TAG = "SysUiDrawableSize" 24 25 /** 26 * Downscales passed Drawable to set maximum width and height. This will only be done for 27 * Drawables that can be downscaled non-destructively - e.g. animated drawables, stateful 28 * drawables, and drawables with mixed-type layers will not be downscaled. 29 * 30 * Downscaling will keep the aspect ratio. This method will not touch drawables that already 31 * fit into size specification. 32 * 33 * @param resources Resources on which to base the density of resized drawable. 34 * @param drawable Drawable to downscale. 35 * @param maxWidth Maximum width of the downscaled drawable. 36 * @param maxHeight Maximum height of the downscaled drawable. 37 * @return returns downscaled drawable if it's possible to downscale it or original if it's 38 * not. 39 */ 40 @JvmStatic downscaleToSizenull41 fun downscaleToSize( 42 res: Resources, 43 drawable: Drawable, 44 @Px maxWidth: Int, 45 @Px maxHeight: Int, 46 ): Drawable { 47 traceSection("DrawableSize#downscaleToSize") { 48 // Bitmap drawables can contain big bitmaps as their content while sneaking it past 49 // us using density scaling. Inspect inside the Bitmap drawables for actual bitmap 50 // size for those. 51 val originalWidth = 52 (drawable as? BitmapDrawable)?.bitmap?.width ?: drawable.intrinsicWidth 53 val originalHeight = 54 (drawable as? BitmapDrawable)?.bitmap?.height ?: drawable.intrinsicHeight 55 56 // Don't touch drawable if we can't resolve sizes for whatever reason. 57 if (originalWidth <= 0 || originalHeight <= 0) { 58 return drawable 59 } 60 61 // Do not touch drawables that are already within bounds. 62 if (originalWidth < maxWidth && originalHeight < maxHeight) { 63 if (Log.isLoggable(TAG, Log.DEBUG)) { 64 Log.d( 65 TAG, 66 "Not resizing $originalWidth x $originalHeight" + 67 " " + 68 "to $maxWidth x $maxHeight", 69 ) 70 } 71 72 return drawable 73 } 74 75 if (isComplicatedBitmap(drawable)) { 76 return drawable 77 } 78 79 val scaleWidth = maxWidth.toFloat() / originalWidth.toFloat() 80 val scaleHeight = maxHeight.toFloat() / originalHeight.toFloat() 81 val scale = minOf(scaleHeight, scaleWidth) 82 83 val width = (originalWidth * scale).toInt() 84 val height = (originalHeight * scale).toInt() 85 86 if (width <= 0 || height <= 0) { 87 Log.w( 88 TAG, 89 "Attempted to resize ${drawable.javaClass.simpleName} " + 90 "from $originalWidth x $originalHeight to invalid $width x $height.", 91 ) 92 return drawable 93 } 94 95 if (Log.isLoggable(TAG, Log.DEBUG)) { 96 Log.d( 97 TAG, 98 "Resizing large drawable (${drawable.javaClass.simpleName}) " + 99 "from $originalWidth x $originalHeight to $width x $height", 100 ) 101 } 102 103 // We want to keep existing config if it's more efficient than 32-bit RGB. 104 val config = 105 (drawable as? BitmapDrawable)?.bitmap?.config ?: Bitmap.Config.ARGB_8888 106 val scaledDrawableBitmap = Bitmap.createBitmap(width, height, config) 107 val canvas = Canvas(scaledDrawableBitmap) 108 109 val originalBounds = drawable.bounds 110 drawable.setBounds(0, 0, width, height) 111 drawable.draw(canvas) 112 drawable.bounds = originalBounds 113 114 return BitmapDrawable(res, scaledDrawableBitmap) 115 } 116 } 117 isComplicatedBitmapnull118 private fun isComplicatedBitmap(drawable: Drawable): Boolean { 119 return drawable.isStateful || isAnimated(drawable) || hasComplicatedLayers(drawable) 120 } 121 isAnimatednull122 private fun isAnimated(drawable: Drawable): Boolean { 123 if (drawable is Animatable || drawable is Animatable2) { 124 return true 125 } 126 127 return drawable is AnimatedImageDrawable || 128 drawable is AnimatedRotateDrawable || 129 drawable is AnimatedStateListDrawable || 130 drawable is AnimatedVectorDrawable 131 } 132 hasComplicatedLayersnull133 private fun hasComplicatedLayers(drawable: Drawable): Boolean { 134 if (drawable !is LayerDrawable) { 135 return false 136 } 137 if (drawable.numberOfLayers == 1) { 138 return false 139 } 140 141 val firstLayerType = drawable.getDrawable(0).javaClass 142 for (i in 1..<drawable.numberOfLayers) { 143 val layer = drawable.getDrawable(i) 144 if (layer.javaClass != firstLayerType) { 145 // If different layers have different drawable types, we shouldn't scale it down 146 // because we may lose the level information if one of the layers is a bitmap 147 // and another layer is a level-list. See b/244282477. 148 return true 149 } 150 if (isComplicatedBitmap(layer)) { 151 return true 152 } 153 } 154 155 return false 156 } 157 } 158 } 159