You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter技术问题:不使用AnimatedList实现列表元素移除动画

为列表元素移除添加动画(禁用AnimatedList)

核心思路

通过Overlay临时渲染待移除元素的镜像,在数据直接删除后,让镜像播放退出动画,模拟原元素的移除效果。既不延长原元素在数据模型中的生命周期,也无需依赖AnimatedList组件。

实现步骤

  1. 给每个列表项绑定唯一的GlobalKey,用于获取元素在屏幕上的位置和尺寸。
  2. 触发移除操作时:
    • 通过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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 00:42:47