
各位 Android 刘德华,你们有没有碰到过这个问题?
如果我们使用 FloatingActionButton 去作为 App 暴露给用户的主要操作按钮,会出现功能不够用的情况!

毕竟,只有一个按钮。
近期,Material 3 Compose 新提供了一组与 FloatingActionButtonMenu 相关的 API,它允许我们通过一个可展开的 FAB(FloatingActionButton 的缩写),向用户呈现多个菜单操作。这样我们就可以在菜单中添加多个功能。
废话少说,我们立即开始!
FloatingActionButtonMenu 是什么
FloatingActionButtonMenu 允许我们在屏幕上展示 2 到 6 个相关操作,让用户在不牺牲界面空间的前提下访问一组相关动作。
从组成上看,这个 composable 由三个部分构成:
FloatingActionButtonMenu:容器 composable,用来承载嵌套子元素ToggleFloatingActionButton:也就是FAB本身,用来构建用户最先看到的那个按钮FloatingActionButtonMenuItem:用于显示可点击菜单项的 composable,菜单中会由多个该组件实例组成
三者组合在一起,形成了一个解耦的 API,用来同时构建浮动操作按钮及其展示的菜单项。
准备工作
请确保你已经使用了最新的 compose BOM ,同时修改 material3 的版本 :
gradle
androidx.compose.material3:material3:1.5.0-alpha17
如果你想使用文中 FloatingActionButtonMenu,需要使用包含它们的 material3 1.5.0-alpha17 或更高版本。
由于它还处于 alpha 阶段,后续版本中 API 细节仍有可能调整。
开始
FloatingActionButtonMenu 是一个可以创建可展开悬浮操作按钮的组件,在触发时会显示额外的菜单项。
这与我们熟悉的标准 FAB 用法不同,因为它让我们能够向用户提供多个主操作入口。
kotlin
@Composable
fun FloatingActionButtonMenu(
expanded: Boolean,
button: @Composable () -> Unit,
modifier: Modifier = Modifier,
horizontalAlignment: Alignment.Horizontal = Alignment.End,
content: @Composable FloatingActionButtonMenuScope.() -> Unit,
)
借助这一能力,我们就能在单一主操作之外向用户暴露多个动作。
例如,我们不必只提供一个 "创建" 操作,而是可以提供多个动作,比如 "创建便签"、"创建任务"、"创建事件" 等。
这样既能避免在界面上堆满多个按钮,也能把这些动作集中到一个位置,只在用户需要时再展开显示。
我们从一个简单的 FloatingActionButtonMenu 开始,并传入一个展开状态引用。
kotlin
var fabMenuExpanded by remember {
mutableStateOf(false)
}
FloatingActionButtonMenu(
expanded = fabMenuExpanded,
button = {
},
content = {
}
)
FloatingActionButtonMenu 会利用这个状态来控制菜单项的可见性与动画效果。我们期望通过这个值来控制菜单在显示与隐藏之间切换。
不过现在直接运行这段代码,你还看不到效果。
我们还需要实现 button 和 content 两个部分。
我们先从 button 说起。这个 composable 会作为整体结构中的 FAB 显示出来,也就是负责触发菜单显示的那个按钮。
这里使用的是 ToggleFloatingActionButton;它的形态与标准 FloatingActionButton 相似,关键差别在于它支持 checked 状态。
kotlin
@Composable
fun ToggleFloatingActionButton(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
containerColor: (Float) -> Color = ToggleFloatingActionButtonDefaults.containerColor(),
contentAlignment: Alignment = Alignment.TopEnd,
containerSize: (Float) -> Dp = ToggleFloatingActionButtonDefaults.containerSize(),
containerCornerRadius: (Float) -> Dp =
ToggleFloatingActionButtonDefaults.containerCornerRadius(),
content: @Composable ToggleFloatingActionButtonScope.() -> Unit,
)
这个 checked 状态让我们可以表达当前 FAB 是否处于展开状态,也因此能表示该按钮的菜单是否应该显示。
下面来看一个 ToggleFloatingActionButton 的示例:
Kotlin
ToggleFloatingActionButton(
checked = fabMenuExpanded,
onCheckedChange = { fabMenuExpanded = !fabMenuExpanded },
) {
Icon(
painter = rememberVectorPainter(if (fabMenuExpanded) Icons.Filled.Close else Icons.Filled.Add),
contentDescription = null
)
}
这里,我们使用前面定义的 fabMenuExpanded 状态值作为 checked 状态。
同时,在 onCheckedChange lambda 被触发时,也会切换 fabMenuExpanded 的值。
按钮内容则很简单,只是一个 Icon 组件,并根据菜单当前是否展开来设置对应的 painter 引用。
有了这部分之后,我们就可以把它放进 FloatingActionButtonMenu 的 button 槽位中。
Kotlin
FloatingActionButtonMenu(
expanded = fabMenuExpanded,
button = {
ToggleFloatingActionButton(...)
},
content = {
}
)
此时运行这段代码,你就会看到一个简单的切换效果:

因为图片的帧率问题,看起来不顺畅,但实际上效果很流畅。
至于菜单部分,每个菜单项都由 FloatingActionButtonMenuItem 来表示。
这个 composable 可以展示扁平风格的悬浮菜单项,它们会垂直堆叠在浮动操作按钮上方,并通过点击事件触发相应操作。
Kotlin
@Composable
fun FloatingActionButtonMenuScope.FloatingActionButtonMenuItem(
onClick: () -> Unit,
text: @Composable () -> Unit,
icon: @Composable () -> Unit,
modifier: Modifier = Modifier,
containerColor: Color = MaterialTheme.colorScheme.primaryContainer,
contentColor: Color = contentColorFor(containerColor),
)
我们可以先创建一个简化的 FloatingActionButtonMenuItem 实例:
Kotlin
FloatingActionButtonMenuItem(
onClick = { fabMenuExpanded = false },
icon = { Icon(Icons.Default.Create, contentDescription = null) },
text = { Text(text = "创建便签") },
)
这里我们提供了一些要显示在浮动菜单项中的 text,以及配套的 icon。
同时,我们还在 onClick lambda 中通过把 fabMenuExpanded 设为 false 来折叠菜单。
接着,我们会把 FloatingActionButtonMenuItem 放入 FloatingActionButtonMenu 的 content 块中。
通常我们还会有某种数据类型来表示菜单项,因此,用一个简单的循环来组合它们,大致会像下面这样:
Kotlin
FloatingActionButtonMenu(
expanded = fabMenuExpanded,
button = {
ToggleFloatingActionButton(...)
},
content = {
menuItems.forEach {
FloatingActionButtonMenuItem(
onClick = { fabMenuExpanded = false },
icon = { Icon(it.icon, contentDescription = null) },
text = { Text(text = it.label) },
)
}
}
)
当然,在真实项目里,我们通常也会在 FloatingActionButtonMenuItem 的 onClick lambda 中触发某种实际操作,比如跳转到用户选中的页面。
我们给出全量代码:
kotlin
var fabMenuExpanded by remember { mutableStateOf(false) }
FloatingActionButtonMenu(
modifier = Modifier.align(Alignment.BottomEnd),
expanded = fabMenuExpanded,
button = {
ToggleFloatingActionButton(
checked = fabMenuExpanded,
onCheckedChange = { fabMenuExpanded = !fabMenuExpanded }
) {
Icon(
painter = rememberVectorPainter(if (fabMenuExpanded) Icons.Filled.Close else Icons.Filled.Add),
contentDescription = null
)
}
},
content = {
FloatingActionButtonMenuItem(
onClick = { fabMenuExpanded = false },
icon = { Icon(Icons.Default.Create, contentDescription = null) },
text = { Text(text = "创建便签") }
)
FloatingActionButtonMenuItem(
onClick = { fabMenuExpanded = false },
icon = { Icon(Icons.Default.DateRange, contentDescription = null) },
text = { Text(text = "创建任务") }
)
FloatingActionButtonMenuItem(
onClick = { fabMenuExpanded = false },
icon = { Icon(Icons.Default.Notifications, contentDescription = null) },
text = { Text(text = "创建事件") }
)
FloatingActionButtonMenuItem(
onClick = { fabMenuExpanded = false },
icon = { Icon(Icons.Default.Face, contentDescription = null) },
text = { Text(text = "关注RockByte公众号") }
)
}
)
效果如下:

通过上面的示例,我们就得到了一套简单的 FloatingActionButtonMenu 实现,可以在浮动操作按钮上方向用户展示一组选项操作。
需要记住的是,这些操作应该彼此相关,并且要与浮动操作按钮本身的上下文保持一致;同时,2 到 6 个菜单项通常是比较理想的数量。
一点想法
FloatingActionButtonMenu 是 Material 3 组件库中一个很不错的补充,它让我们更容易在 Compose 中构建这类交互,也能更顺畅地把 View 系统里的既有实现迁移过来。
不过从产品设计的角度看,这个组件也不是"只要功能多就往里塞" 的万能解法。它更适合承载一组强相关、且用户能够快速理解的操作;如果菜单项之间缺少明确关联,或者用户需要频繁使用其中某一个动作,那把它们全都塞进可展开菜单里,反而会增加点击路径和理解成本。
所以相比"能不能用",我更倾向于先判断"应不应该用"。如果你的页面确实存在一个主操作,以及 2 到 6 个围绕这个主操作展开的次级动作,那么 FloatingActionButtonMenu 会是一个很自然的选择;但如果它只是为了把更多按钮藏起来,那最终带来的可能不是更好的体验,而只是更隐蔽的复杂度。