From d2d2f53b596b03c640be0554494d23ee8589ed21 Mon Sep 17 00:00:00 2001 From: jonasgaudian <43753916+jonasgaudian@users.noreply.github.com> Date: Mon, 16 Feb 2026 13:12:15 +0100 Subject: [PATCH] Change bottom bar navigation and make space for new order --- .../eu/gaudian/translator/view/Navigation.kt | 6 +- .../view/composable/BottomNavigationBar.kt | 180 ++++++++++++++++-- .../translator/view/home/HomeScreen.kt | 13 +- 3 files changed, 169 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/eu/gaudian/translator/view/Navigation.kt b/app/src/main/java/eu/gaudian/translator/view/Navigation.kt index 40e86b5..91b1fb2 100644 --- a/app/src/main/java/eu/gaudian/translator/view/Navigation.kt +++ b/app/src/main/java/eu/gaudian/translator/view/Navigation.kt @@ -60,9 +60,9 @@ fun AppNavHost( val mainTabRoutes = setOf( Screen.Home.route, Screen.Translation.route, - "main_dictionary", - "main_vocabulary", - "main_exercise", + Screen.Vocabulary.route, + Screen.Dictionary.route, + Screen.Exercises.route, SettingsRoutes.LIST ) diff --git a/app/src/main/java/eu/gaudian/translator/view/composable/BottomNavigationBar.kt b/app/src/main/java/eu/gaudian/translator/view/composable/BottomNavigationBar.kt index 31466f6..1901651 100644 --- a/app/src/main/java/eu/gaudian/translator/view/composable/BottomNavigationBar.kt +++ b/app/src/main/java/eu/gaudian/translator/view/composable/BottomNavigationBar.kt @@ -11,20 +11,33 @@ import androidx.compose.animation.core.spring import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItemDefaults import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale @@ -41,6 +54,7 @@ import androidx.navigation.NavDestination.Companion.hierarchy import eu.gaudian.translator.R import eu.gaudian.translator.ui.theme.ThemePreviews import eu.gaudian.translator.view.LocalShowExperimentalFeatures +import kotlinx.coroutines.launch sealed class Screen( val route: String, @@ -50,33 +64,39 @@ sealed class Screen( ) { object Home : Screen("home", R.string.label_home, AppIcons.Home, AppIcons.Home) object Translation : Screen("translation", R.string.label_translation, AppIcons.TranslateFilled, AppIcons.TranslateOutlined) - object Dictionary : Screen("dictionary", R.string.label_dictionary, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined) - object Exercises : Screen("exercises", R.string.label_exercises, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined) object Vocabulary : Screen("vocabulary", R.string.label_vocabulary, AppIcons.VocabularyFilled, AppIcons.VocabularyOutlined) object Settings : Screen("settings", R.string.title_settings, AppIcons.SettingsFilled, AppIcons.SettingsOutlined) + object More : Screen("more", R.string.label_more, AppIcons.MoreVert, AppIcons.MoreVert) + object Dictionary : Screen("dictionary", R.string.label_dictionary, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined) + object Exercises : Screen("exercises", R.string.label_exercises, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined) companion object { fun getAllScreens(showExperimental: Boolean = false): List { - val screens = mutableListOf(Home, Translation, Dictionary, Vocabulary, Settings) + return listOf(Home, Vocabulary) + } + + fun getMoreMenuItems(showExperimental: Boolean = false): List { + val items = mutableListOf() + items.add(Translation) + items.add(Dictionary) + items.add(Settings) if (showExperimental) { - screens.add(3, Exercises) + items.add(Exercises) } - return screens + return items } @Composable fun fromDestination(destination: NavDestination?): Screen { val showExperimental = LocalShowExperimentalFeatures.current - return getAllScreens(showExperimental).find { screen -> + val allScreens = getAllScreens(showExperimental) + getMoreMenuItems(showExperimental) + More + return allScreens.find { screen -> destination?.hierarchy?.any { it.route == screen.route } == true } ?: Home } } } -/** - * A modernized Material 3 bottom navigation bar with spring animations and haptic feedback. - */ @SuppressLint("UnusedBoxWithConstraintsScope") @Composable fun BottomNavigationBar( @@ -88,7 +108,11 @@ fun BottomNavigationBar( ) { val showExperimental = LocalShowExperimentalFeatures.current val screens = remember(showExperimental) { Screen.getAllScreens(showExperimental) } + val moreScreen = remember { Screen.More } val haptic = LocalHapticFeedback.current + var showMoreMenu by remember { mutableStateOf(false) } + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val scope = rememberCoroutineScope() AnimatedVisibility( visible = isVisible, @@ -101,7 +125,6 @@ fun BottomNavigationBar( targetOffsetY = { it } ) ) { - val baseHeight = if (showLabels) 80.dp else 56.dp val density = LocalDensity.current val navBarDp = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() } @@ -109,16 +132,15 @@ fun BottomNavigationBar( NavigationBar( modifier = modifier.height(height), - containerColor = MaterialTheme.colorScheme.surface, // Cleaner look than surfaceVariant - tonalElevation = 8.dp, // Slight elevation for depth + containerColor = MaterialTheme.colorScheme.surface, + tonalElevation = 8.dp, ) { screens.forEach { screen -> val isSelected = screen == selectedItem val title = stringResource(id = screen.title) - // 1. Spring Animation for the Icon Scale val scale by animateFloatAsState( - targetValue = if (isSelected) 1.2f else 1.0f, // Subtle pop effect + targetValue = if (isSelected) 1.2f else 1.0f, animationSpec = spring( dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow @@ -129,10 +151,8 @@ fun BottomNavigationBar( NavigationBarItem( selected = isSelected, onClick = { - if (!isSelected) { - haptic.performHapticFeedback(HapticFeedbackType.LongPress) // 2. Tactile feedback - onItemSelected(screen) - } + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + onItemSelected(screen) }, label = if (showLabels) { { @@ -146,12 +166,11 @@ fun BottomNavigationBar( } } else null, icon = { - // 3. Crossfade between Outlined and Filled icons Crossfade(targetState = isSelected, label = "iconFade") { selected -> Icon( imageVector = if (selected) screen.selectedIcon else screen.unselectedIcon, contentDescription = title, - modifier = Modifier.scale(scale) // Apply the spring scale + modifier = Modifier.scale(scale) ) } }, @@ -164,8 +183,127 @@ fun BottomNavigationBar( ) ) } + + // More menu item + val moreSelected = selectedItem is Screen.More || + Screen.getMoreMenuItems(showExperimental).contains(selectedItem) + val moreTitle = stringResource(R.string.label_more) + val moreScale by animateFloatAsState( + targetValue = if (moreSelected) 1.2f else 1.0f, + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessLow + ), + label = "moreIconScale" + ) + + NavigationBarItem( + selected = moreSelected, + onClick = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + showMoreMenu = true + }, + label = if (showLabels) { + { + Text( + text = moreTitle, + maxLines = 1, + fontSize = 10.sp, + fontWeight = if (moreSelected) FontWeight.Bold else FontWeight.Normal, + color = if(moreSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } else null, + icon = { + Icon( + imageVector = moreScreen.selectedIcon, + contentDescription = moreTitle, + modifier = Modifier.scale(moreScale) + ) + }, + colors = NavigationBarItemDefaults.colors( + indicatorColor = MaterialTheme.colorScheme.primaryContainer, + selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer, + selectedTextColor = MaterialTheme.colorScheme.primary, + unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant, + unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) } } + + // Modal Bottom Sheet for More menu + if (showMoreMenu) { + ModalBottomSheet( + onDismissRequest = { showMoreMenu = false }, + sheetState = sheetState + ) { + MoreBottomSheetContent( + showExperimental = showExperimental, + onItemSelected = { screen -> + scope.launch { + sheetState.hide() + showMoreMenu = false + onItemSelected(screen) + } + } + ) + } + } +} + +@Composable +private fun MoreBottomSheetContent( + showExperimental: Boolean, + onItemSelected: (Screen) -> Unit +) { + val moreItems = remember(showExperimental) { Screen.getMoreMenuItems(showExperimental) } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 32.dp) + ) { + Text( + text = stringResource(R.string.label_more), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) + ) + + HorizontalDivider() + + moreItems.forEach { screen -> + MoreMenuItem( + screen = screen, + onClick = { onItemSelected(screen) } + ) + } + } +} + +@Composable +private fun MoreMenuItem( + screen: Screen, + onClick: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(horizontal = 24.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = screen.selectedIcon, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = stringResource(screen.title), + style = MaterialTheme.typography.bodyLarge + ) + } } @ThemePreviews @@ -186,4 +324,4 @@ fun BottomNavigationBarPreview() { ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/gaudian/translator/view/home/HomeScreen.kt b/app/src/main/java/eu/gaudian/translator/view/home/HomeScreen.kt index c38a8fc..da5a9ba 100644 --- a/app/src/main/java/eu/gaudian/translator/view/home/HomeScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/home/HomeScreen.kt @@ -21,9 +21,9 @@ import androidx.compose.material.icons.filled.AddCircle import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.LocalFireDepartment -import androidx.compose.material.icons.filled.Notifications import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Psychology +import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.TrendingUp import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults @@ -42,6 +42,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController +import eu.gaudian.translator.view.composable.Screen @Composable fun HomeScreen( @@ -60,7 +61,7 @@ fun HomeScreen( .padding(horizontal = 20.dp, vertical = 24.dp), verticalArrangement = Arrangement.spacedBy(20.dp) ) { - item { TopProfileSection() } + item { TopProfileSection(navController = navController) } item { StreakAndGoalSection() } item { ActionCard( @@ -90,7 +91,7 @@ fun HomeScreen( } @Composable -fun TopProfileSection() { +fun TopProfileSection(navController: NavHostController) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth() @@ -123,14 +124,14 @@ fun TopProfileSection() { } IconButton( - onClick = { /* TODO: Open notifications */ }, + onClick = { navController.navigate(Screen.Settings.route) }, modifier = Modifier .clip(CircleShape) .background(MaterialTheme.colorScheme.surfaceVariant) ) { Icon( - imageVector = Icons.Default.Notifications, - contentDescription = "Notifications", + imageVector = Icons.Default.Settings, + contentDescription = "Settings", tint = MaterialTheme.colorScheme.onSurfaceVariant ) }