【收尾以及复盘】flutter开发鸿蒙APP之今日选择吃什么水果弹出框
本文介绍了一个Flutter实现的底部弹出式水果选择对话框组件。该弹窗采用圆角卡片设计,包含标题栏(带关闭按钮)、2列网格布局的水果选项(每项含图片和名称)和底部提示文字。核心实现使用showDialog显示透明背景弹窗,通过GridView构建水果网格,每个水果卡片支持点击选择并返回数据。组件具有良好交互性,支持点击外部关闭,返回选中的水果信息供后续处理。完整代码结构清晰,可直接集成到项目中实现
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
1.今日选择吃什么水果弹出框先看截图效果

这是一个底部弹出的水果选择对话框,用户可以选择今天吃了什么水果。
弹窗特点:
- 圆角卡片样式,白色背景
- 顶部有标题"今天吃了什么?"和副标题"记录下你今天的营养时刻"
- 右上角有关闭按钮(X)
- 中间是 2 列网格,显示多个水果选项
- 每个水果卡片有图片和名称
- 底部提示"选择一个水果以继续"
2. 核心实现
2.1 弹窗调用
用 showDialog 显示弹窗:
void _showFruitSelectionDialog(BuildContext context) {
showDialog(
context: context,
barrierDismissible: true, // 点击外部可关闭
builder: (BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent, // 透明背景
insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: FruitSelectionDialog(),
);
},
);
}
关键点:
barrierDismissible: true允许点击外部关闭
backgroundColor: Colors.transparent让背景透明,显示自定义样式
insetPadding控制弹窗距离屏幕边缘的距离
2.2 弹窗内容
class FruitSelectionDialog extends StatelessWidget {
const FruitSelectionDialog({super.key});
// 水果数据
final List<Map<String, String>> fruits = const [
{'name': '脐橙', 'image': 'assets/images/fruits/orange.png', 'color': '0xFFFFF4E6'},
{'name': '蜜橘', 'image': 'assets/images/fruits/tangerine.png', 'color': '0xFFFFE8CC'},
{'name': '香蕉', 'image': 'assets/images/fruits/banana.png', 'color': '0xFFFFFAE6'},
{'name': '葡萄', 'image': 'assets/images/fruits/grape.png', 'color': '0xFFF3E5F5'},
{'name': '西瓜', 'image': 'assets/images/fruits/watermelon.png', 'color': '0xFFFFEBEE'},
{'name': '猕猴桃', 'image': 'assets/images/fruits/kiwi.png', 'color': '0xFFE8F5E9'},
];
@override
Widget build(BuildContext context) {
return Container(
constraints: const BoxConstraints(maxHeight: 600), // 最大高度
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24), // 圆角
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 顶部标题栏
_buildHeader(context),
// 水果网格
Flexible(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: _buildFruitGrid(context),
),
),
// 底部提示
_buildFooter(),
],
),
);
}
}
用 Column 布局,从上到下依次是:标题栏、水果网格、底部提示。
2.3 顶部展示
Widget _buildHeader(BuildContext context) {
return Container(
padding: const EdgeInsets.all(20),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(color: Color(0xFFF0F0F0), width: 1),
),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'今天吃了什么?',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 4),
Text(
'记录下你今天的营养时刻',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
),
// 关闭按钮
IconButton(
icon: const Icon(Icons.close, color: Color(0xFF9CA3AF)),
onPressed: () => Navigator.pop(context),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
),
);
}
布局:
- 左边是标题和副标题,用
Expanded占满剩余空间
- 右边是关闭按钮,固定宽度
- 底部有一条浅灰色分割线
2.4 水果网格
Widget _buildFruitGrid(BuildContext context) {
return GridView.builder(
shrinkWrap: true, // 自适应高度
physics: const NeverScrollableScrollPhysics(), // 禁止滚动(外层有 ScrollView)
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, // 2 列
childAspectRatio: 0.85, // 宽高比
crossAxisSpacing: 12, // 列间距
mainAxisSpacing: 12, // 行间距
),
itemCount: fruits.length,
itemBuilder: (context, index) {
return _buildFruitCard(context, fruits[index]);
},
);
}
用 GridView.builder 渲染 2 列网格,每个格子是一个水果卡片。
2.5 水果卡片
Widget _buildFruitCard(BuildContext context, Map<String, String> fruit) {
return GestureDetector(
onTap: () {
// 选择水果后关闭弹窗,并返回选中的水果
Navigator.pop(context, fruit);
},
child: Container(
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFF0F0F0), width: 1),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 水果图片容器
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: Color(int.parse(fruit['color']!)), // 背景色
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.all(16),
child: Image.asset(
fruit['image']!,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
// 图片加载失败显示占位图标
return const Icon(
Icons.image_not_supported,
size: 48,
color: Colors.grey,
);
},
),
),
const SizedBox(height: 12),
// 水果名称
Text(
fruit['name']!,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Color(0xFF1F2937),
),
),
],
),
),
);
}
卡片结构:
- 外层是浅灰色背景 + 圆角 + 边框
- 中间是图片容器
- 底部是水果名称
点击卡片时,用 Navigator.pop(context, fruit) 关闭弹窗并返回选中的水果数据。
2.6 底部提示
Widget _buildFooter() {
return Container(
padding: const EdgeInsets.all(20),
decoration: const BoxDecoration(
border: Border(
top: BorderSide(color: Color(0xFFF0F0F0), width: 1),
),
),
child: Text(
'选择一个水果以继续',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
textAlign: TextAlign.center,
),
);
}
简单的文字提示,顶部有分割线。
3. 使用方式
在需要显示弹窗的地方调用:
// 显示弹窗
final selectedFruit = await _showFruitSelectionDialog(context);
// 处理选中的水果
if (selectedFruit != null) {
print('用户选择了: ${selectedFruit['name']}');
// 执行后续操作,比如打卡
}
用 await 等待用户选择,返回值是选中的水果数据(Map)。如果用户点击关闭或点击外部,返回 null。
4. 完整代码
import 'package:flutter/material.dart';
class FruitSelectionDialog extends StatelessWidget {
const FruitSelectionDialog({super.key});
final List<Map<String, String>> fruits = const [
{'name': '脐橙', 'image': 'assets/images/fruits/orange.png', 'color': '0xFFFFF4E6'},
{'name': '蜜橘', 'image': 'assets/images/fruits/tangerine.png', 'color': '0xFFFFE8CC'},
{'name': '香蕉', 'image': 'assets/images/fruits/banana.png', 'color': '0xFFFFFAE6'},
{'name': '葡萄', 'image': 'assets/images/fruits/grape.png', 'color': '0xFFF3E5F5'},
{'name': '西瓜', 'image': 'assets/images/fruits/watermelon.png', 'color': '0xFFFFEBEE'},
{'name': '猕猴桃', 'image': 'assets/images/fruits/kiwi.png', 'color': '0xFFE8F5E9'},
];
@override
Widget build(BuildContext context) {
return Container(
constraints: const BoxConstraints(maxHeight: 600),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildHeader(context),
Flexible(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: _buildFruitGrid(context),
),
),
_buildFooter(),
],
),
);
}
Widget _buildHeader(BuildContext context) {
return Container(
padding: const EdgeInsets.all(20),
decoration: const BoxDecoration(
border: Border(bottom: BorderSide(color: Color(0xFFF0F0F0), width: 1)),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'今天吃了什么?',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 4),
Text(
'记录下你今天的营养时刻',
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
],
),
),
IconButton(
icon: const Icon(Icons.close, color: Color(0xFF9CA3AF)),
onPressed: () => Navigator.pop(context),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
),
);
}
Widget _buildFruitGrid(BuildContext context) {
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.85,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: fruits.length,
itemBuilder: (context, index) {
return _buildFruitCard(context, fruits[index]);
},
);
}
Widget _buildFruitCard(BuildContext context, Map<String, String> fruit) {
return GestureDetector(
onTap: () => Navigator.pop(context, fruit),
child: Container(
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFF0F0F0), width: 1),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: Color(int.parse(fruit['color']!)),
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.all(16),
child: Image.asset(
fruit['image']!,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return const Icon(Icons.image_not_supported, size: 48, color: Colors.grey);
},
),
),
const SizedBox(height: 12),
Text(
fruit['name']!,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Color(0xFF1F2937),
),
),
],
),
),
);
}
Widget _buildFooter() {
return Container(
padding: const EdgeInsets.all(20),
decoration: const BoxDecoration(
border: Border(top: BorderSide(color: Color(0xFFF0F0F0), width: 1)),
),
child: Text(
'选择一个水果以继续',
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
textAlign: TextAlign.center,
),
);
}
}
// 调用方法
Future<Map<String, String>?> showFruitSelectionDialog(BuildContext context) {
return showDialog<Map<String, String>>(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: const FruitSelectionDialog(),
);
},
);
}
5. 总结
弹窗背景: Colors.white
标题: Color(0xFF1F2937) // 深灰
副标题: Colors.grey[600] // 中灰
关闭按钮: Color(0xFF9CA3AF) // 浅灰
分割线: Color(0xFFF0F0F0) // 极浅灰卡片背景: Color(0xFFFAFAFA) // 浅灰
卡片边框: Color(0xFFF0F0F0) // 极浅灰水果背景色(根据水果不同):
脐橙: Color(0xFFFFF4E6) // 淡橙色
蜜橘: Color(0xFFFFE8CC) // 淡橙色
香蕉: Color(0xFFFFFAE6) // 淡黄色
葡萄: Color(0xFFF3E5F5) // 淡紫色
西瓜: Color(0xFFFFEBEE) // 淡红色
猕猴桃: Color(0xFFE8F5E9) // 淡绿色
这个弹窗实现起来不复杂,就是 showDialog + 自定义布局。
关键点是用 Dialog 的 backgroundColor: Colors.transparent 让背景透明,然后自己画一个圆角白色容器。这样比用默认的 AlertDialog 灵活多了,想怎么设计就怎么设计。
水果卡片用 GestureDetector 包裹,点击时用 Navigator.pop(context, fruit) 关闭弹窗并返回数据。调用方用 await 接收返回值,就能拿到用户选择的水果了。
网格布局用 GridView.builder,2 列固定,间距 12。每个卡片里面是图片 + 名称。
更多推荐




所有评论(0)