Change bottom bar navigation and make space for new order

This commit is contained in:
jonasgaudian
2026-02-16 13:12:15 +01:00
parent 7fccda7f77
commit d2d2f53b59
3 changed files with 169 additions and 30 deletions

View File

@@ -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
)

View File

@@ -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<Screen> {
val screens = mutableListOf(Home, Translation, Dictionary, Vocabulary, Settings)
if (showExperimental) {
screens.add(3, Exercises)
return listOf(Home, Vocabulary)
}
return screens
fun getMoreMenuItems(showExperimental: Boolean = false): List<Screen> {
val items = mutableListOf<Screen>()
items.add(Translation)
items.add(Dictionary)
items.add(Settings)
if (showExperimental) {
items.add(Exercises)
}
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
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,7 +183,126 @@ 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
)
}
}

View File

@@ -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
)
}