通过网盘分享的文件: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