在 Scaffold Jetpack Compose 内的特定屏幕上隐藏顶部和底部导航器

cuo*_*gtd 4 android kotlin android-jetpack android-jetpack-navigation android-jetpack-compose

我正在创建一个带有底部导航和抽屉的简单应用程序。

我将所有屏幕都包裹在带有顶栏和底栏的 Scaffold 中。我想隐藏特定屏幕上的顶部栏和底部栏。有谁知道如何实现

这是设置导航的代码。

val navController = rememberNavController()
val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed))

Scaffold(
    bottomBar = {
        AppBottomBar(navController)
    },
    topBar = {
        AppTopBar(scaffoldState)
    },
    drawerContent = {
        DrawerContent(navController, scaffoldState)
    },
    scaffoldState = scaffoldState
) {
    // ovoid bottom bar overlay content
    Column(modifier = Modifier.padding(bottom = 58.dp)) {
        AppNavigation(navController)
    }
}
Run Code Online (Sandbox Code Playgroud)

AppNavigation包含NavHost用于导航到屏幕

And*_*i R 59

我建议您使用AnimatedVisibilityBottomNavigation部件和TopAppBar小部件,我认为这是最清晰的撰写方式。

  1. 您应该用来remeberSaveable存储 BottomBar 和 TopAppBar 的状态:
// State of bottomBar, set state to false, if current page route is "car_details"
val bottomBarState = rememberSaveable { (mutableStateOf(true)) }

// State of topBar, set state to false, if current page route is "car_details"
val topBarState = rememberSaveable { (mutableStateOf(true)) }
Run Code Online (Sandbox Code Playgroud)
  1. 在可组合函数中,我们用于when控制 BottomBar 和 TopAppBar 的状态,如果我们想显示 BottomBar 和 TopAppBar,下面我们将bottomBarState和设置topBarState为,否则我们设置和:truebottomBarStatetopBarStatefalse
val navController = rememberNavController()

// Subscribe to navBackStackEntry, required to get current route
val navBackStackEntry by navController.currentBackStackEntryAsState()

// Control TopBar and BottomBar
when (navBackStackEntry?.destination?.route) {
    "cars" -> {
        // Show BottomBar and TopBar
        bottomBarState.value = true
        topBarState.value = true
    }
    "bikes" -> {
        // Show BottomBar and TopBar
        bottomBarState.value = true
        topBarState.value = true
    }
    "settings" -> {
        // Show BottomBar and TopBar
        bottomBarState.value = true
        topBarState.value = true
    }
    "car_details" -> {
        // Hide BottomBar and TopBar
        bottomBarState.value = false
        topBarState.value = false
    }
}

com.google.accompanist.insets.ui.Scaffold(
    bottomBar = {
        BottomBar(
            navController = navController,
            bottomBarState = bottomBarState
        )
    },
    topBar = {
        TopBar(
            navController = navController,
            topBarState = topBarState
        )
    },
    content = {
        NavHost(
            navController = navController,
            startDestination = NavigationItem.Cars.route,
        ) {
            composable(NavigationItem.Cars.route) {
                CarsScreen(
                    navController = navController,
                )
            }
            composable(NavigationItem.Bikes.route) {
                BikesScreen(
                    navController = navController
                )
            }
            composable(NavigationItem.Settings.route) {
                SettingsScreen(
                    navController = navController,
                )
            }
            composable(NavigationItem.CarDetails.route) {
                CarDetailsScreen(
                    navController = navController,
                )
            }
        }
    }
)
Run Code Online (Sandbox Code Playgroud)

重要提示:来自 Accompanist 的支架,在 build.gradle 中初始化。我们使用 Accompanist 中的 Scaffold,因为我们需要完全控制填充,例如,在 Compose 的默认 Scaffold 中,如果我们有 TopAppBar,则无法禁用顶部内容的填充。在我们的例子中,这是必需的,因为我们有 TopAppBar 的动画,内容应该位于 TopAppBar 下,并且我们手动控制每个页面的填充。来自 Accompanist 的文档: https: //google.github.io/accompanist/insets/

  1. 放入BottomNavigation里面AnimatedVisibility,设置visiblebottomBarState并设置enterexit动画,在我的例子中,我用于slideInVertically动画enterslideOutVertically动画exit
AnimatedVisibility(
        visible = bottomBarState.value,
        enter = slideInVertically(initialOffsetY = { it }),
        exit = slideOutVertically(targetOffsetY = { it }),
        content = {
            BottomNavigation {
                val navBackStackEntry by navController.currentBackStackEntryAsState()
                val currentRoute = navBackStackEntry?.destination?.route

                items.forEach { item ->
                    BottomNavigationItem(
                        icon = {
                            Icon(
                                painter = painterResource(id = item.icon),
                                contentDescription = item.title
                            )
                        },
                        label = { Text(text = item.title) },
                        selected = currentRoute == item.route,
                        onClick = {
                            navController.navigate(item.route) {
                                popUpTo(navController.graph.findStartDestination().id) {
                                    saveState = true
                                }
                                launchSingleTop = true
                                restoreState = true
                            }
                        }
                    )
                }
            }
        }
    )
Run Code Online (Sandbox Code Playgroud)
  1. 放入TopAppBar里面AnimatedVisibility,设置visibletopBarState并设置enterexit动画,在我的例子中,我用于slideInVertically动画enterslideOutVertically动画exit
AnimatedVisibility(
        visible = topBarState.value,
        enter = slideInVertically(initialOffsetY = { -it }),
        exit = slideOutVertically(targetOffsetY = { -it }),
        content = {
            TopAppBar(
                title = { Text(text = title) },
            )
        }
    )
Run Code Online (Sandbox Code Playgroud)

MainActivity的完整代码:

package codes.andreirozov.bottombaranimation

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.res.painterResource
import androidx.navigation.NavController
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import codes.andreirozov.bottombaranimation.screens.BikesScreen
import codes.andreirozov.bottombaranimation.screens.CarDetailsScreen
import codes.andreirozov.bottombaranimation.screens.CarsScreen
import codes.andreirozov.bottombaranimation.screens.SettingsScreen
import codes.andreirozov.bottombaranimation.ui.theme.BottomBarAnimationTheme

@ExperimentalAnimationApi
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BottomBarAnimationApp()
        }
    }
}

@ExperimentalAnimationApi
@Composable
fun BottomBarAnimationApp() {

    // State of bottomBar, set state to false, if current page route is "car_details"
    val bottomBarState = rememberSaveable { (mutableStateOf(true)) }

    // State of topBar, set state to false, if current page route is "car_details"
    val topBarState = rememberSaveable { (mutableStateOf(true)) }

    BottomBarAnimationTheme {
        val navController = rememberNavController()

        // Subscribe to navBackStackEntry, required to get current route
        val navBackStackEntry by navController.currentBackStackEntryAsState()

        // Control TopBar and BottomBar
        when (navBackStackEntry?.destination?.route) {
            "cars" -> {
                // Show BottomBar and TopBar
                bottomBarState.value = true
                topBarState.value = true
            }
            "bikes" -> {
                // Show BottomBar and TopBar
                bottomBarState.value = true
                topBarState.value = true
            }
            "settings" -> {
                // Show BottomBar and TopBar
                bottomBarState.value = true
                topBarState.value = true
            }
            "car_details" -> {
                // Hide BottomBar and TopBar
                bottomBarState.value = false
                topBarState.value = false
            }
        }

        // IMPORTANT, Scaffold from Accompanist, initialized in build.gradle.
        // We use Scaffold from Accompanist, because we need full control of paddings, for example
        // in default Scaffold from Compose we can't disable padding for content from top if we
        // have TopAppBar. In our case it's required because we have animation for TopAppBar,
        // content should be under TopAppBar and we manually control padding for each pages.
        com.google.accompanist.insets.ui.Scaffold(
            bottomBar = {
                BottomBar(
                    navController = navController,
                    bottomBarState = bottomBarState
                )
            },
            topBar = {
                TopBar(
                    navController = navController,
                    topBarState = topBarState
                )
            },
            content = {
                NavHost(
                    navController = navController,
                    startDestination = NavigationItem.Cars.route,
                ) {
                    composable(NavigationItem.Cars.route) {
                        // show BottomBar and TopBar
                        LaunchedEffect(Unit) {
                            bottomBarState.value = true
                            topBarState.value = true
                        }
                        CarsScreen(
                            navController = navController,
                        )
                    }
                    composable(NavigationItem.Bikes.route) {
                        // show BottomBar and TopBar
                        LaunchedEffect(Unit) {
                            bottomBarState.value = true
                            topBarState.value = true
                        }
                        BikesScreen(
                            navController = navController
                        )
                    }
                    composable(NavigationItem.Settings.route) {
                        // show BottomBar and TopBar
                        LaunchedEffect(Unit) {
                            bottomBarState.value = true
                            topBarState.value = true
                        }
                        SettingsScreen(
                            navController = navController,
                        )
                    }
                    composable(NavigationItem.CarDetails.route) {
                        // hide BottomBar and TopBar
                        LaunchedEffect(Unit) {
                            bottomBarState.value = false
                            topBarState.value = false
                        }
                        CarDetailsScreen(
                            navController = navController,
                        )
                    }
                }
            }
        )
    }
}

@ExperimentalAnimationApi
@Composable
fun BottomBar(navController: NavController, bottomBarState: MutableState<Boolean>) {
    val items = listOf(
        NavigationItem.Cars,
        NavigationItem.Bikes,
        NavigationItem.Settings
    )

    AnimatedVisibility(
        visible = bottomBarState.value,
        enter = slideInVertically(initialOffsetY = { it }),
        exit = slideOutVertically(targetOffsetY = { it }),
        content = {
            BottomNavigation {
                val navBackStackEntry by navController.currentBackStackEntryAsState()
                val currentRoute = navBackStackEntry?.destination?.route

                items.forEach { item ->
                    BottomNavigationItem(
                        icon = {
                            Icon(
                                painter = painterResource(id = item.icon),
                                contentDescription = item.title
                            )
                        },
                        label = { Text(text = item.title) },
                        selected = currentRoute == item.route,
                        onClick = {
                            navController.navigate(item.route) {
                                popUpTo(navController.graph.findStartDestination().id) {
                                    saveState = true
                                }
                                launchSingleTop = true
                                restoreState = true
                            }
                        }
                    )
                }
            }
        }
    )
}

@ExperimentalAnimationApi
@Composable
fun TopBar(navController: NavController, topBarState: MutableState<Boolean>) {
    val navBackStackEntry by navController.currentBackStackEntryAsState()
    val title: String = when (navBackStackEntry?.destination?.route ?: "cars") {
        "cars" -> "Cars"
        "bikes" -> "Bikes"
        "settings" -> "Settings"
        "car_details" -> "Cars"
        else -> "Cars"
    }

    AnimatedVisibility(
        visible = topBarState.value,
        enter = slideInVertically(initialOffsetY = { -it }),
        exit = slideOutVertically(targetOffsetY = { -it }),
        content = {
            TopAppBar(
                title = { Text(text = title) },
            )
        }
    )
}
Run Code Online (Sandbox Code Playgroud)

结果:

BottomBar 和 TopBar 动画

不要忘记对 compose 函数使用 @ExperimentalAnimationApi 注释。

更新:@ExperimentalAnimationApi不需要Compose 版本 1.1.0 及更高版本。

22.02.2022 更新:我做了一些研究,并更新了第 2 点。现在我们使用when来控制topBarStatebottomBarState

完整代码可在 gitHub 上找到: https://github.com/AndreiRoze/BottomBarAnimation/tree/with_animated_topbar

官方文档中提供的动画示例: https ://developer.android.com/jetpack/compose/animation/composables-modifiers


car*_*ich 11

我找到的最简单的解决方案(没有动画)

fun MainScreen(modifier: Modifier = Modifier) {

    val navController = rememberNavController()
    var showBottomBar by rememberSaveable { mutableStateOf(true) }
    val navBackStackEntry by navController.currentBackStackEntryAsState()

    showBottomBar = when (navBackStackEntry?.destination?.route) {
        "RouteOfScreenA" -> false // on this screen bottom bar should be hidden
        "RouteOfScreenB" -> false // here too
        else -> true // in all other cases show bottom bar
    }

    Scaffold(
        modifier = modifier,
        bottomBar = { if (showBottomBar) MyBottomNavigation(navController = navController) }
    ) { innerPadding ->
        MyNavHost(
            navController = navController,
            modifier = Modifier.padding(innerPadding)
        )
    }
}
Run Code Online (Sandbox Code Playgroud)


cuo*_*gtd 8

现在,我可以通过检查当前路由来显示或隐藏 bottomBar、topBar 来实现这一点。但我认为必须有更好的解决方案。我将所有屏幕都包裹在 Scaffold 中的方式可能不对。

val navController = rememberNavController()
val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed))

Scaffold(
    bottomBar = {
        if (currentRoute(navController) != "Example Screen") {
            AppBottomBar(navController)
        }
    },
    topBar = {
        AppTopBar(scaffoldState)
    },
    drawerContent = {
        DrawerContent(navController, scaffoldState)
    },
    floatingActionButton = {
        FloatingButton(navController)
    },
    scaffoldState = scaffoldState
) {
    // ovoid bottom bar overlay content
    Column(modifier = Modifier.padding(bottom = 58.dp)) {
        AppNavigation(navController)
    }
}

@Composable
public fun currentRoute(navController: NavHostController): String? {
    val navBackStackEntry by navController.currentBackStackEntryAsState()
    return navBackStackEntry?.arguments?.getString(KEY_ROUTE)
}
Run Code Online (Sandbox Code Playgroud)

  • 是的,这正是 Compose 的工作原理:如果您不希望出现“AppBottomBar”,请不要输出它。 (3认同)
  • 这是一个很好的解决方案,但如果您找到更好的解决方案,请更新。我正在探索同样的问题。 (2认同)