feat: migrate to Material 3 Expressive APIs (#4934)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-03-26 11:42:46 -05:00 committed by GitHub
parent c259c76550
commit 141b54ff9c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 112 additions and 103 deletions

View file

@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@ -39,7 +40,7 @@ import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.ic_meshtastic
import org.meshtastic.core.resources.navigate_back
@OptIn(ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@Composable
fun MainAppBar(
modifier: Modifier = Modifier,
@ -54,26 +55,25 @@ fun MainAppBar(
) {
TopAppBar(
title = {
androidx.compose.foundation.layout.Column {
Text(
text = title,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.titleLarge,
)
},
subtitle = {
subtitle?.let {
Text(
text = title,
text = it,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.titleLarge,
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
subtitle?.let {
Text(
text = it,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
},
modifier = modifier,
navigationIcon =
if (canNavigateUp) {
{
IconButton(onClick = onNavigateUp) {

View file

@ -16,34 +16,23 @@
*/
package org.meshtastic.core.ui.component
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.OfflineShare
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FloatingActionButtonMenu
import androidx.compose.material3.FloatingActionButtonMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SmallFloatingActionButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.ToggleFloatingActionButton
import androidx.compose.material3.ToggleFloatingActionButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun MenuFAB(
expanded: Boolean,
@ -53,61 +42,31 @@ fun MenuFAB(
contentDescription: String? = null,
testTag: String? = null,
) {
Column(
FloatingActionButtonMenu(
modifier = modifier.then(if (testTag != null) Modifier.testTag(testTag) else Modifier),
expanded = expanded,
button = {
ToggleFloatingActionButton(
checked = expanded,
onCheckedChange = onExpandedChange,
content = {
val imageVector = if (expanded) Icons.Filled.Close else Icons.AutoMirrored.Rounded.OfflineShare
Icon(imageVector = imageVector, contentDescription = contentDescription)
},
containerColor = ToggleFloatingActionButtonDefaults.containerColor(),
)
},
horizontalAlignment = Alignment.End,
) {
AnimatedVisibility(
visible = expanded,
enter = fadeIn() + slideInVertically(initialOffsetY = { it / 2 }),
exit = fadeOut() + slideOutVertically(targetOffsetY = { it / 2 }),
) {
Column(
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(bottom = 16.dp),
) {
items.forEach { item ->
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = if (item.testTag != null) Modifier.testTag(item.testTag) else Modifier,
) {
Surface(
shape = MaterialTheme.shapes.small,
color = MaterialTheme.colorScheme.surfaceContainerHigh,
modifier = Modifier.padding(end = 8.dp),
) {
Text(
text = item.label,
style = MaterialTheme.typography.labelLarge,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
)
}
SmallFloatingActionButton(
onClick = {
item.onClick()
onExpandedChange(false)
},
) {
Icon(item.icon, contentDescription = item.label)
}
}
}
}
}
val rotation by animateFloatAsState(targetValue = if (expanded) 180f else 0f, label = "fab_rotation")
FloatingActionButton(
onClick = { onExpandedChange(!expanded) },
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
) {
Icon(
imageVector = if (expanded) Icons.Filled.Close else Icons.AutoMirrored.Rounded.OfflineShare,
contentDescription = contentDescription,
modifier = Modifier.rotate(rotation),
items.forEach { item ->
FloatingActionButtonMenuItem(
modifier = if (item.testTag != null) Modifier.testTag(item.testTag) else Modifier,
onClick = {
item.onClick()
onExpandedChange(false)
},
icon = { Icon(item.icon, contentDescription = null) },
text = { Text(item.label) },
)
}
}

View file

@ -19,7 +19,9 @@
package org.meshtastic.core.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialExpressiveTheme
import androidx.compose.material3.MotionScheme.Companion.expressive
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
@ -265,6 +267,7 @@ data class ColorFamily(val color: Color, val onColor: Color, val colorContainer:
val unspecified_scheme = ColorFamily(Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified)
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
@ -276,7 +279,12 @@ fun AppTheme(
val dynamicScheme = if (dynamicColor) dynamicColorScheme(darkTheme) else null
val colorScheme = dynamicScheme ?: if (darkTheme) darkScheme else lightScheme
MaterialTheme(colorScheme = colorScheme, typography = AppTypography, content = content)
MaterialExpressiveTheme(
colorScheme = colorScheme,
typography = AppTypography,
motionScheme = expressive(),
content = content,
)
}
const val MODE_DYNAMIC = 6969420