Flutter for OpenHarmony 微动漫App实战 - 评分星级显示实现
摘要:本文介绍了微动漫App中评分展示的多种实现方式。在卡片视图中,评分通过星星图标和数字形式紧凑显示在底部渐变遮罩层上;列表项中评分置于副标题区域,采用简洁布局;详情页则使用醒目标签展示。技术实现上,卡片采用Stack层叠布局和渐变遮罩确保文字可读性,列表项利用ListTile的标准结构优化空间使用。文章详细讲解了Flutter组件如ClipRRect、Positioned、ListTile的应
通过网盘分享的文件:flutter1.zip
链接: https://pan.baidu.com/s/1jkLZ9mZXjNm0LgP6FTVRzw 提取码: 2t97
评分是动漫应用中最重要的信息之一。用户在浏览动漫列表时,往往会先看评分来判断一部作品是否值得观看。微动漫App在多个地方展示了评分信息:动漫卡片、列表项、详情页等。本文将深入讲解如何在不同场景下实现评分的展示,以及一些提升用户体验的设计技巧。
评分展示的几种形式
在微动漫App中,评分的展示形式根据场景不同而有所变化:
- 卡片视图:在动漫卡片底部,用星星图标加数字的紧凑形式展示
- 列表视图:在列表项的副标题位置,同样是星星加数字的形式
- 详情页:用一个醒目的标签展示,配合主题色背景
这种差异化的设计是有道理的。卡片空间有限,需要紧凑展示;列表项需要快速扫描,信息要简洁;详情页空间充裕,可以做得更醒目。接下来我们逐一实现这些场景。
动漫卡片中的评分展示
动漫卡片是首页和发现页的主要展示形式。卡片底部有一个渐变遮罩层,评分信息就显示在这个遮罩层上。
先看卡片的整体结构:
class AnimeCard extends StatelessWidget {
final Anime anime;
final bool showRank;
const AnimeCard({super.key, required this.anime, this.showRank = false});
卡片组件接收两个参数:
anime:动漫数据对象,包含标题、评分、图片等信息showRank:是否显示排名标签,默认不显示这个设计让卡片组件更加灵活。在排行榜页面可以传入
showRank: true显示排名,在其他页面则只显示基本信息。
卡片使用 Stack 实现图片和信息的叠加:
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Stack(
fit: StackFit.expand,
children: [
_buildImage(),
ClipRRect用于裁剪圆角,让卡片看起来更精致。borderRadius: BorderRadius.circular(12)设置了 12 像素的圆角。
Stack允许子组件层叠显示。fit: StackFit.expand让 Stack 填满父容器的全部空间。第一个子组件_buildImage()是动漫封面图,作为背景层。
底部信息区域使用渐变遮罩:
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withOpacity(0.9),
Colors.transparent,
],
),
),
Positioned在 Stack 中定位子组件。bottom: 0, left: 0, right: 0让这个容器贴在底部,并且左右撑满。渐变遮罩的设计:从底部的 90% 黑色渐变到顶部的透明。这样做有两个好处:
- 让白色文字在任何颜色的封面图上都能清晰显示
- 渐变过渡比纯色遮罩更自然,不会显得突兀
评分信息的具体实现:
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
anime.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
标题显示在评分上方,最多两行,超出部分用省略号表示。
mainAxisSize: MainAxisSize.min让 Column 只占用必要的高度,不会撑满整个遮罩区域。
const SizedBox(height: 4),
Row(
children: [
if (anime.score != null) ...[
const Icon(Icons.star, color: Colors.amber, size: 14),
const SizedBox(width: 2),
Text(
anime.score!.toStringAsFixed(1),
style: const TextStyle(color: Colors.white, fontSize: 11),
),
],
评分显示的核心代码:
if (anime.score != null):只有当评分数据存在时才显示。有些动漫可能还没有评分,这时候就不显示这部分内容...[:这是 Dart 的展开操作符,配合if可以条件性地添加多个元素到列表中Icons.star:使用 Material Icons 的星星图标color: Colors.amber:琥珀色,这是评分星星的经典颜色size: 14:图标大小设为 14 像素,与文字大小协调toStringAsFixed(1):将评分格式化为一位小数,比如 8.5、9.0
const Spacer(),
if (anime.episodes != null)
Text(
'${anime.episodes}集',
style: const TextStyle(color: Colors.white70, fontSize: 10),
),
],
),
Spacer()是一个弹性空间,会占据 Row 中所有剩余的空间。这样评分靠左,集数靠右,形成两端对齐的效果。集数使用了 70% 透明度的白色(
Colors.white70),比评分颜色淡一些,形成视觉层次。
列表项中的评分展示
列表项用于收藏页、历史记录页等场景,信息展示更加紧凑。
class AnimeListTile extends StatelessWidget {
final Anime anime;
final VoidCallback? onDelete;
const AnimeListTile({super.key, required this.anime, this.onDelete});
列表项组件除了动漫数据,还接收一个可选的删除回调。在收藏页和历史记录页,用户可以滑动删除,这个回调就是用来处理删除操作的。
评分显示在 ListTile 的 subtitle 中:
child: ListTile(
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => AnimeDetailScreen(anime: anime)),
),
leading: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: SizedBox(
width: 50,
height: 70,
child: _buildImage(),
),
),
ListTile是 Flutter 提供的列表项组件,内置了标准的布局结构:
leading:左侧区域,这里放动漫封面title:标题区域subtitle:副标题区域,我们把评分放在这里trailing:右侧区域,这里放一个箭头图标封面图使用
ClipRRect裁剪圆角,尺寸固定为 50x70 像素,这是一个比较标准的海报比例。
title: Text(
anime.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w600),
),
subtitle: Row(
children: [
if (anime.score != null) ...[
const Icon(Icons.star, color: Colors.amber, size: 14),
const SizedBox(width: 2),
Text(anime.score!.toStringAsFixed(1)),
const SizedBox(width: 8),
],
if (anime.type != null) Text(anime.type!),
],
),
trailing: const Icon(Icons.chevron_right),
),
副标题区域的布局:
- 先显示评分(星星图标 + 数字)
- 然后是 8 像素的间距
- 最后显示动漫类型(TV、Movie 等)
这种布局让用户一眼就能看到最关键的信息。评分和类型是用户决定是否点击查看详情的重要依据。
trailing: const Icon(Icons.chevron_right):右侧的箭头图标暗示这是一个可点击的项目,引导用户点击查看详情。
详情页中的评分展示
详情页有更多的空间,评分可以做得更醒目。我们使用一个带背景色的标签来展示。
Widget _buildInfoRow() {
return Row(
children: [
if (_anime.score != null) ...[
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(20),
),
评分标签使用
Container实现,关键样式:
padding:水平方向 12 像素,垂直方向 6 像素,让内容不会贴边color: Theme.of(context).primaryColor:使用应用的主题色作为背景,保持视觉一致性borderRadius: BorderRadius.circular(20):20 像素的圆角,让标签呈现胶囊形状这种胶囊形状的标签在现代 UI 设计中很常见,看起来简洁又精致。
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.star, color: Colors.white, size: 16),
const SizedBox(width: 4),
Text(
_anime.score!.toStringAsFixed(1),
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
],
),
),
const SizedBox(width: 8),
],
标签内部是一个 Row,包含星星图标和评分数字。
- 图标和文字都是白色,在主题色背景上清晰可见
- 文字使用粗体,更加醒目
mainAxisSize: MainAxisSize.min让 Row 只占用必要的宽度
排名标签紧跟在评分标签后面:
if (_anime.rank != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.amber,
borderRadius: BorderRadius.circular(20),
),
child: Text(
'#${_anime.rank}',
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),
],
);
}
排名标签的样式与评分标签类似,但使用琥珀色背景,与评分标签形成区分。
为什么排名用琥珀色? 琥珀色是金色的近似色,在很多场景下代表"排名"、"奖牌"的含义。用户看到这个颜色会自然联想到排行榜。
处理评分为空的情况
不是所有动漫都有评分数据。新上映的动漫、冷门作品可能还没有足够的评分。我们需要优雅地处理这种情况。
if (anime.score != null) ...[
const Icon(Icons.star, color: Colors.amber, size: 14),
const SizedBox(width: 2),
Text(anime.score!.toStringAsFixed(1)),
]
使用
if条件判断,只有当score不为空时才渲染评分相关的组件。这样当评分为空时,这部分内容完全不会显示,不会出现"null"或者空白区域。
anime.score!中的感叹号是 Dart 的空断言操作符。因为我们已经在if中检查了score != null,所以这里可以安全地使用!告诉编译器这个值一定不为空。
另一种处理方式是显示"暂无评分":
if (anime.score != null)
Text(anime.score!.toStringAsFixed(1))
else
Text('暂无评分', style: TextStyle(color: Colors.grey))
这种方式在某些场景下更合适,比如详情页。让用户明确知道这部动漫还没有评分,而不是误以为是数据加载失败。
评分数字的格式化
评分通常是一个浮点数,比如 8.756。直接显示这么多小数位会显得很乱,我们需要格式化。
anime.score!.toStringAsFixed(1)
toStringAsFixed(1)将数字格式化为保留一位小数的字符串。
- 8.756 → “8.8”(四舍五入)
- 9.0 → “9.0”(保留一位小数)
- 7.123 → “7.1”
一位小数是评分展示的标准做法,既精确又简洁。
如果你想要更灵活的格式化,可以使用 intl 包:
import 'package:intl/intl.dart';
final formatter = NumberFormat('#.#');
Text(formatter.format(anime.score))
这种方式可以去掉末尾的零,比如 9.0 会显示为 “9” 而不是 “9.0”。根据你的设计需求选择合适的格式化方式。
评分颜色的动态变化
有些应用会根据评分高低显示不同的颜色,比如高分绿色、中等黄色、低分红色。这种设计可以让用户更直观地感知评分的好坏。
Color getScoreColor(double score) {
if (score >= 8.0) return Colors.green;
if (score >= 6.0) return Colors.orange;
return Colors.red;
}
这个函数根据评分返回不同的颜色:
- 8 分及以上:绿色,代表优秀
- 6-8 分:橙色,代表中等
- 6 分以下:红色,代表较差
使用时可以这样:
Icon(Icons.star, color: getScoreColor(anime.score!), size: 14)
微动漫App目前使用统一的琥珀色,这也是一种设计选择。统一颜色看起来更简洁,动态颜色则信息量更大。
星级评分组件
除了数字评分,有些应用还会显示星星数量,比如 4 颗星、4.5 颗星。这种展示方式更直观。
Widget buildStarRating(double score) {
// 将 10 分制转换为 5 星制
double stars = score / 2;
int fullStars = stars.floor();
bool hasHalfStar = (stars - fullStars) >= 0.5;
return Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(5, (index) {
if (index < fullStars) {
return const Icon(Icons.star, color: Colors.amber, size: 16);
} else if (index == fullStars && hasHalfStar) {
return const Icon(Icons.star_half, color: Colors.amber, size: 16);
} else {
return const Icon(Icons.star_border, color: Colors.amber, size: 16);
}
}),
);
}
这个函数将 10 分制的评分转换为 5 星显示:
score / 2:10 分制转 5 星制fullStars:完整星星的数量hasHalfStar:是否有半颗星
List.generate(5, ...)生成 5 个星星图标:
- 索引小于
fullStars的位置显示实心星星- 如果有半颗星,在
fullStars位置显示半星- 其余位置显示空心星星
比如评分 8.5:
- stars = 4.25
- fullStars = 4
- hasHalfStar = false(0.25 < 0.5)
- 显示:★★★★☆
性能优化建议
评分组件虽然简单,但在列表中会被大量复用,有一些优化点值得注意:
使用 const 构造函数
const Icon(Icons.star, color: Colors.amber, size: 14)
当图标的所有参数都是编译时常量时,使用
const可以让 Flutter 复用同一个实例,减少内存分配。
避免在 build 方法中创建对象
// 不好的做法
Text(
score.toString(),
style: TextStyle(color: Colors.white), // 每次 build 都创建新对象
)
// 好的做法
static const _scoreStyle = TextStyle(color: Colors.white);
Text(
score.toString(),
style: _scoreStyle, // 复用同一个对象
)
将不变的样式定义为静态常量,避免每次 build 都创建新的
TextStyle对象。
条件渲染而非透明度
// 不好的做法
Opacity(
opacity: anime.score != null ? 1.0 : 0.0,
child: ScoreWidget(),
)
// 好的做法
if (anime.score != null) ScoreWidget()
使用
Opacity隐藏组件时,组件仍然会被渲染,只是不可见。使用条件渲染可以完全跳过不需要的组件。
小结
评分展示看似简单,但要做好需要考虑很多细节:
- 不同场景使用不同的展示形式:卡片用紧凑形式,详情页用醒目标签
- 优雅处理空值:评分为空时不显示或显示"暂无评分"
- 合理的数字格式化:保留一位小数,既精确又简洁
- 视觉层次设计:通过颜色、大小、透明度区分主次信息
- 性能优化:使用 const、复用样式对象、条件渲染
这些技巧不仅适用于评分展示,在其他信息展示场景也同样适用。希望本文对你有所帮助。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)