角色详情页面是展示角色信息的重要窗口。这篇文章我们来实现一个信息丰富的角色详情页面,包括角色头像、基本信息、出场剧集列表、以及收藏功能。通过精心的设计,我们能为用户提供一个沉浸式的角色浏览体验
请添加图片描述

页面的基本结构

CharacterDetailScreen接收一个角色对象作为参数:

class CharacterDetailScreen extends StatefulWidget {
  final Map<String, dynamic> character;

  const CharacterDetailScreen({super.key, required this.character});

  
  State<CharacterDetailScreen> createState() => _CharacterDetailScreenState();
}

class _CharacterDetailScreenState extends State<CharacterDetailScreen> {
  Map<String, dynamic> get character => widget.character;

  Color _getStatusColor(String status) {
    switch (status.toLowerCase()) {
      case 'alive':
        return Colors.green;
      case 'dead':
        return Colors.red;
      default:
        return Colors.grey;
    }
  }

使用getter来简化对character的访问。

_getStatusColor方法根据角色状态返回对应的颜色。

收藏功能

_toggleFavorite方法处理收藏操作:

  void _toggleFavorite() {
    final favorites = context.read<FavoritesProvider>();
    final isFav = favorites.isFavorite(character['id'].toString(), 'rick_morty');
    
    favorites.toggleFavorite(FavoriteItem(
      id: character['id'].toString(),
      type: 'rick_morty',
      name: character['name'] ?? '',
      imageUrl: character['image'],
      data: character,
    ));
    
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(isFav ? '已取消收藏' : '已添加到收藏'),
        duration: const Duration(seconds: 1),
      ),
    );
  }

使用context.read来获取FavoritesProvider。

创建一个FavoriteItem对象,包含角色的所有必要信息。

调用toggleFavorite来切换收藏状态。

显示SnackBar提示用户操作结果。

页面的整体布局

页面用CustomScrollView实现复杂的滚动效果:

  
  Widget build(BuildContext context) {
    final episodes = character['episode'] as List? ?? [];

    return Scaffold(
      body: CustomScrollView(
        slivers: [
          SliverAppBar(
            expandedHeight: 350,
            pinned: true,
            flexibleSpace: FlexibleSpaceBar(
              background: Stack(
                fit: StackFit.expand,
                children: [
                  AppNetworkImage(imageUrl: character['image'] ?? '', fit: BoxFit.cover, borderRadius: BorderRadius.zero),
                  Container(
                    decoration: BoxDecoration(
                      gradient: LinearGradient(
                        begin: Alignment.topCenter,
                        end: Alignment.bottomCenter,
                        colors: [Colors.transparent, Colors.black.withOpacity(0.7)],
                      ),
                    ),
                  ),
                ],
              ),
            ),

SliverAppBar的expandedHeight为350,这样能充分展示角色头像

pinned: true表示AppBar滚动到顶部时会固定。

背景用Stack实现,包含角色图片和一个渐变遮罩。渐变遮罩从透明到黑色,这样能提高文字的可读性

收藏按钮

AppBar右上角有一个收藏按钮:

            actions: [
              Consumer<FavoritesProvider>(
                builder: (context, favorites, _) {
                  final isFav = favorites.isFavorite(character['id'].toString(), 'rick_morty');
                  return IconButton(
                    icon: Icon(isFav ? Icons.favorite : Icons.favorite_border, color: Colors.white),
                    onPressed: _toggleFavorite,
                  );
                },
              ),
            ],

使用Consumer来监听FavoritesProvider的变化。

根据收藏状态显示不同的图标:已收藏时显示实心心形,未收藏时显示空心心形。

角色基本信息

SliverToBoxAdapter用来展示角色的基本信息:

          SliverToBoxAdapter(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Row(
                    children: [
                      Expanded(
                        child: Text(character['name'] ?? '', style: Theme.of(context).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold)),
                      ),
                      Container(
                        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
                        decoration: BoxDecoration(
                          color: _getStatusColor(character['status'] ?? ''),
                          borderRadius: BorderRadius.circular(20),
                        ),
                        child: Text(character['status'] ?? '', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
                      ),
                    ],
                  ),

角色名称用headlineMedium样式,加粗显示。

状态用一个圆角容器展示,背景颜色根据状态变化。

角色的物种和性别信息:

                  const SizedBox(height: 8),
                  Text('${character['species'] ?? ''} • ${character['gender'] ?? ''}', style: TextStyle(fontSize: 16, color: Colors.grey[600])),

用•分隔物种和性别,这样能清晰地展示多个属性

详细信息卡片

_buildInfoCard方法展示角色的详细信息:

                  const SizedBox(height: 24),
                  _buildInfoCard(context),

_buildInfoCard的实现:

  Widget _buildInfoCard(BuildContext context) {
    final origin = character['origin'] as Map<String, dynamic>?;
    final location = character['location'] as Map<String, dynamic>?;

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            _buildInfoRow(Icons.public, '起源', origin?['name'] ?? 'Unknown'),
            const Divider(),
            _buildInfoRow(Icons.location_on, '当前位置', location?['name'] ?? 'Unknown'),
            const Divider(),
            _buildInfoRow(Icons.category, '物种', character['species'] ?? 'Unknown'),
            const Divider(),
            _buildInfoRow(Icons.person, '性别', character['gender'] ?? 'Unknown'),
          ],
        ),
      ),
    );
  }

用Card包装详细信息,这样能突出显示这些信息

每个信息用_buildInfoRow来显示,用Divider分隔。

_buildInfoRow方法创建信息行:

  Widget _buildInfoRow(IconData icon, String label, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8),
      child: Row(
        children: [
          Icon(icon, size: 20, color: Colors.grey),
          const SizedBox(width: 12),
          Text(label, style: const TextStyle(color: Colors.grey)),
          const Spacer(),
          Flexible(child: Text(value, style: const TextStyle(fontWeight: FontWeight.w500), textAlign: TextAlign.end)),
        ],
      ),
    );
  }

每行包含一个图标、标签和值。

用Spacer把值推到右边。

用Flexible包装值,防止文字过长时溢出。

出场剧集列表

出场剧集用Wrap展示:

                  const SizedBox(height: 24),
                  Text('出场剧集 (${episodes.length})', style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
                  const SizedBox(height: 12),
                  Wrap(
                    spacing: 8,
                    runSpacing: 8,
                    children: episodes.take(20).map((e) {
                      final episodeNum = e.toString().split('/').last;
                      return Chip(label: Text('EP $episodeNum'));
                    }).toList(),
                  ),
                  if (episodes.length > 20)
                    Padding(
                      padding: const EdgeInsets.only(top: 8),
                      child: Text('还有 ${episodes.length - 20} 集...', style: const TextStyle(color: Colors.grey)),
                    ),

用Wrap展示剧集,这样能自动换行

只显示前20集,如果超过20集就显示"还有X集…"的提示。

从剧集URL中提取剧集号。

总结

这篇文章我们实现了一个信息丰富的角色详情页面。涉及到的知识点包括:

  • CustomScrollView - 实现复杂的滚动效果
  • SliverAppBar - 创建可展开的AppBar
  • 渐变遮罩 - 使用LinearGradient提高文字可读性
  • 收藏功能 - 集成收藏功能到详情页面
  • 信息展示 - 使用Card、Row、Icon等组件清晰地展示信息
  • 列表展示 - 使用Wrap实现自动换行的列表

角色详情页面虽然功能相对简单,但通过精心的设计和布局,能为用户提供一个沉浸式的角色浏览体验


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

Logo

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

更多推荐