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

Posted

技术标签:

【中文标题】在 Scaffold Jetpack Compose 内的特定屏幕上隐藏顶部和底部导航器【英文标题】:hide Top and Bottom Navigator on a specific screen inside Scaffold Jetpack Compose 【发布时间】:2021-06-24 12:36:02 【问题描述】:

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

我将所有屏幕都包裹在一个带有顶栏和底栏的 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)
    

AppNavigation 包含用于导航到屏幕的NavHost

【问题讨论】:

【参考方案1】:

目前,我可以通过检查当前路线来显示或隐藏底部栏、顶部栏来实现这一点。但我认为必须有更好的解决方案。我将所有屏幕包裹在 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)

【讨论】:

这是一个很好的解决方案,但如果您找到更好的解决方案,请更新。我正在探索同样的问题。【参考方案2】:

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

    您应该使用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)) 
    在composable函数中我们使用when来控制BottomBar和TopAppBar的状态,下面我们设置bottomBarStatetopBarStatetrue,如果我们想显示BottomBar和TopAppBar,否则我们设置bottomBarStatetopBarStatefalse
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,
                )
            
        
    
)

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

    BottomNavigation 放入AnimatedVisibility 中,从bottomBarState 设置visible 值并设置enterexit 动画,在我的例子中,我使用slideInVerticallyenter 动画和@98765438 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
                            
                        
                    )
                
            
        
    )
    TopAppBar 放入AnimatedVisibility 中,从topBarState 中设置visible 值并设置enterexit 动画,在我的例子中,我使用slideInVertically 代表enter 动画和@98765439 exit动画:
AnimatedVisibility(
        visible = topBarState.value,
        enter = slideInVertically(initialOffsetY =  -it ),
        exit = slideOutVertically(targetOffsetY =  -it ),
        content = 
            TopAppBar(
                title =  Text(text = title) ,
            )
        
    )

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

结果:

不要忘记为撰写函数使用@ExperimentalAnimationApi 注解。

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

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

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

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

【讨论】:

关注了这个答案,但我的底部栏移动滞后,我不知道为什么 @ysfcyln 没有代码很难回答,我在 2016 年生产的 OnePlus 3 上测试了该示例,没有滞后。 我把它作为一个问题发布,你可以查看it。【参考方案3】:

您可以使用compositionlocal,当您使用CompositionLocalProvider 包装您的mainActivity 时,将supportActionBar 传递给您的孩子可组合在您希望隐藏topBar 的屏幕上,调用.hide() 方法 在顶部。见下文:

data class ShowAppBar(val show: ActionBar?)

internal val LocalAppBar = compositionLocalOf<ShowAppBar> error("No ActionBar provided") 

在mainActivity中,通过ActionBar传递

val showy = ShowAppBar(show = supportActionBar )
.....

 CompositionLocalProvider(
   LocalAppBar provides showy
  ) 
      YourTheme 
        yourApp()
      
   

在屏幕上调用 >> LocalAppBar.current.show?.hide()

【讨论】:

以上是关于在 Scaffold Jetpack Compose 内的特定屏幕上隐藏顶部和底部导航器的主要内容,如果未能解决你的问题,请参考以下文章

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

Android日志:Jetpack Compose中的布局

如何在jetpack compose中设置抽屉的宽度>

带有浮动顶栏的脚手架(Jetpack Compose)

Jetpack Compose - ScaffoldSnackBar

Jetpack compose 更新到 1.0.0-rc01 后无法预览