Change bottom bar navigation and make space for new order
This commit is contained in:
@@ -60,9 +60,9 @@ fun AppNavHost(
|
|||||||
val mainTabRoutes = setOf(
|
val mainTabRoutes = setOf(
|
||||||
Screen.Home.route,
|
Screen.Home.route,
|
||||||
Screen.Translation.route,
|
Screen.Translation.route,
|
||||||
"main_dictionary",
|
Screen.Vocabulary.route,
|
||||||
"main_vocabulary",
|
Screen.Dictionary.route,
|
||||||
"main_exercise",
|
Screen.Exercises.route,
|
||||||
SettingsRoutes.LIST
|
SettingsRoutes.LIST
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -11,20 +11,33 @@ import androidx.compose.animation.core.spring
|
|||||||
import androidx.compose.animation.slideInVertically
|
import androidx.compose.animation.slideInVertically
|
||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
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.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
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.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.NavigationBar
|
import androidx.compose.material3.NavigationBar
|
||||||
import androidx.compose.material3.NavigationBarItem
|
import androidx.compose.material3.NavigationBarItem
|
||||||
import androidx.compose.material3.NavigationBarItemDefaults
|
import androidx.compose.material3.NavigationBarItemDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.scale
|
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.R
|
||||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||||
import eu.gaudian.translator.view.LocalShowExperimentalFeatures
|
import eu.gaudian.translator.view.LocalShowExperimentalFeatures
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
sealed class Screen(
|
sealed class Screen(
|
||||||
val route: String,
|
val route: String,
|
||||||
@@ -50,33 +64,39 @@ sealed class Screen(
|
|||||||
) {
|
) {
|
||||||
object Home : Screen("home", R.string.label_home, AppIcons.Home, AppIcons.Home)
|
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 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 Vocabulary : Screen("vocabulary", R.string.label_vocabulary, AppIcons.VocabularyFilled, AppIcons.VocabularyOutlined)
|
||||||
object Settings : Screen("settings", R.string.title_settings, AppIcons.SettingsFilled, AppIcons.SettingsOutlined)
|
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 {
|
companion object {
|
||||||
fun getAllScreens(showExperimental: Boolean = false): List<Screen> {
|
fun getAllScreens(showExperimental: Boolean = false): List<Screen> {
|
||||||
val screens = mutableListOf(Home, Translation, Dictionary, Vocabulary, Settings)
|
return listOf(Home, Vocabulary)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMoreMenuItems(showExperimental: Boolean = false): List<Screen> {
|
||||||
|
val items = mutableListOf<Screen>()
|
||||||
|
items.add(Translation)
|
||||||
|
items.add(Dictionary)
|
||||||
|
items.add(Settings)
|
||||||
if (showExperimental) {
|
if (showExperimental) {
|
||||||
screens.add(3, Exercises)
|
items.add(Exercises)
|
||||||
}
|
}
|
||||||
return screens
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun fromDestination(destination: NavDestination?): Screen {
|
fun fromDestination(destination: NavDestination?): Screen {
|
||||||
val showExperimental = LocalShowExperimentalFeatures.current
|
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
|
destination?.hierarchy?.any { it.route == screen.route } == true
|
||||||
} ?: Home
|
} ?: Home
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A modernized Material 3 bottom navigation bar with spring animations and haptic feedback.
|
|
||||||
*/
|
|
||||||
@SuppressLint("UnusedBoxWithConstraintsScope")
|
@SuppressLint("UnusedBoxWithConstraintsScope")
|
||||||
@Composable
|
@Composable
|
||||||
fun BottomNavigationBar(
|
fun BottomNavigationBar(
|
||||||
@@ -88,7 +108,11 @@ fun BottomNavigationBar(
|
|||||||
) {
|
) {
|
||||||
val showExperimental = LocalShowExperimentalFeatures.current
|
val showExperimental = LocalShowExperimentalFeatures.current
|
||||||
val screens = remember(showExperimental) { Screen.getAllScreens(showExperimental) }
|
val screens = remember(showExperimental) { Screen.getAllScreens(showExperimental) }
|
||||||
|
val moreScreen = remember { Screen.More }
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
|
var showMoreMenu by remember { mutableStateOf(false) }
|
||||||
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = isVisible,
|
visible = isVisible,
|
||||||
@@ -101,7 +125,6 @@ fun BottomNavigationBar(
|
|||||||
targetOffsetY = { it }
|
targetOffsetY = { it }
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val baseHeight = if (showLabels) 80.dp else 56.dp
|
val baseHeight = if (showLabels) 80.dp else 56.dp
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val navBarDp = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() }
|
val navBarDp = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() }
|
||||||
@@ -109,16 +132,15 @@ fun BottomNavigationBar(
|
|||||||
|
|
||||||
NavigationBar(
|
NavigationBar(
|
||||||
modifier = modifier.height(height),
|
modifier = modifier.height(height),
|
||||||
containerColor = MaterialTheme.colorScheme.surface, // Cleaner look than surfaceVariant
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
tonalElevation = 8.dp, // Slight elevation for depth
|
tonalElevation = 8.dp,
|
||||||
) {
|
) {
|
||||||
screens.forEach { screen ->
|
screens.forEach { screen ->
|
||||||
val isSelected = screen == selectedItem
|
val isSelected = screen == selectedItem
|
||||||
val title = stringResource(id = screen.title)
|
val title = stringResource(id = screen.title)
|
||||||
|
|
||||||
// 1. Spring Animation for the Icon Scale
|
|
||||||
val scale by animateFloatAsState(
|
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(
|
animationSpec = spring(
|
||||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||||
stiffness = Spring.StiffnessLow
|
stiffness = Spring.StiffnessLow
|
||||||
@@ -129,10 +151,8 @@ fun BottomNavigationBar(
|
|||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
selected = isSelected,
|
selected = isSelected,
|
||||||
onClick = {
|
onClick = {
|
||||||
if (!isSelected) {
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress) // 2. Tactile feedback
|
onItemSelected(screen)
|
||||||
onItemSelected(screen)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
label = if (showLabels) {
|
label = if (showLabels) {
|
||||||
{
|
{
|
||||||
@@ -146,12 +166,11 @@ fun BottomNavigationBar(
|
|||||||
}
|
}
|
||||||
} else null,
|
} else null,
|
||||||
icon = {
|
icon = {
|
||||||
// 3. Crossfade between Outlined and Filled icons
|
|
||||||
Crossfade(targetState = isSelected, label = "iconFade") { selected ->
|
Crossfade(targetState = isSelected, label = "iconFade") { selected ->
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (selected) screen.selectedIcon else screen.unselectedIcon,
|
imageVector = if (selected) screen.selectedIcon else screen.unselectedIcon,
|
||||||
contentDescription = title,
|
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
|
@ThemePreviews
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ import androidx.compose.material.icons.filled.AddCircle
|
|||||||
import androidx.compose.material.icons.filled.CheckCircle
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
import androidx.compose.material.icons.filled.ChevronRight
|
import androidx.compose.material.icons.filled.ChevronRight
|
||||||
import androidx.compose.material.icons.filled.LocalFireDepartment
|
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.Person
|
||||||
import androidx.compose.material.icons.filled.Psychology
|
import androidx.compose.material.icons.filled.Psychology
|
||||||
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material.icons.filled.TrendingUp
|
import androidx.compose.material.icons.filled.TrendingUp
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
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.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
|
import eu.gaudian.translator.view.composable.Screen
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeScreen(
|
fun HomeScreen(
|
||||||
@@ -60,7 +61,7 @@ fun HomeScreen(
|
|||||||
.padding(horizontal = 20.dp, vertical = 24.dp),
|
.padding(horizontal = 20.dp, vertical = 24.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(20.dp)
|
verticalArrangement = Arrangement.spacedBy(20.dp)
|
||||||
) {
|
) {
|
||||||
item { TopProfileSection() }
|
item { TopProfileSection(navController = navController) }
|
||||||
item { StreakAndGoalSection() }
|
item { StreakAndGoalSection() }
|
||||||
item {
|
item {
|
||||||
ActionCard(
|
ActionCard(
|
||||||
@@ -90,7 +91,7 @@ fun HomeScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TopProfileSection() {
|
fun TopProfileSection(navController: NavHostController) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
@@ -123,14 +124,14 @@ fun TopProfileSection() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { /* TODO: Open notifications */ },
|
onClick = { navController.navigate(Screen.Settings.route) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Notifications,
|
imageVector = Icons.Default.Settings,
|
||||||
contentDescription = "Notifications",
|
contentDescription = "Settings",
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user