Flutter for OpenHarmony 微动漫App实战:列表项组件实现

通过网盘分享的文件:flutter1.zip

链接: https://pan.baidu.com/s/1jkLZ9mZXjNm0LgP6FTVRzw 提取码: 2t97

除了网格卡片,列表形式也是展示动漫的常用方式。列表项更紧凑,一屏能显示更多内容,适合收藏、历史记录等需要快速浏览的场景。

这篇文章会实现动漫列表项组件,重点讲解 Dismissible 滑动删除、ListTile 的灵活运用,以及如何设计一个既美观又实用的列表项。

---

列表项 vs 卡片

什么时候用列表项,什么时候用卡片?

卡片适合:封面是重点的场景,如首页推荐、搜索结果。

列表项适合:需要快速浏览的场景,如收藏列表、历史记录。

列表项信息密度更高,一屏能看到更多内容。卡片视觉冲击力更强,封面更吸引眼球。


组件基础结构

dart 复制代码
import 'package:flutter/material.dart';
import '../models/anime.dart';
import '../screens/anime_detail_screen.dart';

class AnimeListTile extends StatelessWidget {
  final Anime anime;
  final VoidCallback? onDelete;

  const AnimeListTile({super.key, required this.anime, this.onDelete});

anime 是必需参数,包含动漫数据。

onDelete 是可选的删除回调,传入后列表项支持滑动删除。


Dismissible 滑动删除

dart 复制代码
@override
Widget build(BuildContext context) {
  return Dismissible(
    key: Key(anime.malId.toString()),
    direction: onDelete != null ? DismissDirection.endToStart : DismissDirection.none,
    onDismissed: (_) => onDelete?.call(),
    background: Container(
      alignment: Alignment.centerRight,
      padding: const EdgeInsets.only(right: 20),
      color: Colors.red,
      child: const Icon(Icons.delete, color: Colors.white),
    ),
    child: ListTile(
      // 列表项内容
    ),
  );
}

Dismissible 让子组件支持滑动删除。

key 必须唯一,用动漫 ID 转成字符串。

direction 控制滑动方向,endToStart 是从右往左滑。如果没有传 onDelete,设为 none 禁用滑动。

onDismissed 在滑动完成后触发,调用删除回调。


滑动背景

dart 复制代码
background: Container(
  alignment: Alignment.centerRight,
  padding: const EdgeInsets.only(right: 20),
  color: Colors.red,
  child: const Icon(Icons.delete, color: Colors.white),
),

background 是滑动时露出的背景。红色背景 + 删除图标,用户一看就知道是删除操作。

alignment: Alignment.centerRight 让图标靠右居中,和滑动方向一致。


Dismissible 的更多配置

dart 复制代码
Dismissible(
  key: Key(anime.malId.toString()),
  direction: DismissDirection.endToStart,
  confirmDismiss: (direction) async {
    return await showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('确认删除'),
        content: const Text('确定要删除这条记录吗?'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context, false),
            child: const Text('取消'),
          ),
          TextButton(
            onPressed: () => Navigator.pop(context, true),
            child: const Text('删除'),
          ),
        ],
      ),
    );
  },
  onDismissed: (_) => onDelete?.call(),
  // 其他属性
)

confirmDismiss 在滑动完成前触发,返回 true 才会真正删除。可以弹出确认对话框,防止误删。


ListTile 基础结构

dart 复制代码
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(),
    ),
  ),
  title: Text(
    anime.title,
    maxLines: 2,
    overflow: TextOverflow.ellipsis,
    style: const TextStyle(fontWeight: FontWeight.w600),
  ),
  subtitle: Row(
    children: [
      // 评分和类型
    ],
  ),
  trailing: const Icon(Icons.chevron_right),
),

ListTile 是 Flutter 内置的列表项组件,提供了标准的布局。

onTap 处理点击,跳转到详情页。

leading 放封面图,title 放标题,subtitle 放评分和类型,trailing 放箭头图标。


封面图处理

dart 复制代码
leading: ClipRRect(
  borderRadius: BorderRadius.circular(8),
  child: SizedBox(
    width: 50,
    height: 70,
    child: _buildImage(),
  ),
),

ClipRRect 裁剪圆角。SizedBox 固定尺寸为 50x70,竖向比例适合动漫封面。

dart 复制代码
Widget _buildImage() {
  final imageUrl = anime.imageUrl;
  if (imageUrl == null || imageUrl.isEmpty) {
    return Container(
      color: Colors.grey[300],
      child: const Icon(Icons.movie),
    );
  }

  return Image.network(
    imageUrl,
    fit: BoxFit.cover,
    loadingBuilder: (context, child, loadingProgress) {
      if (loadingProgress == null) return child;
      return Container(color: Colors.grey[300]);
    },
    errorBuilder: (context, error, stackTrace) {
      return Container(
        color: Colors.grey[300],
        child: const Icon(Icons.movie),
      );
    },
  );
}

图片加载处理和卡片组件一样:空 URL、加载中、加载失败都有对应的 UI。


标题样式

dart 复制代码
title: Text(
  anime.title,
  maxLines: 2,
  overflow: TextOverflow.ellipsis,
  style: const TextStyle(fontWeight: FontWeight.w600),
),

标题最多 2 行,超出显示省略号。fontWeight.w600 半粗体,突出但不过分。


副标题信息

dart 复制代码
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!),
  ],
),

副标题用 Row 水平排列评分和类型。

评分用星星图标 + 数字,类型直接显示文本(如 TV、Movie)。

...[] 展开操作符配合 if,条件性添加多个元素。


右侧箭头

dart 复制代码
trailing: const Icon(Icons.chevron_right),

Icons.chevron_right 是向右的箭头,提示用户可以点击进入详情。

这是列表项的常见设计,用户一看就知道可以点击。


组件的使用方式

dart 复制代码
// 基本使用(不支持删除)
AnimeListTile(anime: anime)

// 支持滑动删除
AnimeListTile(
  anime: anime,
  onDelete: () {
    // 删除逻辑
    setState(() {
      favorites.remove(anime);
    });
  },
)

// 在 ListView 中使用
ListView.builder(
  itemCount: animeList.length,
  itemBuilder: (_, i) => AnimeListTile(
    anime: animeList[i],
    onDelete: () => _removeAnime(i),
  ),
)

通过 onDelete 参数控制是否支持滑动删除,灵活适应不同场景。


添加更多信息

可以在副标题显示更多信息:

dart 复制代码
subtitle: Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    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!),
      ],
    ),
    if (anime.episodes != null)
      Text(
        '${anime.episodes}集',
        style: TextStyle(fontSize: 12, color: Colors.grey[600]),
      ),
  ],
),

Column 垂直排列多行信息。集数用小字号灰色,作为辅助信息。


添加收藏按钮

可以在 trailing 放收藏按钮:

dart 复制代码
class AnimeListTile extends StatelessWidget {
  final Anime anime;
  final VoidCallback? onDelete;
  final bool isFavorite;
  final VoidCallback? onFavoriteToggle;

  const AnimeListTile({
    super.key,
    required this.anime,
    this.onDelete,
    this.isFavorite = false,
    this.onFavoriteToggle,
  });

新增收藏相关的参数。

dart 复制代码
trailing: Row(
  mainAxisSize: MainAxisSize.min,
  children: [
    if (onFavoriteToggle != null)
      IconButton(
        icon: Icon(
          isFavorite ? Icons.favorite : Icons.favorite_border,
          color: isFavorite ? Colors.red : null,
        ),
        onPressed: onFavoriteToggle,
      ),
    const Icon(Icons.chevron_right),
  ],
),

mainAxisSize: MainAxisSize.min 让 Row 只占用需要的空间。

收藏按钮和箭头并排显示。


双向滑动

可以支持左滑删除、右滑收藏:

dart 复制代码
Dismissible(
  key: Key(anime.malId.toString()),
  background: Container(
    alignment: Alignment.centerLeft,
    padding: const EdgeInsets.only(left: 20),
    color: Colors.green,
    child: const Icon(Icons.favorite, color: Colors.white),
  ),
  secondaryBackground: Container(
    alignment: Alignment.centerRight,
    padding: const EdgeInsets.only(right: 20),
    color: Colors.red,
    child: const Icon(Icons.delete, color: Colors.white),
  ),
  confirmDismiss: (direction) async {
    if (direction == DismissDirection.startToEnd) {
      // 右滑收藏
      onFavoriteToggle?.call();
      return false;  // 不删除,只触发收藏
    } else {
      // 左滑删除
      return true;
    }
  },
  onDismissed: (_) => onDelete?.call(),
  child: ListTile(...),
)

background 是右滑时的背景,secondaryBackground 是左滑时的背景。

confirmDismiss 根据滑动方向执行不同操作。右滑收藏后返回 false,列表项不会消失。


添加时间信息

历史记录需要显示浏览时间:

dart 复制代码
class AnimeListTile extends StatelessWidget {
  final Anime anime;
  final DateTime? viewedAt;
  // 其他参数

  const AnimeListTile({
    super.key,
    required this.anime,
    this.viewedAt,
    // 其他参数
  });
dart 复制代码
subtitle: Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    Row(
      children: [
        // 评分和类型
      ],
    ),
    if (viewedAt != null)
      Text(
        _formatTime(viewedAt!),
        style: TextStyle(fontSize: 12, color: Colors.grey[600]),
      ),
  ],
),
dart 复制代码
String _formatTime(DateTime time) {
  final now = DateTime.now();
  final diff = now.difference(time);
  
  if (diff.inMinutes < 1) return '刚刚';
  if (diff.inHours < 1) return '${diff.inMinutes}分钟前';
  if (diff.inDays < 1) return '${diff.inHours}小时前';
  if (diff.inDays < 7) return '${diff.inDays}天前';
  
  return '${time.month}月${time.day}日';
}

时间格式化成相对时间,"刚刚"、"5分钟前"、"3天前",比绝对时间更友好。


深色模式适配

dart 复制代码
Widget _buildImage() {
  final isDark = Theme.of(context).brightness == Brightness.dark;
  final placeholderColor = isDark ? Colors.grey[800] : Colors.grey[300];
  
  final imageUrl = anime.imageUrl;
  if (imageUrl == null || imageUrl.isEmpty) {
    return Container(
      color: placeholderColor,
      child: Icon(Icons.movie, color: isDark ? Colors.grey[600] : Colors.grey),
    );
  }

  return Image.network(
    imageUrl,
    fit: BoxFit.cover,
    loadingBuilder: (context, child, loadingProgress) {
      if (loadingProgress == null) return child;
      return Container(color: placeholderColor);
    },
    errorBuilder: (context, error, stackTrace) {
      return Container(
        color: placeholderColor,
        child: Icon(Icons.movie, color: isDark ? Colors.grey[600] : Colors.grey),
      );
    },
  );
}

深色模式下用深灰色占位,图标也用深灰色。


动画效果

给列表项添加入场动画:

dart 复制代码
ListView.builder(
  itemCount: animeList.length,
  itemBuilder: (_, i) {
    return TweenAnimationBuilder<double>(
      tween: Tween(begin: 0, end: 1),
      duration: Duration(milliseconds: 200 + i * 50),
      builder: (context, value, child) {
        return Opacity(
          opacity: value,
          child: Transform.translate(
            offset: Offset(30 * (1 - value), 0),
            child: child,
          ),
        );
      },
      child: AnimeListTile(anime: animeList[i]),
    );
  },
)

每个列表项从右侧淡入,延迟递增形成依次入场的效果。


小结

列表项组件涉及的技术点:Dismissible 滑动删除ListTile 列表项ClipRRect 圆角裁剪条件渲染展开操作符时间格式化

Dismissible 是实现滑动删除的标准方式,配合 confirmDismiss 可以添加确认对话框或实现双向滑动。

ListTile 提供了标准的列表项布局,leading、title、subtitle、trailing 四个位置覆盖了大多数需求。

好的列表项设计要信息密度适中,主要信息突出,次要信息辅助,交互提示明确。


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

相关推荐
【赫兹威客】浩哥1 小时前
【赫兹威客】Redis安装与验证教程
开发语言·php
froginwe111 小时前
SVG 简介
开发语言
小风呼呼吹儿1 小时前
Flutter 框架跨平台鸿蒙开发 - 种子发芽记录器:记录植物成长的每一刻
android·flutter·华为·harmonyos
宵时待雨2 小时前
数据结构(初阶)笔记归纳7:链表OJ
c语言·开发语言·数据结构·笔记·算法·链表
一人の梅雨2 小时前
中国制造网商品详情接口进阶实战:跨境场景下的差异化适配与问题攻坚
java·前端·javascript
无心水2 小时前
8、吃透Go语言container包:链表(List)与环(Ring)的核心原理+避坑指南
java·开发语言·链表·微服务·架构·golang·list
froginwe112 小时前
Python3 集合
开发语言
lly2024062 小时前
R 绘图 - 条形图
开发语言
Miguo94well2 小时前
Flutter框架跨平台鸿蒙开发——学茶知识APP开发流程
flutter·华为·harmonyos·鸿蒙