mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: refactor adaptive navigation using NavigationSuiteScaffold
- Integrate the `material3-adaptive-navigation-suite` library to streamline cross-platform navigation layouts. - Replace manual branching logic for `NavigationBar` and `NavigationRail` with `NavigationSuiteScaffold` in `MeshtasticNavigationSuite`. - Implement a `coerceNavigationType` helper to ensure `NavigationRail` is used for expanded widths instead of promoting to a `NavigationDrawer`. - Utilize `currentWindowAdaptiveInfo` with large-width breakpoint support to improve responsiveness on Desktop and external displays. - Remove redundant `MeshtasticNavigationBar` and `MeshtasticNavigationRail` private composables in favor of the standard navigation suite item API.
This commit is contained in:
parent
3feec759a1
commit
a5f72734ed
3 changed files with 48 additions and 99 deletions
|
|
@ -54,6 +54,7 @@ kotlin {
|
||||||
implementation(libs.jetbrains.compose.material3.adaptive)
|
implementation(libs.jetbrains.compose.material3.adaptive)
|
||||||
implementation(libs.jetbrains.compose.material3.adaptive.layout)
|
implementation(libs.jetbrains.compose.material3.adaptive.layout)
|
||||||
implementation(libs.jetbrains.compose.material3.adaptive.navigation)
|
implementation(libs.jetbrains.compose.material3.adaptive.navigation)
|
||||||
|
implementation(libs.jetbrains.compose.material3.adaptive.navigation.suite)
|
||||||
implementation(libs.jetbrains.navigationevent.compose)
|
implementation(libs.jetbrains.navigationevent.compose)
|
||||||
implementation(libs.jetbrains.navigation3.ui)
|
implementation(libs.jetbrains.navigation3.ui)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,27 +22,21 @@ import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.scaleIn
|
import androidx.compose.animation.scaleIn
|
||||||
import androidx.compose.animation.scaleOut
|
import androidx.compose.animation.scaleOut
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.Badge
|
import androidx.compose.material3.Badge
|
||||||
import androidx.compose.material3.BadgedBox
|
import androidx.compose.material3.BadgedBox
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||||
import androidx.compose.material3.NavigationBar
|
|
||||||
import androidx.compose.material3.NavigationBarItem
|
|
||||||
import androidx.compose.material3.NavigationRail
|
|
||||||
import androidx.compose.material3.NavigationRailItem
|
|
||||||
import androidx.compose.material3.PlainTooltip
|
import androidx.compose.material3.PlainTooltip
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TooltipAnchorPosition
|
import androidx.compose.material3.TooltipAnchorPosition
|
||||||
import androidx.compose.material3.TooltipBox
|
import androidx.compose.material3.TooltipBox
|
||||||
import androidx.compose.material3.TooltipDefaults
|
import androidx.compose.material3.TooltipDefaults
|
||||||
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||||
|
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
|
||||||
|
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults
|
||||||
|
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType
|
||||||
import androidx.compose.material3.rememberTooltipState
|
import androidx.compose.material3.rememberTooltipState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
|
@ -53,7 +47,6 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation3.runtime.NavBackStack
|
import androidx.navigation3.runtime.NavBackStack
|
||||||
import androidx.navigation3.runtime.NavKey
|
import androidx.navigation3.runtime.NavKey
|
||||||
import androidx.window.core.layout.WindowWidthSizeClass
|
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
import org.meshtastic.core.model.ConnectionState
|
import org.meshtastic.core.model.ConnectionState
|
||||||
import org.meshtastic.core.model.DeviceType
|
import org.meshtastic.core.model.DeviceType
|
||||||
|
|
@ -70,8 +63,11 @@ import org.meshtastic.core.ui.navigation.icon
|
||||||
import org.meshtastic.core.ui.viewmodel.UIViewModel
|
import org.meshtastic.core.ui.viewmodel.UIViewModel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shared adaptive navigation shell. Provides a Bottom Navigation bar on phones, and a Navigation Rail on tablets and
|
* Shared adaptive navigation shell using [NavigationSuiteScaffold].
|
||||||
* desktop targets.
|
*
|
||||||
|
* Automatically renders a [NavigationBar][androidx.compose.material3.NavigationBar] on compact screens and a
|
||||||
|
* [NavigationRail][androidx.compose.material3.NavigationRail] on medium/expanded widths, without manual branching. Uses
|
||||||
|
* [currentWindowAdaptiveInfo] with large-width breakpoint support for Desktop and External Display targets.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -86,7 +82,6 @@ fun MeshtasticNavigationSuite(
|
||||||
val selectedDevice by uiViewModel.currentDeviceAddressFlow.collectAsStateWithLifecycle()
|
val selectedDevice by uiViewModel.currentDeviceAddressFlow.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val adaptiveInfo = currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)
|
val adaptiveInfo = currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)
|
||||||
val isCompact = adaptiveInfo.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT
|
|
||||||
val currentKey = backStack.lastOrNull()
|
val currentKey = backStack.lastOrNull()
|
||||||
val rootKey = backStack.firstOrNull()
|
val rootKey = backStack.firstOrNull()
|
||||||
val topLevelDestination = TopLevelDestination.fromNavKey(rootKey)
|
val topLevelDestination = TopLevelDestination.fromNavKey(rootKey)
|
||||||
|
|
@ -95,37 +90,48 @@ fun MeshtasticNavigationSuite(
|
||||||
handleNavigation(destination, topLevelDestination, currentKey, backStack, uiViewModel)
|
handleNavigation(destination, topLevelDestination, currentKey, backStack, uiViewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCompact) {
|
// Cap the layout type at NavigationRail for expanded widths — we don't want a permanent
|
||||||
Scaffold(
|
// NavigationDrawer. NavigationSuiteScaffoldDefaults resolves COMPACT → NavigationBar,
|
||||||
modifier = modifier,
|
// MEDIUM/EXPANDED → NavigationRail already; passing the custom adaptiveInfo ensures the
|
||||||
bottomBar = {
|
// large-width (1200dp+) breakpoints are respected.
|
||||||
MeshtasticNavigationBar(
|
val layoutType = NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(adaptiveInfo).coerceNavigationType()
|
||||||
topLevelDestination = topLevelDestination,
|
|
||||||
connectionState = connectionState,
|
NavigationSuiteScaffold(
|
||||||
unreadMessageCount = unreadMessageCount,
|
modifier = modifier,
|
||||||
selectedDevice = selectedDevice,
|
layoutType = layoutType,
|
||||||
uiViewModel = uiViewModel,
|
navigationSuiteItems = {
|
||||||
onNavigate = onNavigate,
|
TopLevelDestination.entries.forEach { destination ->
|
||||||
|
item(
|
||||||
|
selected = destination == topLevelDestination,
|
||||||
|
onClick = { onNavigate(destination) },
|
||||||
|
icon = {
|
||||||
|
NavigationIconContent(
|
||||||
|
destination = destination,
|
||||||
|
isSelected = destination == topLevelDestination,
|
||||||
|
connectionState = connectionState,
|
||||||
|
unreadMessageCount = unreadMessageCount,
|
||||||
|
selectedDevice = selectedDevice,
|
||||||
|
uiViewModel = uiViewModel,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = { Text(stringResource(destination.label)) },
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
) { padding ->
|
},
|
||||||
Box(modifier = Modifier.fillMaxSize().padding(padding)) { content() }
|
) {
|
||||||
}
|
content()
|
||||||
} else {
|
|
||||||
Row(modifier = modifier.fillMaxSize()) {
|
|
||||||
MeshtasticNavigationRail(
|
|
||||||
topLevelDestination = topLevelDestination,
|
|
||||||
connectionState = connectionState,
|
|
||||||
unreadMessageCount = unreadMessageCount,
|
|
||||||
selectedDevice = selectedDevice,
|
|
||||||
uiViewModel = uiViewModel,
|
|
||||||
onNavigate = onNavigate,
|
|
||||||
)
|
|
||||||
Box(modifier = Modifier.weight(1f).fillMaxSize()) { content() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caps [NavigationSuiteType] so that expanded/extra-large widths still use a NavigationRail instead of promoting to a
|
||||||
|
* permanent NavigationDrawer.
|
||||||
|
*/
|
||||||
|
private fun NavigationSuiteType.coerceNavigationType(): NavigationSuiteType = when (this) {
|
||||||
|
NavigationSuiteType.NavigationDrawer -> NavigationSuiteType.NavigationRail
|
||||||
|
else -> this
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleNavigation(
|
private fun handleNavigation(
|
||||||
destination: TopLevelDestination,
|
destination: TopLevelDestination,
|
||||||
topLevelDestination: TopLevelDestination?,
|
topLevelDestination: TopLevelDestination?,
|
||||||
|
|
@ -164,65 +170,6 @@ private fun handleNavigation(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MeshtasticNavigationBar(
|
|
||||||
topLevelDestination: TopLevelDestination?,
|
|
||||||
connectionState: ConnectionState,
|
|
||||||
unreadMessageCount: Int,
|
|
||||||
selectedDevice: String?,
|
|
||||||
uiViewModel: UIViewModel,
|
|
||||||
onNavigate: (TopLevelDestination) -> Unit,
|
|
||||||
) {
|
|
||||||
NavigationBar {
|
|
||||||
TopLevelDestination.entries.forEach { destination ->
|
|
||||||
NavigationBarItem(
|
|
||||||
selected = destination == topLevelDestination,
|
|
||||||
onClick = { onNavigate(destination) },
|
|
||||||
icon = {
|
|
||||||
NavigationIconContent(
|
|
||||||
destination = destination,
|
|
||||||
isSelected = destination == topLevelDestination,
|
|
||||||
connectionState = connectionState,
|
|
||||||
unreadMessageCount = unreadMessageCount,
|
|
||||||
selectedDevice = selectedDevice,
|
|
||||||
uiViewModel = uiViewModel,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MeshtasticNavigationRail(
|
|
||||||
topLevelDestination: TopLevelDestination?,
|
|
||||||
connectionState: ConnectionState,
|
|
||||||
unreadMessageCount: Int,
|
|
||||||
selectedDevice: String?,
|
|
||||||
uiViewModel: UIViewModel,
|
|
||||||
onNavigate: (TopLevelDestination) -> Unit,
|
|
||||||
) {
|
|
||||||
NavigationRail {
|
|
||||||
TopLevelDestination.entries.forEach { destination ->
|
|
||||||
NavigationRailItem(
|
|
||||||
selected = destination == topLevelDestination,
|
|
||||||
onClick = { onNavigate(destination) },
|
|
||||||
icon = {
|
|
||||||
NavigationIconContent(
|
|
||||||
destination = destination,
|
|
||||||
isSelected = destination == topLevelDestination,
|
|
||||||
connectionState = connectionState,
|
|
||||||
unreadMessageCount = unreadMessageCount,
|
|
||||||
selectedDevice = selectedDevice,
|
|
||||||
uiViewModel = uiViewModel,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
label = { Text(stringResource(destination.label)) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun NavigationIconContent(
|
private fun NavigationIconContent(
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,7 @@ compose-multiplatform-materialIconsExtended = { module = "org.jetbrains.compose.
|
||||||
jetbrains-compose-material3-adaptive = { module = "org.jetbrains.compose.material3.adaptive:adaptive", version.ref = "jetbrains-adaptive" }
|
jetbrains-compose-material3-adaptive = { module = "org.jetbrains.compose.material3.adaptive:adaptive", version.ref = "jetbrains-adaptive" }
|
||||||
jetbrains-compose-material3-adaptive-layout = { module = "org.jetbrains.compose.material3.adaptive:adaptive-layout", version.ref = "jetbrains-adaptive" }
|
jetbrains-compose-material3-adaptive-layout = { module = "org.jetbrains.compose.material3.adaptive:adaptive-layout", version.ref = "jetbrains-adaptive" }
|
||||||
jetbrains-compose-material3-adaptive-navigation = { module = "org.jetbrains.compose.material3.adaptive:adaptive-navigation", version.ref = "jetbrains-adaptive" }
|
jetbrains-compose-material3-adaptive-navigation = { module = "org.jetbrains.compose.material3.adaptive:adaptive-navigation", version.ref = "jetbrains-adaptive" }
|
||||||
|
jetbrains-compose-material3-adaptive-navigation-suite = { module = "org.jetbrains.compose.material3:material3-adaptive-navigation-suite", version.ref = "compose-multiplatform-material3" }
|
||||||
|
|
||||||
# Google
|
# Google
|
||||||
firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
|
firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue