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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.07 15:40:28