1 /*
<lambda>null2 * Copyright (C) 2025 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.statusbar.pipeline.battery.ui.composable
18
19 import android.graphics.Rect
20 import androidx.compose.foundation.Canvas
21 import androidx.compose.foundation.layout.fillMaxSize
22 import androidx.compose.runtime.Composable
23 import androidx.compose.runtime.getValue
24 import androidx.compose.runtime.mutableFloatStateOf
25 import androidx.compose.runtime.mutableStateOf
26 import androidx.compose.runtime.remember
27 import androidx.compose.runtime.setValue
28 import androidx.compose.ui.Modifier
29 import androidx.compose.ui.geometry.CornerRadius
30 import androidx.compose.ui.geometry.Offset
31 import androidx.compose.ui.geometry.Size
32 import androidx.compose.ui.graphics.drawscope.clipRect
33 import androidx.compose.ui.graphics.drawscope.inset
34 import androidx.compose.ui.graphics.drawscope.scale
35 import androidx.compose.ui.layout.onLayoutRectChanged
36 import com.android.systemui.common.ui.compose.load
37 import com.android.systemui.lifecycle.rememberViewModel
38 import com.android.systemui.statusbar.phone.domain.interactor.IsAreaDark
39 import com.android.systemui.statusbar.pipeline.battery.shared.ui.BatteryColors
40 import com.android.systemui.statusbar.pipeline.battery.shared.ui.BatteryFrame
41 import com.android.systemui.statusbar.pipeline.battery.shared.ui.BatteryGlyph
42 import com.android.systemui.statusbar.pipeline.battery.shared.ui.PathSpec
43 import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel
44 import kotlin.math.ceil
45
46 /**
47 * Draws a battery directly on to a [Canvas]. The canvas is scaled to fill its container, and the
48 * resulting battery is scaled using a FIT_CENTER type scaling that preserves the aspect ratio.
49 */
50 @Composable
51 fun BatteryCanvas(
52 path: PathSpec,
53 innerWidth: Float,
54 innerHeight: Float,
55 glyphs: List<BatteryGlyph>,
56 level: Int,
57 isFull: Boolean,
58 colorsProvider: () -> BatteryColors,
59 modifier: Modifier = Modifier,
60 contentDescription: String = "",
61 ) {
62
63 val totalWidth by
64 remember(glyphs) {
65 mutableFloatStateOf(
66 if (glyphs.isEmpty()) {
67 0f
68 } else {
69 // Pads in between each glyph, skipping the first
70 glyphs.drop(1).fold(glyphs.first().width) { acc: Float, next: BatteryGlyph ->
71 acc + INTER_GLYPH_PADDING_PX + next.width
72 }
73 }
74 )
75 }
76
77 Canvas(modifier = modifier.fillMaxSize(), contentDescription = contentDescription) {
78 val scale = path.scaleTo(size.width, size.height)
79 val colors = colorsProvider()
80
81 scale(scale, pivot = Offset.Zero) {
82 if (isFull) {
83 // Saves a layer since we don't need background here
84 drawPath(path = path.path, color = colors.fill)
85 } else {
86 // First draw the body
87 drawPath(path.path, colors.background)
88 // Then draw the body, clipped to the fill level
89 clipRect(0f, 0f, innerWidth, innerHeight) {
90 drawRoundRect(
91 color = colors.fill,
92 topLeft = Offset.Zero,
93 size = Size(width = level.scaledLevel(), height = innerHeight),
94 cornerRadius = CornerRadius(2f),
95 )
96 }
97 }
98
99 // Now draw the glyphs
100 var horizontalOffset = (BatteryFrame.innerWidth - totalWidth) / 2
101 for (glyph in glyphs) {
102 // Move the glyph to the right spot
103 val verticalOffset = (BatteryFrame.innerHeight - glyph.height) / 2
104 inset(
105 // Never try and inset more than half of the available size - see b/400246091.
106 minOf(horizontalOffset, size.width / 2),
107 minOf(verticalOffset, size.height / 2),
108 ) {
109 glyph.draw(this, colors)
110 }
111
112 horizontalOffset += glyph.width + INTER_GLYPH_PADDING_PX
113 }
114 }
115 }
116 }
117
118 // Experimentally-determined value
119 private const val INTER_GLYPH_PADDING_PX = 0.8f
120
121 @Composable
UnifiedBatterynull122 fun UnifiedBattery(
123 viewModelFactory: BatteryViewModel.Factory,
124 isDark: IsAreaDark,
125 modifier: Modifier = Modifier,
126 ) {
127 val viewModel = rememberViewModel(traceName = "UnifiedBattery") { viewModelFactory.create() }
128 val path = viewModel.batteryFrame
129
130 var bounds by remember { mutableStateOf(Rect()) }
131
132 val colorProvider = {
133 if (isDark.isDark(bounds)) {
134 viewModel.colorProfile.dark
135 } else {
136 viewModel.colorProfile.light
137 }
138 }
139
140 BatteryCanvas(
141 path = path,
142 innerWidth = viewModel.innerWidth,
143 innerHeight = viewModel.innerHeight,
144 glyphs = viewModel.glyphList,
145 level = viewModel.level,
146 isFull = viewModel.isFull,
147 colorsProvider = colorProvider,
148 modifier =
149 modifier.onLayoutRectChanged { relativeLayoutBounds ->
150 bounds =
151 with(relativeLayoutBounds.boundsInScreen) { Rect(left, top, right, bottom) }
152 },
153 contentDescription = viewModel.contentDescription.load() ?: "",
154 )
155 }
156
157 /** Calculate the right-edge of the clip for the fill-rect, based on a level of [0-100] */
scaledLevelnull158 private fun Int.scaledLevel(): Float {
159 val endSide = BatteryFrame.innerWidth
160 return ceil((toFloat() / 100f) * endSide)
161 }
162