9

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:

  1. the bottomBar hides
  2. the main screen resizes: (see the bottom aligned text)
  3. the animation to the detail screen takes place.

This looks bad, how can i fix it?


Solution

An optimal solution would look like this:

  • Main screen keeps bottom bar & fades out.
  • Simultaneously the detail page enters without a bottom bar.

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.

m.reiter
  • 1,796
  • 2
  • 11
  • 31
  • What do you want the animation to look like? Your `if` check is doing exactly what you said it should do: immediately hiding the bottom nav without any animation as soon as the destination changes (i.e., when you `navigate()`) – ianhanniballake Aug 23 '21 at 16:12
  • Instead of the `NavHost` have you tried using the `AnimatedNavHost` and see a difference? – hsm59 Aug 24 '21 at 08:48
  • @hsm59 i am using AnimatedNavHost. Sorry if this wasn't clear enough: "Just replace NavHost with AnimatedNavHost." – m.reiter Aug 24 '21 at 09:06
  • this is a valid concern that AndroidX team should address. – guness Nov 21 '21 at 13:27
  • XML also has this problem. I solved it using ViewPager2 + BottomNavigationView. This resulted in a very smooth transition. In compose, I think you can use the accompains - pager layout library. – pie Dec 27 '21 at 06:22
  • I see you have a bottom navigation bar and from the bottom bar (dashboard page) you are navigating to a page without bottom bar. I need to do something similar. I have already asked a question regarding this issue. Since you have already achieved such navigation can you kindly provide some sort of sample? @m.reiter –  Mar 02 '22 at 14:19
  • @FahimHoque I can't provide a sample, since all of my code is in closed source customer projects, sorry. However, my question above + answer below + a tutorial for navigation component should be all you need? – m.reiter Mar 03 '22 at 15:28
  • @m.reiter navigation in compose is harder than flutter and quite confusing for me. I have a only one more question about your approach. Is the navigation system maintainable for applications with lot more pages/screens? –  Mar 03 '22 at 18:19
  • @FahimHoque To be honest i haven't migrated any large app to navigation component, but from what i've seen it should be possible quite "easy". – m.reiter Mar 04 '22 at 08:11
  • I guess I have a similar issue for which I found a temporary solution https://stackoverflow.com/questions/76284055/how-to-set-navigation-bar-padding – user924 May 18 '23 at 20:18

6 Answers6

1

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)
        }
    }
}
EmmaJoe
  • 362
  • 4
  • 9
  • 1
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Sep 22 '21 at 19:05
  • It looks like you can just put the Crossfade in the `bottomBar =` and it fixes the strange animation OP was seeing. I.e. `bottomBar = { Crossfade(targetState = showBottomBar) { show -> when (show) { true -> NavigationBar {} fasle -> {}` – Jacob Ferrero Jun 29 '22 at 07:10
1

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

}
  • This solution uses [Ramcosta navigation library](https://github.com/raamcosta/compose-destinations) – Odhiambo Brandy Mar 31 '22 at 13:56
  • Could you add some explanation about the code? – Peter Bruins Mar 31 '22 at 15:17
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Mar 31 '22 at 15:17
1

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.

Thiago Souza
  • 1,171
  • 8
  • 16
1

In my project I solved this problem like this:

  1. 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() }
             }
         }
     }
    
  2. Add this annotaion where you use Scaffold (for example, in main activity) :

    @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
    class YourActivity() : FragmentActivity()
    
  3. 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)
    }
    
  4. 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
      }
    
  5. Now the animations are working fine, just like you wanted.

Anton
  • 11
  • 2
0

So we went with a work-around:

  • on top-level the scaffold now does not contain a bottomBar anymore
  • each screen which needs it now has its own bottomBar.

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.

m.reiter
  • 1,796
  • 2
  • 11
  • 31
  • no, you better not add bottom bar separately to each top level destination screen because there are animations, even between top level destinations screens like slide horizontal, it's not good! bad approach - bottom bar should be fixed during screen transition animation. You can check how I temporary solved it https://stackoverflow.com/questions/76284055/how-to-set-navigation-bar-padding Also bottom bar can have inner animations for items https://i.stack.imgur.com/s57KV.gif so you better add the bottom bar at app top level composable once – user924 May 18 '23 at 20:24
0

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

user924
  • 8,146
  • 7
  • 57
  • 139