Flutter技术问题:不使用AnimatedList实现列表元素移除动画
为列表元素移除添加动画(禁用AnimatedList)
核心思路
通过Overlay临时渲染待移除元素的镜像,在数据直接删除后,让镜像播放退出动画,模拟原元素的移除效果。既不延长原元素在数据模型中的生命周期,也无需依赖AnimatedList组件。
实现步骤
- 给每个列表项绑定唯一的
GlobalKey,用于获取元素在屏幕上的位置和尺寸。 - 触发移除操作时:
- 通过GlobalKey获取原元素的RenderBox信息(位置、大小)。
- 创建一个OverlayEntry,渲染与原元素完全一致的Widget,并放置在原位置。
- 将OverlayEntry插入到Overlay中。
- 从数据列表中直接删除目标元素,调用
setState更新列表(原元素立刻从Widget树中移除)。 - 启动Overlay中镜像元素的退出动画(如淡出、缩放、滑动)。
- 动画结束后,移除OverlayEntry,完成整个流程。
代码示例
import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar(title: const Text('移除动画示例')), body: const ItemList(), ), ); } } class Item { final String id; final String content; Item(this.id, this.content); } class ItemList extends StatefulWidget { const ItemList({super.key}); @override State<ItemList> createState() => _ItemListState(); } class _ItemListState extends State<ItemList> with SingleTickerProviderStateMixin { late AnimationController _animationController; late Animation<double> _opacityAnim; late Animation<double> _scaleAnim; OverlayEntry? _overlayEntry; final List<Item> _items = List.generate(5, (i) => Item('$i', '列表项 ${i+1}')); @override void initState() { super.initState(); _animationController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); _opacityAnim = Tween<double>(begin: 1.0, end: 0.0).animate(_animationController); _scaleAnim = Tween<double>(begin: 1.0, end: 0.5).animate(_animationController); _animationController.addStatusListener((status) { if (status == AnimationStatus.completed) { _overlayEntry?.remove(); _overlayEntry = null; _animationController.reset(); } }); } @override void dispose() { _animationController.dispose(); super.dispose(); } void _removeItem(Item item, GlobalKey key) { // 获取原元素的位置和尺寸 final renderBox = key.currentContext?.findRenderObject() as RenderBox?; if (renderBox == null) { // 无法获取位置,直接删除 setState(() => _items.remove(item)); return; } final offset = renderBox.localToGlobal(Offset.zero); final size = renderBox.size; // 创建Overlay镜像元素 _overlayEntry = OverlayEntry( builder: (context) => Positioned( left: offset.dx, top: offset.dy, width: size.width, height: size.height, child: AnimatedBuilder( animation: _animationController, builder: (context, child) { return Opacity( opacity: _opacityAnim.value, child: Transform.scale( scale: _scaleAnim.value, child: child, ), ); }, child: _buildItem(item, key), // 复用原列表项的Widget ), ), ); // 插入Overlay并启动动画 Overlay.of(context).insert(_overlayEntry!); // 直接删除数据,原元素立刻从列表中消失 setState(() => _items.remove(item)); _animationController.forward(); } Widget _buildItem(Item item, GlobalKey key) { return Container( key: key, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 4)], ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(item.content, style: const TextStyle(fontSize: 16)), IconButton( icon: const Icon(Icons.delete, color: Colors.red), onPressed: () => _removeItem(item, key), ), ], ), ); } @override Widget build(BuildContext context) { return ListView.builder( itemCount: _items.length, itemBuilder: (context, index) { final item = _items[index]; return _buildItem(item, GlobalKey()); }, ); } }
方案说明
- 数据模型中的元素被直接删除,没有额外的生命周期延长(无需标记元素为“待移除”再延迟删除)。
- 利用Overlay临时渲染的镜像元素播放动画,视觉上与原元素移除的效果一致。
- 动画控制器统一管理,避免内存泄漏,动画结束后自动清理Overlay资源。
内容的提问来源于stack exchange,提问作者May
相关产品推荐
相关产品推荐

