Flutter for OpenHarmony 美食烹饪助手 App 实战:热门菜谱展示实现
本文介绍了热门菜谱页面的实现方案,采用网格布局展示内容。通过 GridView.builder 构建两列网格,配置合理的间距和宽高比,在有限屏幕空间内高效展示更多菜谱。卡片设计包含图片区域、热度标签和基本信息,突出"热门"特色。使用 Stack 布局叠加热度标签,红色背景增强视觉吸引力。文本信息经过优化处理,确保在有限空间内清晰展示。整体设计保持与推荐菜谱一致的视觉风格,同时针

上一篇我们实现了推荐菜谱的列表展示,今天要实现热门菜谱功能。和推荐菜谱不同,热门菜谱采用网格布局,这种布局能在有限的屏幕空间内展示更多内容,也更适合展示以图片为主的内容。
网格布局的设计考量
在设计热门菜谱页面时,我首先考虑的是如何让用户快速浏览大量内容。列表布局虽然信息展示完整,但一屏只能显示几个项目。网格布局则可以在一屏内展示更多内容,让用户能快速扫视。
网格的列数是个关键决策。一列太少,浪费屏幕空间;太多又会让每个卡片太小,看不清内容。经过权衡,我选择了两列布局。这个数字在手机屏幕上刚刚好,既能展示足够的内容,每个卡片又有足够的空间显示图片和文字。
卡片的宽高比也很重要。我设置为 0.75,也就是宽度是高度的 75%。这个比例让卡片略微偏高,能容纳图片和必要的文字信息,同时不会显得太瘦长。
搭建网格页面
热门菜谱页面的基本结构和推荐菜谱类似,但使用 GridView 代替 ListView。
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class PopularRecipesPage extends StatelessWidget {
const PopularRecipesPage({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('热门菜谱'),
),
页面依然使用 StatelessWidget,因为热门列表是相对静态的。AppBar 只显示标题和自动的返回按钮,保持简洁。
这里没有导入 GetX,因为暂时不需要页面跳转。如果以后要添加点击跳转到详情的功能,再添加也不迟。这种按需导入的方式能让代码更清晰,也能减少不必要的依赖。
配置 GridView
GridView.builder 是构建网格的最佳选择,它和 ListView.builder 一样支持懒加载。
body: GridView.builder(
padding: EdgeInsets.all(16.w),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12.w,
mainAxisSpacing: 12.h,
childAspectRatio: 0.75,
),
itemCount: 20,
itemBuilder: (context, index) {
return _buildPopularCard(index);
},
),
);
}
gridDelegate 是网格布局的核心配置。SliverGridDelegateWithFixedCrossAxisCount 表示使用固定列数的网格。crossAxisCount 设置为 2,表示两列布局。
crossAxisSpacing 是列之间的间距,mainAxisSpacing 是行之间的间距。我把它们都设置为 12,这个间距不大不小,既能区分不同的卡片,又不会浪费太多空间。
childAspectRatio 是宽高比,0.75 表示宽度是高度的 75%。这个比例需要根据实际内容调整,如果卡片内容较多,可以设置得更小一些,让卡片更高。
padding 设置为 16.w,让网格内容和屏幕边缘保持距离。这个间距和列间距、行间距配合,形成统一的视觉节奏。
设计热门卡片
热门菜谱的卡片设计要突出"热门"这个特点,所以我添加了热度标签。
Widget _buildPopularCard(int index) {
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),
),
],
),
卡片的基本样式和推荐菜谱类似,白色背景、圆角、轻微的阴影。这种统一的设计语言能让应用看起来更协调。
阴影的设置保持一致,使用半透明灰色,模糊半径 5,向下偏移 2。这个阴影很微妙,但能让卡片有轻微的悬浮感,增加层次。
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
Container(
height: 140.h,
decoration: BoxDecoration(
color: Colors.orange.shade100,
borderRadius: BorderRadius.vertical(top: Radius.circular(12.r)),
),
child: Center(
child: Icon(
Icons.restaurant,
size: 50.sp,
color: Colors.orange,
),
),
),
卡片顶部是图片区域,高度设置为 140.h。这个高度比推荐菜谱的图片稍高一些,因为网格卡片的宽度较小,需要更高的图片来保持视觉平衡。
使用 Stack 布局是为了在图片上叠加热度标签。Stack 的第一个子组件是图片容器,它会作为背景层。
添加热度标签
热度标签是热门菜谱的特色,它能直观地显示菜谱的受欢迎程度。
Positioned(
top: 8.h,
right: 8.w,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
children: [
Icon(Icons.local_fire_department, size: 12.sp, color: Colors.white),
SizedBox(width: 2.w),
Text(
'${1000 + index * 100}',
style: TextStyle(color: Colors.white, fontSize: 10.sp),
),
],
),
),
),
],
),
Positioned 让标签定位在图片的右上角。top 和 right 都设置为 8,让标签和边缘保持一定距离,不会紧贴边缘。
标签使用红色背景,这个颜色能吸引注意力,也符合"热门"的概念。圆角设置为 12.r,和整体的圆角风格保持一致。
标签内容是一个火焰图标配合数字。火焰图标很形象地表达了"热度"的概念。数字使用白色,在红色背景上很醒目。这里我用 1000 + index * 100 生成模拟数据,实际应用中应该使用真实的热度值。
图标大小 12.sp,文字大小 10.sp,都比较小,因为标签只是辅助信息,不应该太突出。Row 中的 SizedBox 宽度只有 2.w,让图标和文字紧密排列。
显示菜谱信息
图片下方是菜谱的基本信息,包括名称和评分。
Padding(
padding: EdgeInsets.all(8.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'热门菜谱 ${index + 1}',
style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.bold),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Padding 设置为 8.w,比推荐菜谱的 12.w 小一些。这是因为网格卡片的空间有限,需要节省每一点空间。
标题使用粗体,字号 14.sp。maxLines 设置为 1,overflow 设置为 ellipsis,确保标题不会换行,太长的标题会显示省略号。这种处理方式在网格布局中很常见,因为空间有限,必须做出取舍。
SizedBox(height: 4.h),
Row(
children: [
Icon(Icons.star, size: 12.sp, color: Colors.amber),
SizedBox(width: 4.w),
Text('4.${8 - (index % 3)}', style: TextStyle(fontSize: 11.sp)),
],
),
],
),
),
],
),
);
}
}
评分信息使用星星图标配合数字。星星图标使用琥珀色,这是评分的标准颜色。数字使用 4.8、4.7、4.6 这样的模拟数据,通过 index % 3 来产生变化。
图标大小 12.sp,文字大小 11.sp,都比较小。在网格布局中,信息要精简,只保留最重要的内容。评分是用户很关心的信息,所以保留了,但烹饪时间、难度等信息就省略了。
优化网格性能
网格布局比列表布局更消耗性能,因为同时显示的项目更多。我们需要特别注意性能优化。
首先是使用 const 构造函数。对于不变的组件,尽量使用 const:
const Icon(Icons.star)
const Text('热门菜谱')
其次是避免在 build 方法中创建复杂对象。比如 BoxDecoration,如果多个卡片使用相同的装饰,可以定义为常量:
static final cardDecoration = BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 5,
offset: Offset(0, 2),
),
],
);
不过使用 flutter_screenutil 时,圆角半径需要动态计算,就不能完全用 const 了。这时候可以考虑缓存计算结果。
实现瀑布流布局
如果菜谱卡片的高度不固定,可以考虑使用瀑布流布局。瀑布流能让卡片高度自适应,看起来更自然。
可以使用 flutter_staggered_grid_view 包:
StaggeredGridView.countBuilder(
crossAxisCount: 2,
itemCount: 20,
itemBuilder: (context, index) => _buildPopularCard(index),
staggeredTileBuilder: (index) => StaggeredTile.fit(1),
mainAxisSpacing: 12,
crossAxisSpacing: 12,
)
StaggeredTile.fit(1) 表示卡片占一列,高度自适应内容。这样不同高度的卡片可以紧密排列,不会留下空白。
不过瀑布流的实现比较复杂,而且性能开销更大。对于我们这个应用,固定高度的网格已经足够了。
添加筛选和排序
热门菜谱可能有不同的维度,比如按浏览量、收藏量、评分等排序。可以在 AppBar 添加筛选按钮:
AppBar(
title: const Text('热门菜谱'),
actions: [
PopupMenuButton<String>(
onSelected: (value) {
// 根据选择的排序方式重新排序
},
itemBuilder: (context) => [
PopupMenuItem(value: 'views', child: Text('按浏览量')),
PopupMenuItem(value: 'favorites', child: Text('按收藏量')),
PopupMenuItem(value: 'rating', child: Text('按评分')),
],
),
],
)
PopupMenuButton 会显示一个下拉菜单,用户可以选择排序方式。选择后,在 onSelected 回调中更新数据,重新渲染列表。
这种交互方式很常见,用户也容易理解。不过要注意,如果添加了排序功能,页面就需要改成 StatefulWidget,以便维护当前的排序状态。
处理不同屏幕尺寸
虽然我们使用了 flutter_screenutil 进行适配,但在特别大或特别小的屏幕上,两列布局可能不是最优选择。
可以根据屏幕宽度动态调整列数:
LayoutBuilder(
builder: (context, constraints) {
int crossAxisCount = 2;
if (constraints.maxWidth > 600) {
crossAxisCount = 3;
} else if (constraints.maxWidth > 900) {
crossAxisCount = 4;
}
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
// ...
),
// ...
);
},
)
LayoutBuilder 可以获取父容器的约束,根据宽度判断应该使用几列。这样在平板等大屏设备上,可以显示更多列,充分利用屏幕空间。
实现骨架屏
在数据加载时,可以显示骨架屏,让用户知道内容正在加载,而不是看到空白页面。
骨架屏就是用灰色的占位块模拟真实内容的布局:
Widget _buildSkeletonCard() {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
),
child: Column(
children: [
Container(
height: 140.h,
color: Colors.grey.shade200,
),
Padding(
padding: EdgeInsets.all(8.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 14.h,
width: double.infinity,
color: Colors.grey.shade200,
),
SizedBox(height: 4.h),
Container(
height: 12.h,
width: 60.w,
color: Colors.grey.shade200,
),
],
),
),
],
),
);
}
可以添加一个闪烁动画,让骨架屏看起来更生动:
AnimatedContainer(
duration: Duration(milliseconds: 1000),
color: _shimmer ? Colors.grey.shade200 : Colors.grey.shade300,
)
配合定时器不断切换 _shimmer 的值,就能产生闪烁效果。不过这需要把 Widget 改成 StatefulWidget。
添加空状态提示
如果没有热门菜谱,应该显示友好的提示,而不是空白页面。
Widget build(BuildContext context) {
if (recipes.isEmpty) {
return Scaffold(
appBar: AppBar(title: const Text('热门菜谱')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.trending_up, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text('暂无热门菜谱', style: TextStyle(color: Colors.grey)),
SizedBox(height: 8),
TextButton(
onPressed: () {
// 刷新数据
},
child: Text('刷新'),
),
],
),
),
);
}
// 正常的网格
}
空状态页面使用图标、文字和按钮的组合。图标使用 trending_up,表示热门趋势。文字说明当前状态,按钮让用户可以主动刷新。
这种设计让应用更友好,即使出现异常情况,用户也知道发生了什么,以及可以做什么。
测试网格布局
网格布局的测试要特别注意不同屏幕尺寸的表现。
在模拟器中测试不同的设备,看网格是否正常显示。特别要注意小屏幕设备,卡片会不会太小,文字会不会看不清。
还要测试滚动性能,快速滚动网格,看是否流畅。如果出现卡顿,可以使用 Performance Overlay 来定位问题。
可以使用 Flutter Inspector 来检查布局,看是否有溢出或者布局错误。Inspector 可以显示每个组件的边界,帮助我们理解布局结构。
总结
热门菜谱页面使用网格布局,能在有限的空间内展示更多内容。我们添加了热度标签来突出"热门"的特点,使用评分来展示菜谱质量。
网格布局的实现比列表布局稍微复杂一些,需要仔细调整列数、间距、宽高比等参数。但一旦调整好,它能提供很好的浏览体验,特别适合展示以图片为主的内容。
下一篇文章我们将实现今日推荐功能,探讨如何设计一个有吸引力的推荐页面。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)