如何在Flutter中复现SwiftUI的列表项移除双动画效果
Flutter 复现 SwiftUI 列表项移除动画
需求说明
- 目标动画:列表项点击移除时,同时实现被移除项淡出和剩余项向中间滑动收拢的效果
- 列表基于动态
items数组,点击项自动移除 - 默认
AnimatedList搭配FadeTransition仅能实现淡出效果,需同时实现两种动画
补充思路:使用两个堆叠的盒子,尺寸均匹配内容固有尺寸;仅不可见的占位盒子影响布局并应用SizeTransition,承载实际内容的盒子作为覆盖层应用FadeTransition,且被移除项需绘制在其他项下方。
实现方案
通过自定义列表项的动画组合,结合Stack、SizeTransition和FadeTransition,配合AnimatedList的动画控制器同步两种动画效果:
1. 核心逻辑
每个列表项用Stack包裹两个组件:
- 底层占位组件:不可见,通过
SizeTransition控制尺寸收缩,驱动布局变化,让剩余项平滑滑动 - 上层内容组件:通过
FadeTransition实现淡出,利用Positioned.fill铺满占位组件,保证位置匹配 - 借助
AnimatedList的removeItem方法同步两个动画的控制器,确保动画时长一致
2. 完整代码示例
import 'package:flutter/material.dart'; void main() => runApp(const RemoveItemAnimationApp()); class RemoveItemAnimationApp extends StatelessWidget { const RemoveItemAnimationApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar(title: const Text('移除项动画示例')), body: const RemoveItemList(), ), ); } } class RemoveItemList extends StatefulWidget { const RemoveItemList({super.key}); @override State<RemoveItemList> createState() => _RemoveItemListState(); } class _RemoveItemListState extends State<RemoveItemList> { final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>(); late List<String> _items; @override void initState() { super.initState(); _items = List.generate(10, (index) => '列表项 ${index + 1}'); } void _removeItem(int index) { final removedItem = _items.removeAt(index); _listKey.currentState!.removeItem( index, (context, animation) => _buildAnimatedItem(removedItem, animation), duration: const Duration(milliseconds: 300), ); } Widget _buildAnimatedItem(String item, Animation<double> animation) { return Stack( children: [ // 占位组件:仅用于驱动布局,应用SizeTransition实现收缩 SizeTransition( sizeFactor: animation, axis: Axis.vertical, child: SizedBox( height: 60, // 固定高度,若需自适应可通过LayoutBuilder获取内容尺寸 child: const SizedBox.shrink(), ), ), // 内容组件:应用FadeTransition实现淡出 Positioned.fill( child: FadeTransition( opacity: ReverseAnimation(animation), child: _buildListItem(item, () => _removeItem(_items.indexOf(item))), ), ), ], ); } Widget _buildListItem(String item, VoidCallback onTap) { return Card( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: ListTile( title: Text(item), onTap: onTap, ), ); } @override Widget build(BuildContext context) { return AnimatedList( key: _listKey, initialItemCount: _items.length, itemBuilder: (context, index, animation) { return _buildListItem(_items[index], () => _removeItem(index)); }, ); } }
3. 关键细节
- 自适应尺寸:若列表项高度不固定,可通过
LayoutBuilder获取内容的固有尺寸,让占位组件和内容组件尺寸完全匹配 - 动画方向:
SizeTransition的axis设为Axis.vertical对应垂直列表,水平列表改为Axis.horizontal即可 - 层级控制:
Stack中占位组件在底层,内容组件在上方,移除时占位收缩驱动其他项移动,被移除项自然处于其他项下方 - 动画同步:复用
AnimatedList提供的animation对象,确保淡出和尺寸收缩动画同步执行
内容的提问来源于stack exchange,提问作者user8393310
相关产品推荐
相关产品推荐

