Change bottom bar navigation and make space for new order
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
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) {
|
||||
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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user