
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
🎯 欢迎来到 Flutter for OpenHarmony 社区!本文将深入讲解 Flutter 中 BottomSheet 底部面板的使用方法,带你从基础到精通,掌握这一常用的底部交互组件。
一、BottomSheet 组件概述
在 Flutter for OpenHarmony 应用开发中,BottomSheet(底部面板)是一种从屏幕底部滑出的面板组件,常用于展示菜单、选项、表单或详细信息。BottomSheet 遵循 Material Design 规范,提供流畅的动画效果和直观的交互体验。
📋 BottomSheet 类型
| 类型 | 说明 | 适用场景 |
|---|---|---|
Persistent |
持久型 | 一直显示在底部的面板 |
Modal |
模态型 | 点击后显示,点击遮罩关闭 |
💡 使用场景:BottomSheet 常用于底部菜单、分享选项、评论列表、设置面板、信息展示等场景。
二、BottomSheet 基础用法
BottomSheet 有两种主要的使用方式:模态底部面板和持久底部面板。理解它们的区别,选择合适的类型,是创建良好用户体验的关键。
2.1 showModalBottomSheet - 模态底部面板
showModalBottomSheet 是最常用的方式,显示一个模态的底部面板。模态意味着面板会覆盖当前界面,用户必须处理面板内容或关闭面板才能继续操作。
dart
ElevatedButton(
onPressed: () {
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return Container(
height: 200,
color: Colors.white,
child: const Center(
child: Text('这是一个底部面板'),
),
);
},
);
},
child: const Text('显示底部面板'),
)
模态面板的特点:
- 阻隔操作:面板显示时,背景会有半透明遮罩,阻止用户操作底层界面
- 自动关闭:点击遮罩区域可以关闭面板
- 流畅动画:带有从底部滑入的流畅动画效果
- 适用场景:菜单选择、分享选项、确认对话框等需要用户集中注意力的场景
2.2 showBottomSheet - 持久底部面板
showBottomSheet 在 Scaffold 中显示一个持久的底部面板。持久型面板会一直显示在屏幕底部,用户可以同时操作面板内容和底层界面。
dart
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
Scaffold(
key: _scaffoldKey,
body: Center(
child: ElevatedButton(
onPressed: () {
_scaffoldKey.currentState?.showBottomSheet(
(BuildContext context) {
return Container(
height: 150,
color: Colors.blue,
child: const Center(
child: Text('持久底部面板'),
),
);
},
);
},
child: const Text('显示持久底部面板'),
),
),
)
持久面板的特点:
- 非阻隔:不会阻止用户操作底层界面
- 手动关闭 :需要调用
Navigator.pop()或设置关闭按钮来关闭 - 多面板共存:可以同时显示多个持久面板
- 适用场景:工具栏、信息面板、播放控制等需要持续访问的功能
💡 选择建议:如果需要用户专注于面板内容,使用模态面板;如果面板是辅助功能,用户可能需要同时操作主界面,使用持久面板。
三、showModalBottomSheet 常用属性
showModalBottomSheet 提供了丰富的属性,让我们可以定制底部面板的外观和行为。
3.1 backgroundColor - 背景颜色
设置底部面板的背景颜色。
dart
showModalBottomSheet(
context: context,
backgroundColor: Colors.blue,
builder: (context) => Container(
height: 200,
child: const Center(child: Text('蓝色背景')),
),
)
设计建议:
- 通常使用白色或浅色背景,保持界面的清爽
- 可以根据应用的主题色选择合适的背景色
- 确保背景色与文字颜色有足够的对比度
3.2 shape - 形状设置
设置底部面板的形状,通常用于设置圆角,让面板看起来更加现代化和精致。
dart
showModalBottomSheet(
context: context,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
),
),
builder: (context) => Container(
height: 200,
child: const Center(child: Text('圆角面板')),
),
)
圆角设置技巧:
- 圆角半径通常设置在 16-24px 之间
- 只设置顶部圆角(
top),底部不需要圆角 - 圆角可以让面板与屏幕边缘自然过渡,提升视觉效果
dart
showModalBottomSheet(
context: context,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
),
),
builder: (context) => Container(
height: 200,
child: const Center(child: Text('圆角底部面板')),
),
)
3.3 isDismissible - 是否可关闭
控制是否可以通过点击遮罩层关闭底部面板。
dart
showModalBottomSheet(
context: context,
isDismissible: false, // 不可点击遮罩关闭
builder: (context) => Container(
height: 200,
child: Column(
children: [
const Text('必须点击按钮关闭'),
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text('关闭'),
),
],
),
),
)
3.4 enableDrag - 是否可拖动
控制是否可以通过拖动关闭底部面板。
dart
showModalBottomSheet(
context: context,
enableDrag: false, // 不可拖动关闭
builder: (context) => Container(
height: 200,
child: const Center(child: Text('不可拖动')),
),
)
3.5 isScrollControlled - 滚动控制
允许底部面板的高度超过屏幕高度,内容可滚动。
dart
showModalBottomSheet(
context: context,
isScrollControlled: true, // 启用滚动控制
builder: (context) => DraggableScrollableSheet(
initialChildSize: 0.5, // 初始高度为屏幕一半
minChildSize: 0.3, // 最小高度
maxChildSize: 0.9, // 最大高度
expand: false,
builder: (context, scrollController) {
return ListView.builder(
controller: scrollController,
itemCount: 50,
itemBuilder: (context, index) {
return ListTile(
title: Text('项目 $index'),
);
},
);
},
),
)
📊 showModalBottomSheet 属性速查表
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
context |
BuildContext | - | 上下文(必填) |
builder |
WidgetBuilder | - | 面板构建器(必填) |
backgroundColor |
Color? | - | 背景颜色 |
shape |
ShapeBorder? | - | 形状 |
isDismissible |
bool | true | 是否可点击遮罩关闭 |
enableDrag |
bool | true | 是否可拖动关闭 |
isScrollControlled |
bool | false | 是否启用滚动控制 |
四、DraggableScrollableSheet 可拖动面板
DraggableScrollableSheet 是一个可拖动的底部面板,用户可以通过拖动调整其高度。
4.1 基础用法
dart
Scaffold(
body: DraggableScrollableSheet(
initialChildSize: 0.3, // 初始高度为屏幕30%
minChildSize: 0.2, // 最小高度为屏幕20%
maxChildSize: 0.8, // 最大高度为屏幕80%
builder: (context, scrollController) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
),
),
child: ListView.builder(
controller: scrollController,
itemCount: 20,
itemBuilder: (context, index) {
return ListTile(title: Text('项目 $index'));
},
),
);
},
),
)
4.2 完整示例
dart
class DraggableSheetExample extends StatelessWidget {
const DraggableSheetExample({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('可拖动面板')),
body: Stack(
children: [
// 背景内容
Container(
color: Colors.grey[200],
child: const Center(
child: Text(
'拖动下方面板',
style: TextStyle(fontSize: 24),
),
),
),
// 可拖动面板
DraggableScrollableSheet(
initialChildSize: 0.3,
minChildSize: 0.15,
maxChildSize: 0.8,
builder: (context, scrollController) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(20),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
),
],
),
child: Column(
children: [
// 拖动指示器
Container(
margin: const EdgeInsets.symmetric(vertical: 12),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text(
'可拖动面板',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
const Divider(),
Expanded(
child: ListView.builder(
controller: scrollController,
itemCount: 30,
itemBuilder: (context, index) {
return ListTile(
leading: Icon(Icons.star, color: Colors.orange),
title: Text('项目 $index'),
);
},
),
),
],
),
);
},
),
],
),
);
}
}
五、实际应用场景
5.1 底部菜单
dart
ElevatedButton(
onPressed: () {
showModalBottomSheet(
context: context,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) {
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.camera_alt),
title: const Text('拍照'),
onTap: () {
Navigator.pop(context);
// 拍照逻辑
},
),
ListTile(
leading: const Icon(Icons.photo_library),
title: const Text('从相册选择'),
onTap: () {
Navigator.pop(context);
// 相册选择逻辑
},
),
ListTile(
leading: const Icon(Icons.videocam),
title: const Text('录制视频'),
onTap: () {
Navigator.pop(context);
// 录制视频逻辑
},
),
],
),
);
},
);
},
child: const Text('打开菜单'),
)
5.2 分享选项
dart
ElevatedButton(
onPressed: () {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Padding(
padding: EdgeInsets.all(16),
child: Text(
'分享到',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
childAspectRatio: 1,
),
itemCount: _shareOptions.length,
itemBuilder: (context, index) {
return Column(
children: [
CircleAvatar(
radius: 24,
backgroundColor: _shareOptions[index]['color'],
child: Icon(_shareOptions[index]['icon']),
),
const SizedBox(height: 8),
Text(_shareOptions[index]['name']),
],
);
},
),
const SizedBox(height: 16),
],
),
),
);
},
);
},
child: const Text('分享'),
)
5.3 评论列表
dart
ElevatedButton(
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) {
return DraggableScrollableSheet(
initialChildSize: 0.6,
minChildSize: 0.4,
maxChildSize: 0.95,
expand: false,
builder: (context, scrollController) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
),
),
child: Column(
children: [
// 拖动指示器
Container(
margin: const EdgeInsets.symmetric(vertical: 12),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'评论 (23)',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Icon(Icons.close),
],
),
),
const Divider(),
Expanded(
child: ListView.builder(
controller: scrollController,
itemCount: 20,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(
child: Text('${index + 1}'),
),
title: Text('用户 ${index + 1}'),
subtitle: Text('这是一条评论内容'),
);
},
),
),
],
),
);
},
);
},
);
},
child: const Text('查看评论'),
)
5.4 表单输入
dart
ElevatedButton(
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) {
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
),
),
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'添加评论',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
TextField(
maxLines: 4,
decoration: InputDecoration(
hintText: '输入评论内容',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('提交'),
),
],
),
),
);
},
);
},
child: const Text('添加评论'),
)
六、完整示例代码
下面是一个完整的 Flutter 应用示例,展示 BottomSheet 组件的各种用法。
dart
import 'package:flutter/material.dart';
void main() {
runApp(const BottomSheetDemo());
}
class BottomSheetDemo extends StatelessWidget {
const BottomSheetDemo({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'BottomSheet 组件演示',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.dark(
primary: const Color(0xFF6366F1),
secondary: const Color(0xFF8B5CF6),
surface: const Color(0xFF1E293B),
background: const Color(0xFF0F172A),
brightness: Brightness.dark,
),
useMaterial3: true,
),
home: const BottomSheetPage(),
);
}
}
class BottomSheetPage extends StatelessWidget {
const BottomSheetPage({super.key});
final List<Map<String, dynamic>> _shareOptions = const [
{'name': '微信', 'icon': Icons.chat, 'color': Colors.green},
{'name': '朋友圈', 'icon': Icons.group, 'color': Colors.green},
{'name': '微博', 'icon': Icons.public, 'color': Colors.orange},
{'name': 'QQ', 'icon': Icons.message, 'color': Colors.blue},
{'name': '复制链接', 'icon': Icons.link, 'color': Colors.grey},
{'name': '保存图片', 'icon': Icons.download, 'color': Colors.purple},
{'name': '举报', 'icon': Icons.report, 'color': Colors.red},
{'name': '更多', 'icon': Icons.more_horiz, 'color': Colors.grey},
];
void _showBasicBottomSheet(BuildContext context) {
showModalBottomSheet(
context: context,
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 4,
margin: const EdgeInsets.only(bottom: 20),
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
const Text(
'底部面板',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
const Text('这是一个基础的底部面板示例'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text('关闭'),
),
],
),
),
);
},
);
}
void _showMenuBottomSheet(BuildContext context) {
showModalBottomSheet(
context: context,
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) {
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 8),
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
ListTile(
leading: const Icon(Icons.camera_alt, color: Colors.blue),
title: const Text('拍照'),
onTap: () => Navigator.pop(context),
),
ListTile(
leading: const Icon(Icons.photo_library, color: Colors.green),
title: const Text('从相册选择'),
onTap: () => Navigator.pop(context),
),
ListTile(
leading: const Icon(Icons.videocam, color: Colors.purple),
title: const Text('录制视频'),
onTap: () => Navigator.pop(context),
),
],
),
);
},
);
}
void _showShareBottomSheet(BuildContext context) {
showModalBottomSheet(
context: context,
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'分享到',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
childAspectRatio: 1,
),
itemCount: _shareOptions.length,
itemBuilder: (context, index) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
CircleAvatar(
radius: 24,
backgroundColor: _shareOptions[index]['color'],
child: Icon(_shareOptions[index]['icon'], color: Colors.white),
),
const SizedBox(height: 8),
Text(
_shareOptions[index]['name'],
style: const TextStyle(fontSize: 12),
),
],
);
},
),
],
),
),
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0F172A),
appBar: AppBar(
title: const Text('BottomSheet 演示'),
backgroundColor: Colors.transparent,
elevation: 0,
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题区域
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
const Color(0xFF6366F1).withOpacity(0.2),
const Color(0xFF8B5CF6).withOpacity(0.2),
],
),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withOpacity(0.1),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'📱 BottomSheet',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 0.5,
),
),
const SizedBox(height: 8),
Text(
'探索 Flutter for OpenHarmony 中底部面板的各种用法',
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.7),
height: 1.5,
),
),
],
),
),
const SizedBox(height: 32),
// 基础面板
_buildSection(
title: '基础底部面板',
icon:Icons.vertical_align_bottom,
color: Colors.blue,
child: _buildCard([
const Text(
'点击按钮显示基础的底部面板',
style: TextStyle(
fontSize: 14,
color: Colors.white70,
),
),
const SizedBox(height: 12),
ElevatedButton.icon(
onPressed: () => _showBasicBottomSheet(context),
icon: const Icon(Icons.open_in_full),
label: const Text('打开面板'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
minimumSize: const Size(double.infinity, 48),
),
),
]),
),
const SizedBox(height: 24),
// 底部菜单
_buildSection(
title: '底部菜单',
icon: Icons.menu,
color: Colors.green,
child: _buildCard([
const Text(
'显示包含多个选项的底部菜单',
style: TextStyle(
fontSize: 14,
color: Colors.white70,
),
),
const SizedBox(height: 12),
ElevatedButton.icon(
onPressed: () => _showMenuBottomSheet(context),
icon: const Icon(Icons.menu_open),
label: const Text('打开菜单'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
minimumSize: const Size(double.infinity, 48),
),
),
]),
),
const SizedBox(height: 24),
// 分享面板
_buildSection(
title: '分享面板',
icon: Icons.share,
color: Colors.purple,
child: _buildCard([
const Text(
'显示分享选项网格',
style: TextStyle(
fontSize: 14,
color: Colors.white70,
),
),
const SizedBox(height: 12),
ElevatedButton.icon(
onPressed: () => _showShareBottomSheet(context),
icon: const Icon(Icons.ios_share),
label: const Text('打开分享'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purple,
minimumSize: const Size(double.infinity, 48),
),
),
]),
),
const SizedBox(height: 24),
const SizedBox(height: 80),
],
),
),
),
);
}
Widget _buildSection({
required String title,
required IconData icon,
required Color color,
required Widget child,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.2),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: color, size: 20),
),
const SizedBox(width: 12),
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
),
const SizedBox(height: 12),
child,
],
);
}
Widget _buildCard(List<Widget> children) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.03),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.white.withOpacity(0.05),
),
),
child: Column(
children: children,
),
);
}
}
七、总结
BottomSheet 是 Flutter for OpenHarmony 中实现底部交互的核心组件,通过合理使用可以创建流畅的底部面板体验。
🎯 核心要点
- 模态面板 :
showModalBottomSheet显示可关闭的底部面板 - 持久面板 :
showBottomSheet在 Scaffold 中显示持久面板 - 样式定制:通过 shape、backgroundColor 等自定义外观
- 交互控制:isDismissible、enableDrag 控制关闭方式
- 可拖动面板:DraggableScrollableSheet 提供可拖动的交互体验
- 滚动控制:isScrollControlled 支持内容滚动
📚 使用建议
| 场景 | 推荐方案 |
|---|---|
| 选项菜单 | showModalBottomSheet |
| 分享面板 | showModalBottomSheet + GridView |
| 评论列表 | DraggableScrollableSheet |
| 表单输入 | showModalBottomSheet + isScrollControlled |
| 长内容展示 | DraggableScrollableSheet + ListView |
掌握 BottomSheet 组件后,你可以轻松创建专业的底部交互界面,为用户提供流畅直观的操作体验。