首页 > 解决方案 > 如何使用 Android Jetpack Compose 实现 BottomAppBar 和 BottomDrawer 模式?

问题描述

我正在使用 Jetpack Compose 构建 Android 应用程序。尝试使用 BottomDrawer 模式实现 BottomAppBar 时卡住了。

底部导航抽屉是固定在屏幕底部而不是左边缘或右边缘的模式抽屉。它们仅与底部应用栏一起使用。这些抽屉在点击底部应用程序栏中的导航菜单图标时打开。

material.io上的说明,以及视频的直接链接

我试过使用 Scaffold,但它只支持侧抽屉。附加到 Scaffold 内容的 BottomDrawer 显示在内容区域,并且 BottomDrawer 打开时不覆盖 BottomAppBar。在 Scaffold 函数之后移动 BottomDrawer 也无济于事:BottomAppBar 被一些不可见的块覆盖并阻止单击按钮。

我也尝试过使用 BottomSheetScaffold,但它没有 BottomAppBar 插槽。

如果 Scaffold 不支持这种模式,那么实现它的正确方法是什么?是否可以扩展 Scaffold 组件?我担心从头开始的不正确实现可能会在以后尝试实现导航和小吃栏时产生问题。

标签: androidkotlinandroid-jetpack-composeandroid-jetpack-compose-scaffold

解决方案


他们(Google 开发人员)邀请您在 Jetpack Compose Layouts路径中尝试添加其他 Material Design 组件,例如 BottomNavigationBottomDrawer到它们各自的Scaffoldslot,但没有给您解决方案。

BottomAppBar在(ie )中确实有自己的插槽,但没有 - 并且似乎专门设计用于明确地与 BottomAppBar 一起使用(请参阅 BottomDrawer的 API 文档)。ScaffoldbottomBarBottomDrawer

在 Jetpack Compose 路径的这一点上,我们已经介绍了状态提升插槽修饰符,并且已经被彻底解释过,我们将不时地尝试看看如何最好地堆叠和组织 Composables——它们几乎总是有一种自然可表达的方式,使它们在实际中发挥最佳作用。

让我进行设置,以便我们在同一页面上:

class MainActivity : ComponentActivity() {
    @ExperimentalMaterialApi
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            LayoutsCodelabTheme {
                // A surface container using the 'background' color from the theme
                Surface(color = MaterialTheme.colors.background) {
                    LayoutsCodelab()
                }
            }
        }
    }
}

这是调用我们的主要/核心可组合的主要活动。这就像在代码实验室中一样,但@ExperimentalMaterialApi注释除外。

接下来是我们的主要/核心可组合:

@ExperimentalMaterialApi
@Composable
fun LayoutsCodelab() {
    val ( gesturesEnabled, toggleGesturesEnabled ) = remember { mutableStateOf( true ) }
    val scope = rememberCoroutineScope()
    val drawerState = rememberBottomDrawerState( BottomDrawerValue.Closed )

    // BottomDrawer has to be the true core of our layout
    BottomDrawer(
        gesturesEnabled = gesturesEnabled,
        drawerState = drawerState,
        drawerContent = {
            Button(
                modifier = Modifier.align( Alignment.CenterHorizontally ).padding( top = 16.dp ),
                onClick = { scope.launch { drawerState.close() } },
                content = { Text( "Close Drawer" ) }
            )
            LazyColumn {
                items( 25 ) {
                    ListItem(
                        text = { Text( "Item $it" ) },
                        icon = {
                            Icon(
                                Icons.Default.Favorite,
                                contentDescription = "Localized description"
                            )
                        }
                    )
                }
            }
        },
        // The API describes this member as "the content of the
        // rest of the UI"
        content = {
            // So let's place the Scaffold here
            Scaffold(
                topBar = {
                    AppBarContent()
                },
                //drawerContent = { BottomBar() }  // <-- Will implement a side drawer
                bottomBar = {
                    BottomBarContent(
                        coroutineScope = scope,
                        drawerState = drawerState
                    )
                            },
            ) {
                innerPadding ->
                BodyContent( Modifier.padding( innerPadding ).fillMaxHeight() )
            }
        }
    )
}

在这里,我们Scaffold完全按照 compose 路径中的 codelab 所建议的那样利用了。请注意我的评论drawerContent是侧抽屉的自动实现。这是直接使用[各自的] Composable(s)(材料设计的模态抽屉/工作表)绕过的一种相当不错的方法!但是,它不适用于我们的BottomDrawer. 我认为 API 是实验性BottomDrawer,因为他们将Scaffold在未来进行更改以添加对 Composables 的支持。

我的基础是使用 的难度BottomDrawer,设计仅与 一起使用BottomAppBar,与Scaffold- 显式包含一个插槽用于BottomAppBar.

为了支持BottomDrawer,我们必须了解它是一个底层布局控制器,它封装了整个应用程序的 UI,防止与drawerContent抽屉打开时以外的任何东西进行交互。这要求它包含Scaffold,并且要求我们将必要的状态控制委托给BottomBarContent包装我们的实现的可组合BottomAppBar

@ExperimentalMaterialApi
@Composable
fun BottomBarContent( modifier: Modifier = Modifier, coroutineScope: CoroutineScope, drawerState: BottomDrawerState   ) {
    BottomAppBar{
        // Leading icons should typically have a high content alpha
        CompositionLocalProvider( LocalContentAlpha provides ContentAlpha.high ) {
            IconButton(
                onClick = {
                    coroutineScope.launch { drawerState.open() }
                }
            ) {
                Icon( Icons.Filled.Menu, contentDescription = "Localized description" )
            }
        }
        // The actions should be at the end of the BottomAppBar. They use the default medium
        // content alpha provided by BottomAppBar
        Spacer( Modifier.weight( 1f, true ) )
        IconButton( onClick = { /* doSomething() */ } ) {
            Icon( Icons.Filled.Favorite, contentDescription = "Localized description" )
        }
        IconButton( onClick = { /* doSomething() */ } ) {
            Icon( Icons.Filled.Favorite, contentDescription = "Localized description" )
        }
    }
}

结果告诉我们:

  • 顶部的TopAppBar_
  • 底部的BottomAppBar_
  • 单击BottomAppBar中的菜单图标将打开我们的BottomDrawer,在打开时适当地覆盖 BottomAppBar 和整个内容空间。
  • BottomDrawer正确隐藏,直到使用上面提到的按钮单击或手势来打开底部抽屉。
  • BottomAppBar中的菜单图标会在中途打开抽屉。
  • 手势通过快速短扫来中途打开底部抽屉,但只要你引导它。

推荐阅读