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