如何在 Material 3 Jetpack Compose Android 中实现 BottomSheet

32

我知道如何使用 BottomSheetScaffold 在 Material 2 Jetpack Compose 中实现 BottomSheet。

但是在 Material 3 中没有 BottomSheetScaffold。此外,在官方示例中也没有关于 BottomSheet 的内容。


2
您可以参考此文章:https://johncodeos.com/how-to-create-a-bottom-sheet-with-jetpack-compose/ - dphans
3
您可以参考Jetpack Compose Material 3中提供的组件此处BottomSheetScaffold在Material 3中尚未可用。 - Abhimanyu
2
今天使用最新的Material 3 Lib material3:1.0.0-alpha16 进行了检查,发现 BottomSheetScaffold 在 Material 3 上还不可用。 - Tonnie
1
ModalBottomSheet已经添加到Compose版本1.1.0-alpha06中,我已经编辑了我的答案以更新这个信息! - Arthur Kasparian
6个回答

26

所以我成功地让它工作了!

截至今天,BottomSheetScaffold在Material3上似乎还没有可用的版本,这个问题在我找到的这个issue中有所讨论:https://issuetracker.google.com/issues/229839039

我摘录了谷歌开发者回复中的重要部分:

Swipeable不是很容易使用。目前它有一些需要解决的关键性错误(我们正在解决这个问题),这就是为什么我们在M3中限制了我们暴露给Swipeable的表面。未来几个月,我们计划专注于这个特定领域,并提高开发人员的体验。

Jetpack Compose的Material 3仍处于alpha阶段-这意味着我们认为组件已经可以投入生产,但API形状在alpha阶段是灵活的。这给了我们空间进行迭代,同时从开发人员那里获得现实反馈,最终有助于改善您的体验。 在等待组件被(完全)实现或在alpha版本中公开之前,复制粘贴源代码可能是一个好方法! 拥有源代码,同时API形状仍然是灵活的,会带给您许多好处,如易于更新依赖项,即使API发生变化,并允许您以自己的速度发展组件。

所以我只是按照建议复制粘贴了 BottomSheetScaffold 到我的项目中。当然,由于缺少一些类和一些小的API更改,它不会立即工作。最后,我通过拉取和修改以下类并将它们添加到我的项目中,成功让它工作了:

  • BottomSheetScaffold.kt
  • Drawer.kt
  • Strings.kt
  • Swipeable.kt

如果您想尝试,我创建了一个gist包含源代码:https://gist.github.com/Marlinski/0b043968c2f574d70ee6060aeda54882

您需要更改导入以使其在您的项目中工作,并通过在gradle文件的android {}部分中添加以下选项来添加"-Xjvm-default=all"

android{ 
   ...
   kotlinOptions {
        freeCompilerArgs += ["-Xjvm-default=all"]

        // "-Xjvm-default=all" option added because of this error:
        // ... Inheritance from an interface with '@JvmDefault' members is only allowed with -Xjvm-default option
        // triggered by porting BottomSheetScaffold for Material3 on Swipeable.kt:844
   }
}

对我来说它非常有效,直到在material3中得到官方支持之前,我将继续使用此解决方案。

希望这可以帮到你!


3
太好了,感谢您在上菜前就将其从烤箱中取出。 - Tonnie
1
感谢您的代码片段!如果有人想要跟踪此问题,这里是缺失底部工作表组件的问题跟踪器链接:https://issuetracker.google.com/issues/244189383。 - goldensoju
这太棒了,干得好!你认为我将ModalBottomSheetLayout移植到我的项目中会有多难?如果你能提供你修改的源代码的链接,那就太好了,我对Google的源代码仓库不是很了解。 - Nathan
它已经在M3中可用。 - IgorGanapolsky

16

我们在Material3终于有了ModalBottomSheet。

var openBottomSheet by rememberSaveable { mutableStateOf(false) }
val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded  = true)



// Sheet content
if (openBottomSheet) {
    ModalBottomSheet(
        onDismissRequest = { openBottomSheet = false },
        sheetState = bottomSheetState,
    ) {
        Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
            Button(
                // Note: If you provide logic outside of onDismissRequest to remove the sheet,
                // you must additionally handle intended state cleanup, if any.
                onClick = {
                    scope.launch { bottomSheetState.hide() }.invokeOnCompletion {
                        if (!bottomSheetState.isVisible) {
                            openBottomSheet = false
                        }
                    }
                }
            ) {
                Text("Hide Bottom Sheet")
            }
        }
    }
}

更多阅读:链接


1
@IgorGanapolsky 可在 androidx.compose.material3:material3:1.1.0-alpha08 中使用。 - Andrei R
1
自从 androidx.compose.material3:material3:1.1.0-alpha08 版本以后,你不能使用 rememberSheetStateskipHalfExpanded,所以你应该将代码从 val bottomSheetState = rememberSheetState(skipHalfExpanded = true) 修改为 val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - Andrei R
3
我刚刚遇到了比Material库中普通的ModalBottomSheet更糟糕的情况,以前你只需要使用底部表单状态来显示/隐藏,但现在你必须通过标志来控制它,因为如果你使用状态并尝试隐藏它,它会保持在那里并带有一点偏移,你无法与屏幕后面进行交互...哈哈 - Gustavo Ross
1
哎呀..谁有一个好的实现呢? - Mehdi.ncb
为什么你要维护一个变量 openBottomSheet,如果我们可以直接调用 bottomSheetState.show() 来显示 ModalBottomSheet 呢? - undefined
显示剩余2条评论

14

更新 2023年2月23日

自Compose Material3版本1.1.0-alpha06起,ModalBottomSheet 现已作为实验性的组合部件可用(文档)。


我使用带有AnimatedVisibility的全屏对话框获得了非常相似的结果,如果感兴趣,这是代码:

// Visibility state for the dialog which will trigger it only once when called
val transitionState = remember {
    MutableTransitionState(false).apply {
        targetState = true
    }
}

Dialog(
    onDismissRequest = {} // You can set a visibility state variable to false in here which will close the dialog when clicked outside its bounds, no reason to when full screen though,
    properties = DialogProperties(
        // This property makes the dialog full width of the screen
        usePlatformDefaultWidth = false
    )
) {

    // Visibility animation, more information in android docs if needed
    AnimatedVisibility(
        visibleState = transitionState,
        enter = slideInVertically(
            initialOffsetY = { it },
            animationSpec = ...
        ),
        exit = slideOutVertically(
            targetOffsetY = { it },
            animationSpec = ...
        )
    )
) {

    Box(
        modifier = Modifier.fillMaxSize().background(color = ...)
    ) {
        // Your layout

        // This can be any user interraction that closes the dialog
        Button(
            transitionState.apply { targetState = false }
        ) ...
    }
}

所有这些都在一个可组合的函数中,当执行 UI 操作以打开该对话框时,该函数将被调用,虽然不是理想的解决方案,但它可以工作。
希望我能帮到你!

1
不错。我肯定会在 onDismissRequest 中添加关闭操作,否则按系统返回按钮将无法关闭对话框。 - bompf
今天是版本1.1.0-alpha07 :| - Tran Chien
是的,但 alpha06 是第一个版本中添加的,不确定这是否是您的意思。 - Arthur Kasparian

6

4

现在也在稳定版(1.1.0)中。 但他们从中删除了抽屉。 - goldensoju

4

2023年7月更新

M2的BottomSheetScaffold现在已经成为M3的一部分,作为实验性API

以下是一个使用示例

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.runtime.rememberCoroutineScope

val scope = rememberCoroutineScope()
val scaffoldState = rememberBottomSheetScaffoldState()

BottomSheetScaffold(
    scaffoldState = scaffoldState,
    sheetPeekHeight = 128.dp,
    sheetContent = {
    Box(
        Modifier
            .fillMaxWidth()
            .height(128.dp),
        contentAlignment = Alignment.Center
    ) {
        Text("Swipe up to expand sheet")
    }
    Column(
        Modifier
            .fillMaxWidth()
            .padding(64.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Sheet content")
        Spacer(Modifier.height(20.dp))
        Button(
            onClick = {
                scope.launch { scaffoldState.bottomSheetState.partialExpand() }
            }
        ) {
            Text("Click to collapse sheet")
        }
    }
}) { innerPadding ->
    Box(Modifier.padding(innerPadding)) {
        Text("Scaffold Content")
    }
}

或者,如果你喜欢的话,你可以像下面代码片段中所示,使用独立的ModelBottomSheet

https://github.com/android/snippets/blob/7322f58994e9ade5ffee8e49f956e4a97339f384/compose/snippets/src/main/java/com/example/compose/snippets/layouts/MaterialLayoutSnippets.kt#L362

导入:

import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.ModalBottomSheet

代码

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BottomSheetDemo(title: String, modifier: Modifier = Modifier) {
    ModalBottomSheet(onDismissRequest = { /* Executed when the sheet is dismissed */ }) {
        // Sheet content
    }

    val sheetState = rememberModalBottomSheetState()
    val scope = rememberCoroutineScope()
    var showBottomSheet by remember { mutableStateOf(false) }
    Scaffold(
        floatingActionButton = {
            ExtendedFloatingActionButton(
                text = { Text(title) },
                icon = { Icon(Icons.Filled.Add, contentDescription = "") },
                onClick = {
                    showBottomSheet = true
                }
            )
        }
    ) { contentPadding ->
        // Screen content
        Box(modifier = Modifier.padding(contentPadding)) { /* ... */ }

        if (showBottomSheet) {
            ModalBottomSheet(
                onDismissRequest = {
                    showBottomSheet = false
                },
                sheetState = sheetState
            ) {
                // Sheet content
                Button(onClick = {
                    scope.launch { sheetState.hide() }.invokeOnCompletion {
                        if (!sheetState.isVisible) {
                            showBottomSheet = false
                        }
                    }
                }) {
                    Text("Hide bottom sheet")
                }
            }
        }
    }
}

谢谢Hitesh,直到现在我都不知道有一个ModalBottomSheet和一个BottomSheetScaffold。感谢您在Material 3上发布关于这两个组件的内容,我们明白了,真希望我能给你点赞*2。 - Tonnie

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接