Jetpack Compose 学习:掌握 ModalBottomSheet(底部弹窗)

ModalBottomSheet 是 Material Design 中一个非常实用的组件,用于从屏幕底部向上滑动显示一个模态化的内容面板(如菜单、选项、详情)。在 Jetpack Compose 中,实现它变得异常简洁优雅。本文将带你深入理解其用法。

核心概念

  1. 模态化 (Modal): 当底部弹窗显示时,它会覆盖在屏幕主要内容之上,并阻止用户与底层内容的交互(通常底层内容会变暗),直到用户关闭弹窗。

  2. 状态驱动: Compose 的核心是状态。ModalBottomSheet 的显示、隐藏、展开、折叠都由状态(ModalBottomSheetState)控制。

  3. 手势支持: 用户通常可以通过向下滑动来关闭弹窗,也可以通过拖动来调整弹窗的高度(如果支持部分展开)。

关键组件与 API

  1. ModalBottomSheetState

    • 这是控制弹窗的核心状态对象。

    • 使用 rememberModalBottomSheetState() 创建并记住状态:

      kotlin

      val sheetState = rememberModalBottomSheetState()
  2. ModalBottomSheet

    • 这是实际的弹窗容器组件。

    • 需要传入 sheetState 和定义弹窗内容的 sheetContent lambda。

    • 通常包裹在需要被弹窗覆盖的界面内容之外。

  3. ModalBottomSheetValue

    • 表示弹窗的当前状态值:

      • Hidden: 完全隐藏(不可见)。

      • Expanded: 完全展开(占据最大允许高度)。

      • PartiallyExpanded: 部分展开(占据预设的部分高度)。(注意:此状态需要显式启用)

  4. rememberModalBottomSheetState()

    • 创建并记住 ModalBottomSheetState

    • 常用参数:

      • skipPartiallyExpanded: Boolean = false: 如果设为 true,则弹窗在显示时会直接跳过 PartiallyExpanded 状态进入 Expanded 状态,关闭时也直接隐藏。用户也无法通过手势将其停留在部分展开状态。这简化了只使用全屏或全关两种状态的场景。

      • initialValue: ModalBottomSheetValue = ModalBottomSheetValue.Hidden: 初始状态。

基本实现步骤

kotlin

@OptIn(ExperimentalMaterial3Api::class) // ModalBottomSheet 在 Material3 中可能仍处于实验阶段
@Composable
fun MyScreenWithBottomSheet() {
    // 1. 创建并记住状态 (默认跳过部分展开)
    val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)

    // 2. 使用 ModalBottomSheet 包裹你的主屏幕内容
    ModalBottomSheet(
        onDismissRequest = {
            // 当用户点击弹窗外部或系统返回键时触发
            // 通常在这里隐藏弹窗
            scope.launch { sheetState.hide() }
        },
        sheetState = sheetState, // 传入状态
    ) {
        // 3. 这里是你的主屏幕内容 (被弹窗覆盖的内容)
        Scaffold(
            topBar = { /* ... */ },
            floatingActionButton = {
                // 4. 触发显示弹窗的按钮 (示例)
                ExtendedFloatingActionButton(
                    onClick = {
                        // 协程作用域 (例如从 rememberCoroutineScope 获取)
                        scope.launch {
                            if (sheetState.isVisible) {
                                sheetState.hide()
                            } else {
                                sheetState.show()
                            }
                        }
                    }
                ) {
                    Text("Show Sheet")
                }
            }
        ) { innerPadding ->
            // 主内容区
            Box(
                modifier = Modifier
                    .padding(innerPadding)
                    .fillMaxSize(),
                contentAlignment = Center
            ) {
                Text("Main Screen Content")
            }
        }
    } // ModalBottomSheet 的 sheetContent 参数在下一个代码块

    // 5. 定义弹窗自身的内容 (sheetContent) - 这实际上属于 ModalBottomSheet 的参数!
    // 注意:实际项目中,sheetContent 应该作为 ModalBottomSheet 的一个参数传入。
    // 这里为了结构清晰分开写,但在同一个 Composable 函数内,你需要将其内联到 ModalBottomSheet 调用里。
    // 正确写法如下 (整合到上面的 ModalBottomSheet 调用中):
    /*
    ModalBottomSheet(
        onDismissRequest = { ... },
        sheetState = sheetState,
        sheetContent = { // 这里是 sheetContent lambda
            // 弹窗内部内容
            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(16.dp),
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Text("Settings", style = MaterialTheme.typography.headlineMedium)
                Spacer(Modifier.height(16.dp))
                // 选项项
                ListItem(
                    headlineContent = { Text("Account") },
                    leadingContent = { Icon(Icons.Default.Person, null) },
                    onClick = { /* 处理点击 */ }
                )
                ListItem(
                    headlineContent = { Text("Notifications") },
                    leadingContent = { Icon(Icons.Default.Notifications, null) },
                    onClick = { /* 处理点击 */ }
                )
                ListItem(
                    headlineContent = { Text("Privacy") },
                    leadingContent = { Icon(Icons.Default.Lock, null) },
                    onClick = { /* 处理点击 */ }
                )
                Spacer(Modifier.height(24.dp))
                Button(
                    onClick = { scope.launch { sheetState.hide() } },
                    modifier = Modifier.fillMaxWidth()
                ) {
                    Text("Close")
                }
            }
        }
    ) {
        ... // 主屏幕内容
    }
    */
}

// 补充:获取协程作用域 (通常在可组合项顶部获取)
val scope = rememberCoroutineScope()

进阶用法与技巧

  1. 启用部分展开 (PartiallyExpanded):

    • 创建状态时设置 skipPartiallyExpanded = false

    • 在 sheetContent 中,确保内容高度可以支持部分展开和完全展开两种状态。Compose 会自动处理手势拖动。

    • 可以通过 sheetState.currentValue 或 sheetState.targetValue 监听当前状态变化。

  2. 监听状态变化:

    kotlin

    // 监听目标值变化
    LaunchedEffect(sheetState.targetValue) {
        if (sheetState.targetValue == ModalBottomSheetValue.Hidden) {
            // 弹窗即将隐藏/已隐藏
        } else if (sheetState.targetValue == ModalBottomSheetValue.Expanded) {
            // 弹窗即将完全展开/已完全展开
        }
    }
    // 或者监听当前值 snapshotFlow
    LaunchedEffect(Unit) {
        snapshotFlow { sheetState.currentValue }.collect { currentValue ->
            // 处理当前值变化
        }
    }
  3. 自定义样式:

    • ModalBottomSheet 提供了一些参数来自外观:

      • sheetShape: Shape: 设置弹窗顶部的圆角形状。

      • containerColor: Color: 弹窗的背景色。

      • dragHandle: @Composable (() -> Unit)?: 自定义顶部用于指示可拖动的“手柄”组件(通常是一个横条),设为 null 可隐藏默认手柄。

      • scrimColor: Color: 弹窗显示时底层内容的遮罩层颜色。

  4. BottomSheetScaffold (Material3):

    • Material3 提供了 BottomSheetScaffold,它整合了 Scaffold 和 ModalBottomSheet,提供更标准的布局结构(包含 TopAppBar, BottomBar, FloatingActionButton, Snackbar 等与底部弹窗的协调)。

    • 用法类似,也需要 sheetState 和 sheetContent,并直接在 sheetContent 参数中定义弹窗内容,在 content 参数中定义主内容(这个主内容会被包裹在 Scaffold 中)。

注意事项

  1. 协程作用域 (CoroutineScope): 调用 sheetState.show() 和 sheetState.hide() 是挂起函数,必须在协程中执行(通常使用 rememberCoroutineScope 获取的作用域)。

  2. 状态重置: 当包含 ModalBottomSheet 的可组合项退出组合时,sheetState 会被重置。如果需要在配置变更(如旋转)后保持状态,考虑使用 rememberSaveable 配合自定义 Saver(ModalBottomSheetState 通常有配套的 Saver)。

  3. Material2 vs Material3: API 可能因使用的 Material 版本(androidx.compose.material vs androidx.compose.material3)略有不同。本文主要基于 Material3 (androidx.compose.material3),Material2 中的类似组件是 ModalDrawer 用于抽屉,底部弹窗通常直接组合状态和 BottomSheetScaffold (Material2) 或自定义布局实现。优先使用 Material3。

  4. 内容高度: 弹窗的内容高度决定了其最大可展开高度。确保你的 sheetContent 有合适的约束(常用 Modifier.fillMaxHeight(fraction) 或 verticalScroll)。

总结

Jetpack Compose 的 ModalBottomSheet 通过声明式的 API 和状态管理,让实现底部弹窗交互变得清晰而强大。掌握 ModalBottomSheetState 的控制、理解不同状态值(HiddenPartiallyExpandedExpanded)、利用协程进行显示/隐藏操作,并学会监听状态变化和自定义外观,你就能轻松地在你的 Compose 应用中集成这个重要的交互模式。无论是简单的菜单还是复杂的内容面板,ModalBottomSheet 都是提升用户体验的利器。

你可能感兴趣的:(android)