介绍一个新的 Compose 控件 — 浮动菜单

各位 Android 刘德华,你们有没有碰到过这个问题?

如果我们使用 FloatingActionButton 去作为 App 暴露给用户的主要操作按钮,会出现功能不够用的情况!

毕竟,只有一个按钮。

近期,Material 3 Compose 新提供了一组与 FloatingActionButtonMenu 相关的 API,它允许我们通过一个可展开的 FABFloatingActionButton 的缩写),向用户呈现多个菜单操作。这样我们就可以在菜单中添加多个功能。

废话少说,我们立即开始!

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 会利用这个状态来控制菜单项的可见性与动画效果。我们期望通过这个值来控制菜单在显示与隐藏之间切换。

不过现在直接运行这段代码,你还看不到效果。

我们还需要实现 buttoncontent 两个部分。

我们先从 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 引用。

有了这部分之后,我们就可以把它放进 FloatingActionButtonMenubutton 槽位中。

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 放入 FloatingActionButtonMenucontent 块中。

通常我们还会有某种数据类型来表示菜单项,因此,用一个简单的循环来组合它们,大致会像下面这样:

Kotlin 复制代码
FloatingActionButtonMenu(
  expanded = fabMenuExpanded,
  button = {
    ToggleFloatingActionButton(...)
  },
  content = {
    menuItems.forEach {
      FloatingActionButtonMenuItem(
        onClick = { fabMenuExpanded = false },
        icon = { Icon(it.icon, contentDescription = null) },
        text = { Text(text = it.label) },
      )
    }
  }
)

当然,在真实项目里,我们通常也会在 FloatingActionButtonMenuItemonClick 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 会是一个很自然的选择;但如果它只是为了把更多按钮藏起来,那最终带来的可能不是更好的体验,而只是更隐蔽的复杂度。

相关推荐
空中海2 小时前
第二章:UI 开发——View 系统与 Jetpack Compose
android·ui
空中海2 小时前
安卓 第五章:网络与数据持久化
android·网络
fengci.2 小时前
php反序列化(复习)(第五章)
android·开发语言·学习·php
美狐美颜sdk2 小时前
视频平台如何实现实时美颜?Android/iOS直播APP美颜SDK接入指南
android·前端·人工智能·ios·音视频·第三方美颜sdk·视频美颜sdk
XiaoLeisj2 小时前
Android 短视频项目实战:从登录态回流、设置页动作分发到缓存清理、协议页复用与密码重置的完整实现个人中心与设置模块
android·mvvm·webview·arouter
CYRUS_STUDIO11 小时前
Frida 源码编译全流程:自己动手编译 frida-server
android·逆向
冬奇Lab12 小时前
音视频同步与渲染:PTS、VSYNC 与 SurfaceFlinger 的协作之道
android·音视频开发
Grackers16 小时前
Android Perfetto 系列 9:CPU 信息解读
android