Add modern battery info component (#2801)

This commit is contained in:
Phil Oliver 2025-08-20 18:44:04 -04:00 committed by GitHub
parent 7e55729ee1
commit 1a9771ea28
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 313 additions and 0 deletions

View file

@ -0,0 +1,129 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.common.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.BatteryUnknown
import androidx.compose.material.icons.rounded.BatteryUnknown
import androidx.compose.material.icons.rounded.Power
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ui.common.icons.BatteryEmpty
import com.geeksville.mesh.ui.common.icons.BatteryUnknown
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusGreen
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusOrange
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed
private const val FORMAT = "%d%%"
private const val SIZE_ICON = 20
@Suppress("MagicNumber")
@Composable
fun MaterialBatteryInfo(modifier: Modifier = Modifier, level: Int) {
val levelString = FORMAT.format(level)
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp),
) {
if (level > 100) {
Icon(
modifier = Modifier.size(SIZE_ICON.dp).rotate(90f),
imageVector = Icons.Rounded.Power,
tint = MaterialTheme.colorScheme.onSurface,
contentDescription = null,
)
Text(text = "PWD", color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.labelLarge)
} else if (level < 0) {
Icon(
modifier = Modifier.size(SIZE_ICON.dp),
imageVector = BatteryUnknown,
tint = MaterialTheme.colorScheme.onSurface,
contentDescription = null,
)
} else {
// Map battery percentage to color
val fillColor =
when (level) {
in 0..19 -> MaterialTheme.colorScheme.StatusRed
in 20..39 -> MaterialTheme.colorScheme.StatusOrange
else -> MaterialTheme.colorScheme.StatusGreen
}
Icon(
modifier =
Modifier.size(SIZE_ICON.dp).drawBehind {
val insetVertical = size.height * .28f
val insetLeft = size.width * .11f
val insetRight = size.width * .22f
val availableWidth = size.width - (insetLeft + insetRight)
val availableHeight = size.height - (insetVertical * 2)
// Fill (grow from left to right)
val fillWidth = availableWidth * (level / 100f)
drawRect(
color = fillColor,
topLeft = Offset(insetLeft, insetVertical),
size = Size(fillWidth, availableHeight),
)
},
imageVector = BatteryEmpty,
tint = MaterialTheme.colorScheme.onSurface,
contentDescription = null,
)
Text(
text = levelString,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.labelLarge,
)
}
}
}
class BatteryLevelProvider : PreviewParameterProvider<Int> {
override val values: Sequence<Int> = sequenceOf(-1, 19, 39, 90, 101)
}
@PreviewLightDark
@Composable
fun MaterialBatteryInfoPreview(@PreviewParameter(BatteryLevelProvider::class) batteryLevel: Int) {
AppTheme { MaterialBatteryInfo(level = batteryLevel) }
}

View file

@ -0,0 +1,184 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.common.icons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
/**
* This is from Material Symbols.
*
* @see
* [battery_android_0](https://fonts.google.com/icons?selected=Material+Symbols+Outlined:battery_android_0:FILL@0;wght@400;GRAD@0;opsz@24&icon.query=battery&icon.set=Material+Symbols&icon.size=24&icon.color=%23000000&icon.platform=android)
*/
val BatteryEmpty: ImageVector
get() {
if (batteryEmpty != null) {
return batteryEmpty!!
}
batteryEmpty =
ImageVector.Builder(
name = "BatteryEmpty",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 960f,
viewportHeight = 960f,
)
.apply {
path(fill = SolidColor(Color.Black)) {
moveTo(160f, 720f)
quadToRelative(-50f, 0f, -85f, -35f)
reflectiveQuadToRelative(-35f, -85f)
verticalLineToRelative(-240f)
quadToRelative(0f, -50f, 35f, -85f)
reflectiveQuadToRelative(85f, -35f)
horizontalLineToRelative(540f)
quadToRelative(50f, 0f, 85f, 35f)
reflectiveQuadToRelative(35f, 85f)
verticalLineToRelative(240f)
quadToRelative(0f, 50f, -35f, 85f)
reflectiveQuadToRelative(-85f, 35f)
lineTo(160f, 720f)
close()
moveTo(160f, 640f)
horizontalLineToRelative(540f)
quadToRelative(17f, 0f, 28.5f, -11.5f)
reflectiveQuadTo(740f, 600f)
verticalLineToRelative(-240f)
quadToRelative(0f, -17f, -11.5f, -28.5f)
reflectiveQuadTo(700f, 320f)
lineTo(160f, 320f)
quadToRelative(-17f, 0f, -28.5f, 11.5f)
reflectiveQuadTo(120f, 360f)
verticalLineToRelative(240f)
quadToRelative(0f, 17f, 11.5f, 28.5f)
reflectiveQuadTo(160f, 640f)
close()
moveTo(860f, 580f)
verticalLineToRelative(-200f)
horizontalLineToRelative(20f)
quadToRelative(17f, 0f, 28.5f, 11.5f)
reflectiveQuadTo(920f, 420f)
verticalLineToRelative(120f)
quadToRelative(0f, 17f, -11.5f, 28.5f)
reflectiveQuadTo(880f, 580f)
horizontalLineToRelative(-20f)
close()
moveTo(120f, 640f)
verticalLineToRelative(-320f)
verticalLineToRelative(320f)
close()
}
}
.build()
return batteryEmpty!!
}
private var batteryEmpty: ImageVector? = null
/**
* This is from Material Symbols.
*
* @see
* [battery_android_question](https://fonts.google.com/icons?selected=Material+Symbols+Outlined:battery_android_question:FILL@0;wght@400;GRAD@0;opsz@24&icon.query=battery&icon.set=Material+Symbols&icon.size=24&icon.color=%23000000&icon.platform=android)
*/
val BatteryUnknown: ImageVector
get() {
if (batteryUnknown != null) {
return batteryUnknown!!
}
batteryUnknown =
ImageVector.Builder(
name = "BatteryUnknown",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 960f,
viewportHeight = 960f,
)
.apply {
path(fill = SolidColor(Color.Black)) {
moveTo(120f, 640f)
verticalLineToRelative(-320f)
verticalLineToRelative(320f)
close()
moveTo(726f, 720f)
lineTo(160f, 720f)
quadToRelative(-50f, 0f, -85f, -35f)
reflectiveQuadToRelative(-35f, -85f)
verticalLineToRelative(-240f)
quadToRelative(0f, -50f, 35f, -85f)
reflectiveQuadToRelative(85f, -35f)
horizontalLineToRelative(521f)
quadToRelative(-20f, 16f, -35f, 36f)
reflectiveQuadToRelative(-25f, 44f)
lineTo(160f, 320f)
quadToRelative(-17f, 0f, -28.5f, 11.5f)
reflectiveQuadTo(120f, 360f)
verticalLineToRelative(240f)
quadToRelative(0f, 17f, 11.5f, 28.5f)
reflectiveQuadTo(160f, 640f)
horizontalLineToRelative(520f)
quadToRelative(2f, 25f, 14.5f, 45.5f)
reflectiveQuadTo(726f, 720f)
close()
moveTo(800f, 660f)
quadToRelative(17f, 0f, 28.5f, -11.5f)
reflectiveQuadTo(840f, 620f)
quadToRelative(0f, -17f, -11.5f, -28.5f)
reflectiveQuadTo(800f, 580f)
quadToRelative(-17f, 0f, -28.5f, 11.5f)
reflectiveQuadTo(760f, 620f)
quadToRelative(0f, 17f, 11.5f, 28.5f)
reflectiveQuadTo(800f, 660f)
close()
moveTo(772f, 538f)
horizontalLineToRelative(57f)
verticalLineToRelative(-21f)
quadToRelative(0f, -10f, 5f, -19f)
quadToRelative(6f, -13f, 15.5f, -22f)
reflectiveQuadToRelative(19.5f, -19f)
quadToRelative(17f, -17f, 28.5f, -37f)
reflectiveQuadToRelative(11.5f, -43f)
quadToRelative(0f, -42f, -32.5f, -69.5f)
reflectiveQuadTo(800f, 280f)
quadToRelative(-38f, 0f, -68f, 22f)
reflectiveQuadToRelative(-40f, 58f)
lineToRelative(51f, 21f)
quadToRelative(6f, -20f, 21.5f, -33f)
reflectiveQuadToRelative(35.5f, -13f)
quadToRelative(21f, 0f, 36.5f, 12f)
reflectiveQuadToRelative(15.5f, 32f)
quadToRelative(0f, 17f, -10f, 30.5f)
reflectiveQuadTo(820f, 434f)
quadToRelative(-11f, 11f, -22.5f, 21.5f)
reflectiveQuadTo(779f, 480f)
quadToRelative(-6f, 14f, -6.5f, 28.5f)
reflectiveQuadTo(772f, 538f)
close()
}
}
.build()
return batteryUnknown!!
}
private var batteryUnknown: ImageVector? = null