1 /*
<lambda>null2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.battery.unified
18 
19 import android.graphics.BlendMode
20 import android.graphics.Canvas
21 import android.graphics.ColorFilter
22 import android.graphics.Matrix
23 import android.graphics.Paint
24 import android.graphics.Path
25 import android.graphics.PixelFormat
26 import android.graphics.Rect
27 import android.graphics.RectF
28 import android.graphics.drawable.Drawable
29 import android.view.View
30 import com.android.systemui.battery.unified.BatteryLayersDrawable.Companion.Metrics
31 import kotlin.math.floor
32 import kotlin.math.roundToInt
33 
34 /**
35  * Draws a right-to-left fill inside of the given [framePath]. This fill is designed to exactly fill
36  * the usable space inside of [framePath], given that the stroke width of the path is 1.5, and we
37  * want an extra 0.5 (canvas units) of a gap between the fill and the stroke
38  */
39 class BatteryFillDrawable(private val framePath: Path) : Drawable() {
40     private var hScale = 1f
41     private val scaleMatrix = Matrix().also { it.setScale(1f, 1f) }
42     private val scaledPath = Path()
43     private val scaledFillRect = RectF()
44     private var scaledLeftOffset = 0f
45     private var scaledRightInset = 0f
46 
47     /** Scale this to the viewport so we fill correctly! */
48     private val fillRectNotScaled = RectF()
49     private var leftInsetNotScaled = 0f
50     private var rightInsetNotScaled = 0f
51 
52     /**
53      * Configure how much space between the battery frame (drawn at 1.5dp stroke width) and the
54      * inner fill. This is accomplished by tracing the exact same path as the frame, but using
55      * [BlendMode.CLEAR] as the blend mode.
56      *
57      * This value also affects the overall width of the fill, so it requires us to re-draw
58      * everything
59      */
60     var fillInsetAmount = -1f
61         set(value) {
62             if (field != value) {
63                 field = value
64                 updateInsets()
65                 updateScale()
66                 invalidateSelf()
67             }
68         }
69 
70     // Drawable.level cannot be overloaded
71     var batteryLevel = 0
72         set(value) {
73             field = value
74             invalidateSelf()
75         }
76 
77     var fillColor: Int = 0
78         set(value) {
79             field = value
80             fillPaint.color = value
81             invalidateSelf()
82         }
83 
84     private val clearPaint =
85         Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
86             p.style = Paint.Style.STROKE
87             p.blendMode = BlendMode.CLEAR
88         }
89 
90     private val fillPaint =
91         Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
92             p.style = Paint.Style.FILL
93             p.color = fillColor
94         }
95 
96     override fun onBoundsChange(bounds: Rect) {
97         super.onBoundsChange(bounds)
98 
99         hScale = bounds.right / Metrics.ViewportWidth
100 
101         if (bounds.isEmpty) {
102             scaleMatrix.setScale(1f, 1f)
103         } else {
104             scaleMatrix.setScale(
105                 (bounds.right / Metrics.ViewportWidth),
106                 (bounds.bottom / Metrics.ViewportHeight)
107             )
108         }
109 
110         updateScale()
111     }
112 
113     /**
114      * To support dynamic insets, we have to keep mutable references to the left/right unscaled
115      * insets, as well as the fill rect.
116      */
117     private fun updateInsets() {
118         leftInsetNotScaled = LeftFillOffsetExcludingPadding + fillInsetAmount
119         rightInsetNotScaled = RightFillInsetExcludingPadding + fillInsetAmount
120 
121         fillRectNotScaled.set(
122             leftInsetNotScaled,
123             0f,
124             Metrics.ViewportWidth - rightInsetNotScaled,
125             Metrics.ViewportHeight
126         )
127     }
128 
129     private fun updateScale() {
130         framePath.transform(/* matrix = */ scaleMatrix, /* dst = */ scaledPath)
131         scaleMatrix.mapRect(/* dst = */ scaledFillRect, /* src = */ fillRectNotScaled)
132 
133         scaledLeftOffset = leftInsetNotScaled * hScale
134         scaledRightInset = rightInsetNotScaled * hScale
135 
136         // stroke width = 1.5 (same as the outer frame) + 2x fillInsetAmount, since N px of padding
137         // requires the entire stroke to be 2N px wider
138         clearPaint.strokeWidth = (1.5f + 2 * fillInsetAmount) * hScale
139     }
140 
141     override fun draw(canvas: Canvas) {
142         if (batteryLevel == 0) {
143             return
144         }
145 
146         // saveLayer is needed here so we don't clip the other layers of our drawable
147         canvas.saveLayer(null, null)
148 
149         // Fill from the opposite direction in rtl mode
150         if (layoutDirection == View.LAYOUT_DIRECTION_RTL) {
151             canvas.scale(-1f, 1f, bounds.width() / 2f, bounds.height() / 2f)
152         }
153 
154         // We need to use 3 draw commands:
155         // 1. Clip to the current level
156         // 2. Clip anything outside of the path
157         // 3. render the fill as a rect the correct size to fit the inner space
158         // 4. Clip out the padding between the frame and the fill
159 
160         val fillLeft: Int =
161             if (batteryLevel == 100) {
162                 0
163             } else {
164                 val fillFraction = batteryLevel / 100f
165                 floor(scaledFillRect.width() * (1 - fillFraction)).roundToInt()
166             }
167 
168         // Clip to the fill level
169         canvas.clipOutRect(
170             scaledLeftOffset,
171             bounds.top.toFloat(),
172             scaledLeftOffset + fillLeft,
173             bounds.height().toFloat()
174         )
175         // Clip everything outside of the path
176         canvas.clipPath(scaledPath)
177 
178         // Draw the fill
179         canvas.drawRect(scaledFillRect, fillPaint)
180 
181         // Clear around the fill
182         canvas.drawPath(scaledPath, clearPaint)
183 
184         // Finally, restore the layer
185         canvas.restore()
186     }
187 
188     override fun setColorFilter(colorFilter: ColorFilter?) {
189         clearPaint.setColorFilter(colorFilter)
190         fillPaint.setColorFilter(colorFilter)
191     }
192 
193     // unused
194     override fun getOpacity(): Int = PixelFormat.OPAQUE
195 
196     // unused
197     override fun setAlpha(alpha: Int) {}
198 
199     companion object {
200         // 3.5f =
201         //       2.75 (left-most edge of the frame path)
202         //     + 0.75 (1/2 of the stroke width)
203         private const val LeftFillOffsetExcludingPadding = 3.5f
204 
205         // 1.5, calculated the same way, but from the right edge (without the battery cap), which
206         // consumes 2 units of width.
207         private const val RightFillInsetExcludingPadding = 1.5f
208     }
209 }
210