Jetpack Compose 中的 ModalBottomSheet 是一个非常有用的 UI 组件,它允许在屏幕底部弹出一个可交互的面板,通常用于显示更多的选项、内容或者操作。ModalBottomSheet 是对传统 Android 中底部弹窗的 Compose 实现,它可以提供比传统对话框更灵活的设计和交互体验。

下面是关于 ModalBottomSheet 的详细学习笔记,包括基本的实现方法、常见用法以及一些高级技巧。

1. 基本概念

ModalBottomSheet 是一个可以滑动的底部弹窗,它提供了用户和应用之间交互的方式。它通常用于展示应用中不需要占据整个屏幕,但需要用户关注的内容。用户可以通过滑动底部弹窗来关闭它,或者通过点击某个选项来执行操作。

在 Jetpack Compose 中,ModalBottomSheet 和 BottomSheetScaffold 配合使用,提供了灵活的底部弹窗实现。

2. 使用 ModalBottomSheet

2.1 基本实现

我们首先来看一个最简单的实现:在按钮点击时弹出一个底部弹窗。

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.ModalBottomSheetLayout
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties

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

@Composable
fun ModalBottomSheetExample() {
    // 创建一个 ModalBottomSheet 的状态
    val sheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)

    // 创建一个按钮,用于打开底部弹窗
    Button(onClick = {
        // 当点击按钮时,底部弹窗展开
        sheetState.show()
    }) {
        Text(text = "Show Bottom Sheet")
    }

    // ModalBottomSheet 布局
    ModalBottomSheetLayout(
        sheetState = sheetState,
        sheetContent = {
            // 底部弹窗的内容
            Text(text = "This is a Modal Bottom Sheet", modifier = Modifier.fillMaxSize(), textAlign = TextAlign.Center)
        },
        content = {
            // 主界面的内容
            Text(
                text = "This is the main content",
                modifier = Modifier
                    .fillMaxSize()
                    .wrapContentSize(Alignment.Center)
            )
        }
    )
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    ModalBottomSheetExample()
}

解释:

  • rememberModalBottomSheetState:用于管理底部弹窗的状态(显示或隐藏)。
  • ModalBottomSheetLayout:包含了底部弹窗的布局,sheetState 控制弹窗的状态,sheetContent 是底部弹窗的内容。
  • Button:点击按钮后弹出底部弹窗。

3. 常见属性

3.1 sheetState

sheetState 用于控制弹窗的当前状态。它可以是以下几种状态之一:

  • ModalBottomSheetValue.Hidden:隐藏状态,底部弹窗不显示。
  • ModalBottomSheetValue.Expanded:扩展状态,底部弹窗完全显示。
  • ModalBottomSheetValue.HalfExpanded:半扩展状态,底部弹窗只显示部分内容。

可以通过 sheetState.show() 和 sheetState.hide() 方法来控制弹窗的显示与隐藏。

3.2 sheetContent

sheetContent 用于定义弹窗的内容,可以是任何 Compose UI 组件,如文本、按钮、图片等。

3.3 content

content 是弹窗之外的主界面内容,它通常包含了应用的主要 UI 元素。

3.4 onDismissRequest

onDismissRequest 用于定义当用户尝试关闭底部弹窗时的行为。它通常与用户点击弹窗外部区域或按下“返回”按钮时触发。

ModalBottomSheetLayout(
    sheetState = sheetState,
    sheetContent = { /* content */ },
    onDismissRequest = {
        // 底部弹窗关闭时的处理逻辑
        sheetState.hide()
    },
    content = { /* main content */ }
)

4. 高级用法

4.1 动态控制底部弹窗的状态

你可以通过动态控制 sheetState 来实现不同的交互效果,例如根据用户的操作来更改弹窗的状态。

@Composable
fun DynamicSheetExample() {
    val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)

    // 状态变化时的操作
    LaunchedEffect(sheetState.isVisible) {
        if (sheetState.isVisible) {
            // 执行弹窗显示时的操作
        } else {
            // 执行弹窗隐藏时的操作
        }
    }

    ModalBottomSheetLayout(
        sheetState = sheetState,
        sheetContent = {
            Text("This is a dynamic sheet")
        },
        content = {
            Button(onClick = { sheetState.show() }) {
                Text("Show Modal Sheet")
            }
        }
    )
}

4.2 滑动关闭弹窗

你可以启用 ModalBottomSheet 的滑动关闭功能,允许用户通过拖动手势来关闭弹窗。

val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden, confirmStateChange = { true })
ModalBottomSheetLayout(
    sheetState = sheetState,
    sheetContent = {
        Text("Swipe down to dismiss")
    },
    content = {
        Button(onClick = { sheetState.show() }) {
            Text("Show Modal Bottom Sheet")
        }
    }
)

4.3 使用 ModalBottomSheet 配合 Scaffold 实现更复杂的布局

ModalBottomSheet 常常配合 Scaffold 使用,Scaffold 是 Compose 中用于构建常见 UI 结构的组件,支持顶栏、底栏、浮动按钮等。

@Composable
fun ModalSheetWithScaffold() {
    val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)

    Scaffold(
        topBar = {
            TopAppBar(title = { Text("Modal Bottom Sheet") })
        },
        floatingActionButton = {
            FloatingActionButton(onClick = { sheetState.show() }) {
                Icon(Icons.Default.Add, contentDescription = null)
            }
        },
        content = {
            ModalBottomSheetLayout(
                sheetState = sheetState,
                sheetContent = {
                    Text("This is a bottom sheet inside Scaffold")
                },
                content = {
                    Text("Main content goes here")
                }
            )
        }
    )
}

5. 常见问题

5.1 底部弹窗的状态变化

  • 如果你想在底部弹窗的显示和隐藏时进行特定操作,可以使用 LaunchedEffect 来监听 sheetState 的变化。
LaunchedEffect(sheetState.isVisible) {
    if (sheetState.isVisible) {
        // 弹窗显示时
    } else {
        // 弹窗隐藏时
    }
}

5.2 底部弹窗关闭时自动恢复焦点

当底部弹窗关闭时,焦点可能会丢失。你可以确保焦点恢复到上次的交互控件。

LaunchedEffect(sheetState.isVisible) {
    if (!sheetState.isVisible) {
        // 恢复焦点到某个控件
        // 例如,恢复到按钮或上一个焦点位置
    }
}

总结

  • ModalBottomSheet 提供了一种简单且灵活的方式来实现底部弹窗,适用于展示额外的选项或内容。
  • 可以通过控制 sheetState 来显示或隐藏弹窗,并通过 sheetContent 自定义弹窗的内容。
  • 通过 LaunchedEffect 等机制可以在弹窗状态变化时执行自定义操作。
  • 配合 Scaffold 使用可以实现更复杂的 UI 结构。

通过理解这些基本和高级技巧,你可以更加灵活地使用 ModalBottomSheet 来增强应用的交互体验。