Как скрыть нижнюю панель в Jetpack Compose при использовании анимации навигации аккомпаниаторов

Ситуация

Я пишу довольно простое приложение, используя Kotlin & Android Jetpack Compose

у меня есть scaffold содержащий мой navHostи а. Я могу использовать это для перехода между тремя основными экранами. На одном из этих основных экранов есть подробный экран, на котором не должно отображаться bottomBar.


Мой код

Пока что это было проще простого:

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

Проблема :

Мне нужны переходы между моими экранами, поэтому я использую анимацию навигации для аккомпаниаторов : просто замените NavHost с участием AnimatedNavHost.

При переходе от mainScreen к detailScreen возникает странный эффект:

  1. нижняя планка скрывает
  2. размер главного экрана изменится: (см. выравниваемый текст внизу)
  3. происходит анимация на экране деталей.

Выглядит плохо, как я могу это исправить?

6 ответов

В моем проекте я решил эту проблему следующим образом:

  1. Сначала вам нужно удалить «Внутреннее дополнение» из 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. Добавьте эту аннотацию, где вы используете Scaffold(например, в основной деятельности):

            @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
    class YourActivity() : FragmentActivity()
    
  3. Так как мы удалили innerPadding из Scaffold(что автоматически добавляло отступы для верхней и нижней панели (если они видны)), теперь на каждом экране нужно вручную добавлять необходимые отступы. Для удобства предлагаю сделать две функции расширения:

            // 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. Теперь для каждого экрана для родительского @Composable нужно указать необходимый отступ в зависимости от видимости нижней/верхней панели на экране:

            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. Теперь анимации работают нормально, как вы и хотели.

Лучшее решение, которое я нашел до сих пор, — это установить нижний отступ для каждого экрана отдельно.

Я не использовал отступы в содержимом скаффолда.

Я использовал @SuppressLint("UnusedMaterialScaffoldPaddingParameter"), чтобы удалить предупреждение о том, что не используется дополнение в скаффолде.

На всех экранах, где видна нижняя полоса, я использовал нижний отступ 56 dp, что соответствует высоте нижней полосы Jetpack Compose.

Столкнулся с аналогичной проблемой в моем коде, и вот как я ее решил;

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

убедитесь, что инициализированы navController и navHostEngine. Создайте запечатанный класс, который содержит объекты пользовательских интерфейсов, которые будут добавлены в нижнюю навигацию. В скаффолде добавьте нижнюю панель, выполните итерацию по нижнему элементу навигации и проверьте, имеет ли каждый элемент пункт назначения в качестве маршрута, если это правда добавить нижнюю навигацию с указанными необходимыми данными.

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

}

Итак, мы пошли с обходным путем:

  • на высшем уровне scaffold теперь больше не содержит
  • каждый экран, которому он сейчас нужен, теперь имеет свой собственный.

Это работает нормально, только щелчок bottomBar не так гладко, как хотелось бы (мы меняем его в середине щелчка, так что этого следовало ожидать)

Это также устраняет проблему, когда на экране было прокручиваемое содержимое, которое немного сбивало с толку расстояние прокрутки из-за его изменения при скрытии нижней панели.

Мое временное решение — сохранить тот же фиксированный размер, добавив его вBoxвместоScaffold(с BottomBar) в приложении верхнего уровня с возможностью компонования и добавлением дополнительного настраиваемого заполнения нижней панели для каждого целевого экрана верхнего уровня:

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

Экран назначения верхнего уровня, на котором видна нижняя панель:

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

БоттомБарКонтейнерХигхт :

val BottomBarContainerHeight = 80.0.dp // Material3's NavigationBar's height

По крайней мере, теперь у вас не будет проблем с анимацией перехода и смещением прокрутки (во время навигации вверх), потому чтоNavHostразмер всегда один и тот же

Другие вопросы по тегам