xref: /aosp_15_r20/frameworks/base/packages/SystemUI/src/com/android/systemui/util/drawable/DrawableSize.kt (revision d57664e9bc4670b3ecf6748a746a57c557b6bc9e)
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