|
@@ -0,0 +1,138 @@
|
|
|
+package com.zaojiao.app.ui
|
|
|
+
|
|
|
+import androidx.compose.foundation.layout.Box
|
|
|
+import androidx.compose.foundation.layout.Column
|
|
|
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|
|
+import androidx.compose.foundation.layout.WindowInsets
|
|
|
+import androidx.compose.foundation.layout.consumeWindowInsets
|
|
|
+import androidx.compose.foundation.layout.fillMaxSize
|
|
|
+import androidx.compose.foundation.layout.fillMaxWidth
|
|
|
+import androidx.compose.foundation.layout.padding
|
|
|
+import androidx.compose.material3.ExperimentalMaterial3Api
|
|
|
+import androidx.compose.material3.Icon
|
|
|
+import androidx.compose.material3.MaterialTheme
|
|
|
+import androidx.compose.material3.NavigationBar
|
|
|
+import androidx.compose.material3.NavigationBarItem
|
|
|
+import androidx.compose.material3.Scaffold
|
|
|
+import androidx.compose.material3.SnackbarHost
|
|
|
+import androidx.compose.material3.SnackbarHostState
|
|
|
+import androidx.compose.material3.Text
|
|
|
+import androidx.compose.runtime.Composable
|
|
|
+import androidx.compose.runtime.remember
|
|
|
+import androidx.compose.ui.ExperimentalComposeUiApi
|
|
|
+import androidx.compose.ui.Modifier
|
|
|
+import androidx.compose.ui.composed
|
|
|
+import androidx.compose.ui.draw.drawWithContent
|
|
|
+import androidx.compose.ui.geometry.Offset
|
|
|
+import androidx.compose.ui.graphics.Color
|
|
|
+import androidx.compose.ui.platform.testTag
|
|
|
+import androidx.compose.ui.semantics.semantics
|
|
|
+import androidx.compose.ui.semantics.testTagsAsResourceId
|
|
|
+import androidx.compose.ui.unit.dp
|
|
|
+import androidx.navigation.NavDestination
|
|
|
+import androidx.navigation.NavDestination.Companion.hierarchy
|
|
|
+import com.zaojiao.app.navigation.HomeDestination
|
|
|
+import com.zaojiao.app.navigation.NavHost
|
|
|
+
|
|
|
+@OptIn(
|
|
|
+ ExperimentalMaterial3Api::class,
|
|
|
+ ExperimentalLayoutApi::class,
|
|
|
+ ExperimentalComposeUiApi::class,
|
|
|
+)
|
|
|
+@Composable
|
|
|
+fun App(
|
|
|
+ appState: AppState = rememberAppState()
|
|
|
+) {
|
|
|
+ val snackbarHostState = remember { SnackbarHostState() }
|
|
|
+
|
|
|
+ Scaffold(
|
|
|
+ modifier = Modifier.semantics {
|
|
|
+ testTagsAsResourceId = true
|
|
|
+ },
|
|
|
+ containerColor = Color.Transparent,
|
|
|
+ contentColor = MaterialTheme.colorScheme.onBackground,
|
|
|
+ contentWindowInsets = WindowInsets(0, 0, 0, 0),
|
|
|
+ snackbarHost = { SnackbarHost(snackbarHostState) },
|
|
|
+ bottomBar = {
|
|
|
+ AppBottomBar(
|
|
|
+ destinations = appState.homeDestinations,
|
|
|
+ onNavigateToDestination = appState::navigateToHomeDestination,
|
|
|
+ currentDestination = appState.currentDestination,
|
|
|
+ modifier = Modifier.testTag("NiaBottomBar"),
|
|
|
+ )
|
|
|
+ },
|
|
|
+ ) { padding ->
|
|
|
+ Box(modifier = Modifier
|
|
|
+ .fillMaxSize()
|
|
|
+ .padding(padding)
|
|
|
+ .consumeWindowInsets(padding)) {
|
|
|
+ NavHost(appState = appState)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@Composable
|
|
|
+private fun AppBottomBar(
|
|
|
+ destinations: List<HomeDestination>,
|
|
|
+ onNavigateToDestination: (HomeDestination) -> Unit,
|
|
|
+ currentDestination: NavDestination?,
|
|
|
+ modifier: Modifier = Modifier,
|
|
|
+) {
|
|
|
+ MaterialTheme.colorScheme.onSurfaceVariant
|
|
|
+
|
|
|
+ NavigationBar(
|
|
|
+ modifier = modifier,
|
|
|
+ contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
|
+ tonalElevation = 0.dp,
|
|
|
+ ) {
|
|
|
+ destinations.forEach { destination ->
|
|
|
+ val selected = currentDestination.isTopLevelDestinationInHierarchy(destination)
|
|
|
+
|
|
|
+ NavigationBarItem(
|
|
|
+ selected = selected,
|
|
|
+ onClick = { onNavigateToDestination(destination) },
|
|
|
+ enabled = true,
|
|
|
+ icon = {
|
|
|
+ if (selected) {
|
|
|
+ Icon(
|
|
|
+ imageVector = destination.selectedIcon,
|
|
|
+ contentDescription = null,
|
|
|
+ )
|
|
|
+ } else {
|
|
|
+ Icon(
|
|
|
+ imageVector = destination.unselectedIcon,
|
|
|
+ contentDescription = null,
|
|
|
+ )
|
|
|
+ }
|
|
|
+ },
|
|
|
+ alwaysShowLabel = true,
|
|
|
+ label = { Text(text = destination.iconTitle) },
|
|
|
+ modifier = Modifier.notificationDot(),
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+private fun Modifier.notificationDot(): Modifier =
|
|
|
+ composed {
|
|
|
+ val tertiaryColor = MaterialTheme.colorScheme.tertiary
|
|
|
+ drawWithContent {
|
|
|
+ drawContent()
|
|
|
+ drawCircle(
|
|
|
+ tertiaryColor,
|
|
|
+ radius = 5.dp.toPx(),
|
|
|
+ // This is based on the dimensions of the NavigationBar's "indicator pill";
|
|
|
+ // however, its parameters are private, so we must depend on them implicitly
|
|
|
+ // (NavigationBarTokens.ActiveIndicatorWidth = 64.dp)
|
|
|
+ center = center + Offset(
|
|
|
+ 64.dp.toPx() * .45f,
|
|
|
+ 32.dp.toPx() * -.45f - 6.dp.toPx(),
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+private fun NavDestination?.isTopLevelDestinationInHierarchy(destination: HomeDestination) =
|
|
|
+ this?.hierarchy?.any {
|
|
|
+ it.route?.contains(destination.name, true) ?: false
|
|
|
+ } ?: false
|