通过网盘分享的文件: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% 黑色渐变到顶部的透明。这样做有两个好处:

  1. 让白色文字在任何颜色的封面图上都能清晰显示
  2. 渐变过渡比纯色遮罩更自然,不会显得突兀

评分信息的具体实现:

            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});

列表项组件除了动漫数据,还接收一个可选的删除回调。在收藏页和历史记录页,用户可以滑动删除,这个回调就是用来处理删除操作的。

评分显示在 ListTilesubtitle 中:

      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 隐藏组件时,组件仍然会被渲染,只是不可见。使用条件渲染可以完全跳过不需要的组件。

小结

评分展示看似简单,但要做好需要考虑很多细节:

  1. 不同场景使用不同的展示形式:卡片用紧凑形式,详情页用醒目标签
  2. 优雅处理空值:评分为空时不显示或显示"暂无评分"
  3. 合理的数字格式化:保留一位小数,既精确又简洁
  4. 视觉层次设计:通过颜色、大小、透明度区分主次信息
  5. 性能优化:使用 const、复用样式对象、条件渲染

这些技巧不仅适用于评分展示,在其他信息展示场景也同样适用。希望本文对你有所帮助。


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

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐