欢迎加入开源鸿蒙跨平台社区: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 + 自定义布局。

关键点是用 DialogbackgroundColor: Colors.transparent 让背景透明,然后自己画一个圆角白色容器。这样比用默认的 AlertDialog 灵活多了,想怎么设计就怎么设计。

水果卡片用 GestureDetector 包裹,点击时用 Navigator.pop(context, fruit) 关闭弹窗并返回数据。调用方用 await 接收返回值,就能拿到用户选择的水果了。

网格布局用 GridView.builder,2 列固定,间距 12。每个卡片里面是图片 + 名称。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐