在这里插入图片描述

中国饮食文化博大精深,八大菜系各有特色。在美食应用中,按菜系分类是最自然的组织方式之一。今天我们要实现菜系分类页面,让用户能够按照自己喜欢的菜系来浏览菜谱。

菜系分类的设计思路

菜系分类页面要解决的核心问题是:如何让用户快速找到自己想要的菜系?我选择了网格布局,因为它能在一屏内展示多个分类,用户可以快速扫视所有选项。

每个菜系使用不同的颜色和图标来区分,这样用户能快速识别。颜色的选择也有讲究,比如川菜用红色表示辣,粤菜用绿色表示清淡,这些颜色能帮助用户建立视觉记忆。

网格采用三列布局,这个数字在手机屏幕上刚好合适。两列会显得太空,四列又会让每个卡片太小。三列是一个平衡点,既能展示足够的内容,每个卡片又有足够的空间。

定义菜系数据

首先要定义我们支持的菜系列表。我选择了12种常见菜系,包括中国八大菜系和几种国际菜系。

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';

class CuisineCategoryPage extends StatelessWidget {
  const CuisineCategoryPage({super.key});

  
  Widget build(BuildContext context) {
    final categories = [
      {'name': '川菜', 'icon': Icons.local_fire_department, 'color': Colors.red},
      {'name': '粤菜', 'icon': Icons.restaurant, 'color': Colors.green},
      {'name': '鲁菜', 'icon': Icons.dining, 'color': Colors.blue},
      {'name': '苏菜', 'icon': Icons.ramen_dining, 'color': Colors.purple},
      {'name': '浙菜', 'icon': Icons.rice_bowl, 'color': Colors.teal},
      {'name': '闽菜', 'icon': Icons.set_meal, 'color': Colors.orange},
      {'name': '湘菜', 'icon': Icons.soup_kitchen, 'color': Colors.deepOrange},
      {'name': '徽菜', 'icon': Icons.lunch_dining, 'color': Colors.brown},
      {'name': '西餐', 'icon': Icons.fastfood, 'color': Colors.amber},
      {'name': '日料', 'icon': Icons.sushi_dining, 'color': Colors.pink},
      {'name': '韩餐', 'icon': Icons.food_bank, 'color': Colors.indigo},
      {'name': '东南亚', 'icon': Icons.outdoor_grill, 'color': Colors.lime},
    ];

每个菜系包含三个属性:名称、图标和颜色。使用 Map 来组织数据虽然不如定义一个类那么严谨,但对于这种简单的场景已经够用了。

图标的选择很有讲究。川菜用火焰图标,因为川菜以辣著称。粤菜用餐厅图标,表示精致。日料用寿司图标,这是日料的代表。这些图标能帮助用户快速识别菜系。

颜色的选择也有考虑。红色给人热情、辣的感觉,适合川菜和湘菜。绿色给人清淡、健康的感觉,适合粤菜。这些颜色不仅美观,还能传达菜系的特点。

构建网格布局

菜系分类使用 GridView 来展示,三列布局,正方形卡片。

    return Scaffold(
      appBar: AppBar(
        title: const Text('菜系分类'),
      ),
      body: GridView.builder(
        padding: EdgeInsets.all(16.w),
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3,
          crossAxisSpacing: 12.w,
          mainAxisSpacing: 12.h,
          childAspectRatio: 1,
        ),
        itemCount: categories.length,
        itemBuilder: (context, index) {
          final category = categories[index];
          return _buildCategoryCard(
            category['name'] as String,
            category['icon'] as IconData,
            category['color'] as Color,
          );
        },
      ),
    );
  }

crossAxisCount 设置为 3,表示三列。crossAxisSpacing 和 mainAxisSpacing 都设置为 12,让卡片之间有适当的间距。

childAspectRatio 设置为 1,表示宽高比 1:1,也就是正方形。正方形的卡片看起来很整齐,也符合分类导航的设计习惯。

itemCount 使用 categories.length,这样如果以后要添加或删除菜系,只需要修改 categories 列表就行了,不需要改其他代码。

在 itemBuilder 中,我们从 Map 中提取数据,然后传递给 _buildCategoryCard 方法。这里需要做类型转换,因为 Map 的值类型是 dynamic。

设计分类卡片

每个分类卡片包含一个圆形图标和菜系名称。

  Widget _buildCategoryCard(String name, IconData icon, Color color) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12.r),
        boxShadow: [
          BoxShadow(
            color: Colors.grey.withOpacity(0.1),
            blurRadius: 5,
            offset: const Offset(0, 2),
          ),
        ],
      ),

卡片使用白色背景、圆角和轻微阴影,这是应用的统一风格。阴影的设置和其他页面保持一致,让整个应用看起来协调。

      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Container(
            width: 50.w,
            height: 50.w,
            decoration: BoxDecoration(
              color: color.withOpacity(0.1),
              borderRadius: BorderRadius.circular(25.r),
            ),
            child: Icon(icon, color: color, size: 28.sp),
          ),
          SizedBox(height: 8.h),
          Text(
            name,
            style: TextStyle(fontSize: 13.sp, fontWeight: FontWeight.bold),
          ),
        ],
      ),
    );
  }
}

卡片内容垂直居中排列。图标放在一个圆形容器中,容器的背景色是菜系颜色的半透明版本。这样既能展示颜色,又不会太突兀。

圆形容器的宽高都是 50.w,borderRadius 设置为 25.r,正好是宽度的一半,形成完美的圆形。图标颜色使用菜系的主色,大小 28.sp。

菜系名称使用粗体,字号 13.sp。这个字号不大不小,既能清晰显示,又不会占用太多空间。

添加点击交互

虽然现在卡片还不能点击,但我们要为以后的功能预留接口。可以用 GestureDetector 或 InkWell 包裹卡片:

Widget _buildCategoryCard(String name, IconData icon, Color color) {
  return InkWell(
    onTap: () {
      // 跳转到该菜系的菜谱列表
      Get.to(() => CuisineRecipesPage(cuisine: name));
    },
    borderRadius: BorderRadius.circular(12.r),
    child: Container(
      // ...
    ),
  );
}

InkWell 会在点击时显示水波纹效果,borderRadius 参数让水波纹遵循容器的圆角。这种反馈效果能让用户知道点击被识别了。

点击后跳转到该菜系的菜谱列表页面,传递菜系名称作为参数。这样列表页面就知道要显示哪个菜系的菜谱了。

优化视觉效果

为了让页面更生动,可以给卡片添加一些动画效果。比如点击时稍微缩小,松开时恢复:

class _CategoryCard extends StatefulWidget {
  final String name;
  final IconData icon;
  final Color color;
  
  const _CategoryCard({
    required this.name,
    required this.icon,
    required this.color,
  });

  
  State<_CategoryCard> createState() => _CategoryCardState();
}

class _CategoryCardState extends State<_CategoryCard> {
  bool _isPressed = false;

  
  Widget build(BuildContext context) {
    return GestureDetector(
      onTapDown: (_) => setState(() => _isPressed = true),
      onTapUp: (_) => setState(() => _isPressed = false),
      onTapCancel: () => setState(() => _isPressed = false),
      child: AnimatedScale(
        scale: _isPressed ? 0.95 : 1.0,
        duration: Duration(milliseconds: 100),
        child: Container(
          // ...
        ),
      ),
    );
  }
}

AnimatedScale 会在 scale 值改变时自动播放动画。按下时 scale 变为 0.95,松开时恢复为 1.0。这种微妙的动画能让交互更有趣。

不过这需要把卡片改成 StatefulWidget,增加了复杂度。对于简单的场景,使用 InkWell 的水波纹效果就够了。

处理不同屏幕尺寸

三列布局在手机上很合适,但在平板等大屏设备上可能显得太空。可以根据屏幕宽度动态调整列数:

LayoutBuilder(
  builder: (context, constraints) {
    int crossAxisCount = 3;
    if (constraints.maxWidth > 600) {
      crossAxisCount = 4;
    } else if (constraints.maxWidth > 900) {
      crossAxisCount = 6;
    }
    
    return GridView.builder(
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: crossAxisCount,
        // ...
      ),
      // ...
    );
  },
)

LayoutBuilder 可以获取父容器的约束,根据宽度判断应该使用几列。这样在大屏设备上可以显示更多列,充分利用屏幕空间。

添加搜索功能

如果菜系很多,用户可能想快速找到特定的菜系。可以在 AppBar 添加搜索按钮:

AppBar(
  title: const Text('菜系分类'),
  actions: [
    IconButton(
      icon: Icon(Icons.search),
      onPressed: () {
        showSearch(
          context: context,
          delegate: CuisineSearchDelegate(),
        );
      },
    ),
  ],
)

showSearch 会打开一个搜索界面,CuisineSearchDelegate 定义搜索的行为。用户可以输入关键词,快速找到想要的菜系。

总结

菜系分类页面使用三列网格布局,每个菜系用不同的颜色和图标区分。这种设计清晰直观,用户能快速找到自己想要的菜系。

通过合理的颜色和图标选择,我们不仅让页面美观,还传达了每个菜系的特点。这种设计既实用又有趣,能提升用户的浏览体验。

下一篇文章我们将实现食材分类功能,探讨另一种常见的分类方式。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐