Flutter for OpenHarmony 实战:底部弹出菜单(ModalBottomSheet)
是Google开发的开源UI工具包,支持用一套代码构建和六大平台应用,实现"一次编写,多处运行"。是由开放原子开源基金会运营的分布式操作系统,为全场景智能设备提供统一底座,具有多设备支持、模块化设计、分布式能力和开源开放等特性。
前言
Flutter是Google开发的开源UI工具包,支持用一套代码构建iOS、Android、Web、Windows、macOS和Linux六大平台应用,实现"一次编写,多处运行"。
OpenHarmony是由开放原子开源基金会运营的分布式操作系统,为全场景智能设备提供统一底座,具有多设备支持、模块化设计、分布式能力和开源开放等特性。
Flutter for OpenHarmony技术方案使开发者能够:
- 复用Flutter现有代码(Skia渲染引擎、热重载、丰富组件库)
- 快速构建符合OpenHarmony规范的UI
- 降低多端开发成本
- 利用Dart生态插件资源加速生态建设
先看效果

在鸿蒙真机 上模拟器上成功运行后的效果

新增的 UI 组件与弹窗工具做成了三个可复用单元:一个负责背景氛围(GradientBackground)、一个负责按钮交互手感(CoolButton)、一个负责把底部弹出菜单封装成稳定 API(bottom_sheet_utils)。你在业务里通常只需要关心两件事:把组件放到合适的位置,以及把参数(尤其是回调和数据)传对,剩下的动效、样式一致性就交给组件本身。
- GradientBackground:给页面铺一个干净的渐变底色 + 自动处理安全区(见 [gradient_background.dart](file:///d:/demo3/lib/widgets/gradient_background.dart))
- CoolButton:按钮按下去有反馈,支持 Loading 防连点(见 [cool_button.dart](file:///d:/demo3/lib/widgets/cool_button.dart))
- bottom_sheet_utils:把
showModalBottomSheet包成稳定 API,列表/网格两套样式都能复用(见 [bottom_sheet_utils.dart](file:///d:/demo3/lib/widgets/bottom_sheet_utils.dart))
业务里大多只要做两件事:摆对位置、传对参数(尤其是回调和数据源)。动画、圆角、阴影、水波纹这些“手感细节”,交给组件兜底就好。
目录
GradientBackground
文件位置
[gradient_background.dart](file:///d:/demo3/lib/widgets/gradient_background.dart)
开发要点
GradientBackground 就一个任务:铺底。它不是页面模板,不会帮你排版,更不会夹带业务逻辑。你把它放在 Scaffold.body 最外层:先把渐变铺满整屏,再用 SafeArea 把内容托起来,避免刘海/状态栏/底部手势条把 UI “啃掉一口”。
我把这层抽出来,纯粹是为了省心:不想每个页面都重复写 Container + BoxDecoration + SafeArea,更不想哪天要调个渐变颜色还得全局搜一圈。
- 组件职责单一:只负责“背景渐变 + 安全区”,不掺业务逻辑
- 使用
Container + LinearGradient实现柔和底色 SafeArea包裹child,避免内容贴边(刘海/状态栏/手势条)
对外参数
child:内容区域(必传)
使用方式
用法很简单:把它当作 Scaffold.body 的最外层。真正的布局(Center / Column / ListView)都放到 child 里。页面需要滚动也没关系:背景在外面,滚动在里面,背景就不会“滚没了”。
import 'package:flutter/material.dart';
import '../widgets/gradient_background.dart';
class ExamplePage extends StatelessWidget {
const ExamplePage({super.key});
Widget build(BuildContext context) {
return Scaffold(
body: GradientBackground(
child: Center(
child: Text(
'Hello',
style: Theme.of(context).textTheme.headlineMedium,
),
),
),
);
}
}
CoolButton
文件位置
[cool_button.dart](file:///d:/demo3/lib/widgets/cool_button.dart)
开发要点
CoolButton 解决的不是“好看”,而是“好按”。有些 Demo 的按钮看着像按钮,但按下去没确认感,用户会下意识再点两下,然后你就收获了两个弹层、三次提交、四个请求……场面很热闹。
这个按钮里我做了三件小事,合起来手感就稳:
- 用
InkWell做水波纹反馈(Material兜住 ripple,不会丢) - 用
onHighlightChanged驱动一个 120ms 的缩放动画(1.0 → 0.92),按下像“轻轻压一下” isLoading时禁用点击并显示进度圈,专治连点
对外参数
text:按钮文案(必传)onPressed:点击回调(必传)isLoading:加载态(可选,默认 false)
使用方式
建议的用法是:Loading 由页面状态控制,按钮只负责展示。比如点击后先把 _loading = true,弹层返回/异步结束再设回 false。用户能明确感觉到:我点了,而且系统真的收到了。
CoolButton(
text: '操作列表样式',
isLoading: _loading,
onPressed: _openActionSheet,
)
BottomSheet 工具封装(bottom_sheet_utils)
文件位置
[bottom_sheet_utils.dart](file:///d:/demo3/lib/widgets/bottom_sheet_utils.dart)
核心 API
这部分是项目里复用价值最高的东西:我把 showModalBottomSheet 的“配置项 + 样式 + 结构”揉成了两个函数。页面侧只需要给 title 和 options,就能弹出一个手感一致的底部菜单。
为了让返回值更贴近业务,我把选项做成了泛型 SheetOption<T>。你可以返回 String、枚举,甚至是业务对象(比如 ActionType 或 ShareTarget)。调用结果是 T?:点了某项就有值;点遮罩/下滑关闭就是 null,业务层自己决定要不要当作“取消”处理。
SheetOption<T>
label:显示文案icon:图标value:返回值(泛型 T)
showActionBottomSheet<T>()
适合“列表型操作”(复制/分享/删除…),返回用户选择的 T?。
showGridBottomSheet<T>()
适合“网格型入口”(图片/视频/位置…),返回用户选择的 T?。
开发要点
实现上我刻意走的是“外层透明、内层自绘”的路线:
- 外层
showModalBottomSheet:只管弹出来,背景设透明,遮罩颜色统一 - 内层
_AnimatedSheet:圆角、阴影、上滑淡入、SafeArea全在这一层收口
这样做的好处很实际:你以后再加第三种样式(比如带搜索的选择器)也不容易把边界/手感做歪,直接复用 _AnimatedSheet 就行。实现里顺手还做了几件“统一口径”的事:
isScrollControlled: true:更容易扩展高度策略barrierColor:遮罩用轻黑,聚焦弹层但不压迫- 内容形态分离:列表内容
_ActionSheetContent,网格内容_GridSheetContent
使用方式
用法建议就两条,很实用:
options尽量写成const(少分配对象,弹层打开更干脆)- 拿到
result先判断null(用户取消时别继续往下跑逻辑)
标题用短一点更像系统弹层;选项文字用动词开头(复制/分享/删除)也更“顺眼”。
1)列表操作(Action Sheet)
final result = await showActionBottomSheet<String>(
context: context,
title: '选择操作',
options: const [
SheetOption(label: '复制', icon: Icons.copy_rounded, value: 'copy'),
SheetOption(label: '分享', icon: Icons.share_rounded, value: 'share'),
SheetOption(label: '收藏', icon: Icons.bookmark_rounded, value: 'fav'),
SheetOption(label: '删除', icon: Icons.delete_rounded, value: 'delete'),
],
);
2)网格快捷(Grid Sheet)
final result = await showGridBottomSheet<String>(
context: context,
title: '快速操作',
options: const [
SheetOption(label: '图片', icon: Icons.image_rounded, value: 'image'),
SheetOption(label: '视频', icon: Icons.videocam_rounded, value: 'video'),
SheetOption(label: '位置', icon: Icons.location_on_rounded, value: 'location'),
SheetOption(label: '文件', icon: Icons.insert_drive_file_rounded, value: 'file'),
],
);
扩展建议
如果你要把它从“好用的工具”推进到“业务级组件”,我建议按这个顺序扩展:
- 样式分层:危险操作(删除/移除)用红色或弱化背景提示风险
- 结构增强:支持分组、加副标题、加分割线
- 交互增强:危险操作二次确认;网格入口加搜索/最近使用
不管怎么扩展,我还是建议坚持“数据驱动”:让 option 自己表达差异,UI 层少写 if/else,后期维护会轻松很多。
- 想要“危险操作”更醒目:为
SheetOption增加可选的颜色/样式字段(例如 destructive) - 想做“分组/分割线”:在 options 里引入分组模型,或提供
headerBuilder - 想限制最大高度:给内容外再包一层
ConstrainedBox,避免超高内容顶满屏幕
组合示例:在页面里一起用
下面就是项目演示页的组合方式:背景负责气氛,按钮负责手感,BottomSheet 工具负责“弹层一致性”。
在真实业务里,我一般会把“按钮 Loading”和“弹层调用”连成一条线:点下去先锁一下按钮(防连点),弹层返回后再解锁,然后根据返回值走不同分支。用户会觉得这套交互很“听话”:点了就有回应,选了就有结果。
return Scaffold(
body: GradientBackground(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CoolButton(text: '操作列表样式', onPressed: _openActionSheet, isLoading: _loading),
const SizedBox(height: 12),
CoolButton(text: '网格快捷样式', onPressed: _openGridSheet, isLoading: _loading),
],
),
),
),
);
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)