Reputation: 2565
Situation
I'm writing a pretty simple app using Kotlin
& Android Jetpack Compose
I have a scaffold
containing my navHost
and a bottomBar
.
I can use that bottomBar
to navigate between three main screens.
One of those main screens has a detail screen, which should not show a bottomBar
.
My Code
So far, this was a piece of cake:
// MainActivitys onCreate
setContent {
val navController = rememberAnimatedNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route?.substringBeforeLast("/")
BottomBarNavTestTheme {
Scaffold(
bottomBar = {
if (currentRoute?.substringBeforeLast("/") == Screen.Detail.route) {
MyBottomNavigation(
navController,
currentRoute,
listOf(Screen.Dashboard, Screen.Map, Screen.Events) // main screens
)
}
}
) { innerPadding ->
NavHost( // to be replaced by AnimatedNavHost
navController = navController,
startDestination = Screen.Dashboard.route,
modifier = Modifier.padding(innerPadding)
) {
composable(Screen.Dashboard.route) { DashboardScreen() }
composable(Screen.Map.route) { MapScreen { navController.navigate(Screen.Detail.route) } }
composable(Screen.Events.route) { EventsScreen() }
composable(Screen.Detail.route) { MapDetailScreen() }
}
}
}
}
Problem:
I would like transitions between my screens, so i'm using Accompanists Navigation Animation:
Just replace NavHost
with AnimatedNavHost
.
When navigating from mainScreen
to detailScreen
there is a strange effect:
This looks bad, how can i fix it?
Solution
An optimal solution would look like this:
Update
I have moved on from this project. For me, this question is no longer relevant and I will likely never be able to accept an answer. However, there seem to be a lot of people interested so i'll just leave this open.
Upvotes: 11
Views: 8007
Reputation: 12293
My temporary solution is to keep NavHost
with the same fixed size adding it in the Box
instead of Scaffold
(with BottomBar) at top level app composable and adding additional custom bottom bar padding for each top level destination screen:
Box(modifier = Modifier.fillMaxSize()) {
AppNavGraph( // NavHost or AnimatedNavHost
startDestination = appStartScreen,
navController = appState.navController,
modifier = Modifier.fillMaxSize()
)
// so the bottom bar will overlap the NavHost instead of placing itself below NavHost as it happens with Scaffold
Box(modifier = Modifier.align(Alignment.BottomCenter)) {
NavigationBar(
...
)
}
}
Top level destination screen where the bottom bar is visible:
@Composable
fun DashboardScreen( // top level destination, the bottom bar is visible
...
) {
Scaffold { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.consumeWindowInsets(innerPadding)
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
// Screen Content
// add additional custom bottom bar height at the bottom of the screen
Spacer(modifier = Modifier.height(BottomBarContainerHeight))
}
}
}
BottomBarContainerHeight:
val BottomBarContainerHeight = 80.0.dp // Material3's NavigationBar's height
At least now you won't have any issues with transition animations and scroll shifting (during navigation up) because NavHost
's size is always the same
Upvotes: 0
Reputation: 31
In my project I solved this problem like this:
First you need to remove "Inner padding" from Scaffold :
BottomBarNavTestTheme {
Scaffold(
...
) { _ -> // rename param to "_"
AnimatedNavHost(
navController = navController,
startDestination = Screen.Dashboard.route,
// modifier = Modifier.padding(innerPadding) delete this
modifier = Modifier
.systemBarsPadding() // add this if you app "edge-to-edge"
.navigationBarsPadding(), // add this if you app "edge-to-edge"
) {
composable(Screen.Dashboard.route) { DashboardScreen() }
composable(Screen.Map.route) { MapScreen { navController.navigate(Screen.Detail.route) } }
composable(Screen.Events.route) { EventsScreen() }
composable(Screen.Detail.route) { MapDetailScreen() }
}
}
}
Add this annotaion where you use Scaffold (for example, in main activity) :
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
class YourActivity() : FragmentActivity()
Since we have removed innerPadding from Scaffold (which automatically added padding for the top and bottom bars (if they visible) ), now on each screen you need to manually add the necessary padding. For convenience, I propose to make two extension functions:
// default heigth for bottom bar in material 3 - 80.dp
// default heigth for top bar in material 3 - 64.dp
fun Modifier.paddingBotAndTopBar(): Modifier {
return padding(top = 64.dp, bottom = 80.dp)
}
fun Modifier.paddingTopBar(): Modifier {
return padding(top = 64.dp)
}
Now for each screen for the parent @Composable you need to specify the necessary padding, depending on the visibility of the bottom/top bar on the screen :
Column(
modifier = Modifier
.fillMaxSize()
.paddingBotAndTopBar() // if bottom bar must be visible on this screen
.paddingTopBar() // if bottom bar must be NOT visible on this screen
) {
// your screen compose content
}
Now the animations are working fine, just like you wanted.
Upvotes: 3
Reputation: 1671
The best solution I have found so far is to set the bottom padding for each screen individually.
I didn't use the padding in the scaffold content.
I used @SuppressLint("UnusedMaterialScaffoldPaddingParameter") to remove the warning for not using padding in the scaffold.
In all the screens where the bottom bar is visible, I used a bottom padding of 56 dp, which is the height of the Jetpack Compose bottom bar.
Upvotes: 1
Reputation: 11
ensure to initialize navController and navHostEngine.Create a sealed class that holds the objects of the UIs to be added to the bottom navigation.within the scaffold add bottom bar iterate through the bottom navigation item and check if each item has the destination as route if true add bottom navigation with specified data needed.
val navController = rememberNavController()
val navHostEngine = rememberNavHostEngine()
val bottomNavigationItems: List<BottomNavItem> = listOf(
BottomNavItem.MainScreen,
BottomNavItem.FavoriteScreen,
BottomNavItem.UserScreen
)
Scaffold(
bottomBar = {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
val route = navBackStackEntry?.destination?.route
bottomNavigationItems.forEach { item ->
if (item.destination.route == route) {
BottomNavigation(
backgroundColor = Color.White,
contentColor = Color.Black
) {
bottomNavigationItems.forEach { item ->
BottomNavigationItem(
icon = {
Icon(
imageVector = item.icon,
contentDescription = null
)
},
label = {
Text(text = item.title)
},
alwaysShowLabel = false,
selectedContentColor = green,
unselectedContentColor = Color.LightGray,
selected = currentDestination?.route?.contains(item.destination.route) == true,
onClick = {
navController.navigate(item.destination.route) {
navController.graph.startDestinationRoute?.let { screenRoute ->
popUpTo(screenRoute) {
saveState = true
}
}
launchSingleTop = true
restoreState = true
}
}
)
}
}
}
}
}
) {
paddingValues ->
Box(
modifier = Modifier.padding(paddingValues)
) {
DestinationsNavHost(
navGraph = NavGraphs.root,
navController = navController,
engine = navHostEngine
)
}
}
Upvotes: 1
Reputation: 452
Faced a similar issue in my code and this is how I solved it; I made use of the crossfade, you can read more https://developer.android.com/jetpack/compose/animation#crossfade and more about bottom nav from https://developer.android.com/jetpack/compose/navigation#bottom-nav
enum class HomeNavType {
DASHBOARD, ACCOUNT
}
@Composable
fun HomeScreen(navController: NavController) {
val homeNavItemState = rememberSaveable { mutableStateOf(HomeNavType.DASHBOARD) }
Scaffold(
content = { HomeContent(homeNavType = homeNavItemState.value , navController)},
bottomBar = { HomeBottomNavigation(homeNavItemState) }
)
}
@Composable
fun HomeBottomNavigation(homeNavItemState: MutableState<HomeNavType>) {
BottomNavigation() {
BottomNavigationItem(
selected = homeNavItemState.value === HomeNavType.DASHBOARD,
onClick = {
homeNavItemState.value = HomeNavType.DASHBOARD
},
icon = {
Icon(
painter = painterResource(id = R.drawable.ic_dashboard),
contentDescription = "Dashboard"
)
},
label = { Text("Dashboard") },
)
BottomNavigationItem(
selected = homeNavItemState.value === HomeNavType.ACCOUNT,
onClick = {
homeNavItemState.value = HomeNavType.ACCOUNT
},
icon = {
Icon(
painter = painterResource(id = R.drawable.ic_person),
contentDescription = "Account"
)
},
label = { Text("Account") },
)
}
}
@Composable
fun HomeContent(homeNavType: HomeNavType, navController: NavController) {
Crossfade(targetState = homeNavType) { navType ->
when (navType) {
HomeNavType.DASHBOARD -> DashboardScreen(navController)
HomeNavType.ACCOUNT -> AccountScreen(navController)
}
}
}
Upvotes: 1
Reputation: 2565
So we went with a work-around:
scaffold
now does not contain a bottomBar
anymorebottomBar
.This works fine, jus the ripple-click of the bottomBar
isn't as smooth as we'd like it to be (we're exchanging it mid-click, so this is to be expected)
This also fixes an issue, where a screen had a scrollable content, which's scroll-distance got confused a little due to it changing when hiding the bottom bar.
Upvotes: 0