文章目录

系列文章

1 列表Item动画效果图

2 列表动画 AnimatedList

  • 2.1 实现Item淡入淡出动画
  • 2.1.1 AnimatedList 列表展示
  • 2.1.2 编写淡入淡出动画效果的Item Widget
  • 2.1.3 添加或删除时触发动画效果
  • 2.1.4 修改动画持续时间
  • 2.1.5 淡入淡出动画图
  • 2.2 实现Item左进左出非线性动画
  • 2.3 设置初次加载时也展示动画效果
  • 3 完整代码


1 列表Item动画效果图

flutter Listview item flutter listview item左滑_Item动画


2 列表动画 AnimatedList

AnimatedList 是Flutter提供的一个可以在插入或移除Item时为Item设置动画的列表Widget。

2.1 实现Item淡入淡出动画

淡入淡出效果使用 FadeTransition实现,若不熟悉该控件的用法,建议先看博客Flutter 平移动画 — 4种实现方式

2.1.1 AnimatedList 列表展示

AnimatedList 的主要的属性

  • Widget Function(BuildContext context, int index, Animation animation) itemBuilder 用于构建Item,可在回调中animation的基础上定义Item动画
  • int initialItemCount 初始的item数量
  • GlobalKey<AnimatedListState> key 可通过 AnimatedListState 来添加或删除Item,以触发动画效果

以下代码为实现 AnimatedList的列表展示,并无动画效果

class _AnimatedListPageState extends State<AnimatedListPage> {
  /// AnimatedList 的 key  可通过AnimatedListState来添加或删除Item
  final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
  final List<String> _items = ["Item 0", "Item 1"];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(title: const Text('AnimatedList'), centerTitle: true),
      body: AnimatedList(
        key: _listKey,
        initialItemCount: _items.length,
        itemBuilder: (context, index, animation) {
          return _buildItem(_items[index]);
        },
      ),
    );
  }

  /// 无动画效果的Item
  Widget _buildItem(String item) {
    return Container(
      height: 44.0,
      margin: const EdgeInsets.only(bottom: 1),
      color: Colors.black,
      alignment: Alignment.center,
      child: Text(item, style: const TextStyle(color: Colors.white)),
    );
  }
}

2.1.2 编写淡入淡出动画效果的Item Widget

在无动画的Item基础上套一层Widgt FadeTransition,实现淡入淡出效果。

/// 淡入淡出的Widget
  Widget _buildFadeWidget(
    Widget child,
    Animation<double> animation,
  ) {
    return FadeTransition(
      opacity: Tween<double>(
        begin: 0,
        end: 1,
      ).animate(animation),
      child: child,
    );
  }

更改AnimatedList - itemBuilder 中返回的ItemWidget

AnimatedList(
  key: _listKey,
  initialItemCount: _items.length,
  itemBuilder: (context, index, animation) {
    var itemChild = _buildItem(_items[index]);
    return _buildFadeWidget(itemChild, animation);
  },

2.1.3 添加或删除时触发动画效果

到现在为止AnimatedList仍无动画效果,还需要使用AnimatedListState来添加或删除Item来触发动画效果。

/// 在列表的尾部添加Item
  void _addItem() {
    var nextItemIndex = _items.length;
    _items.add('Item $nextItemIndex');
    _listKey.currentState?.insertItem(nextItemIndex);
  }

  /// 从列表中间位置移除Item
  void _removeItem() {
    if (_items.isEmpty) {
      return;
    }
    int index = _items.length ~/ 2;

    /// 列表删除时,索引就不可用了,所以需要得到删除的值以及对应的Widget 来展示还在刷新的列表
    String item = _items.removeAt(index);
    _listKey.currentState?.removeItem(
      index,
      (context, animation) => _buildFadeWidget(_buildItem(item), animation),
    );
  }

查看此时动画效果

flutter Listview item flutter listview item左滑_列表动画_02


2.1.4 修改动画持续时间

从效果图看感觉动画太快了,需要修改一下动画效果。

AnimatedListState添加或删除item的时候还可指定动画的持续时间。

/// 设置动画的持续时间为2秒
    _listKey.currentState?.insertItem(
      nextItemIndex,
      duration: const Duration(seconds: 2),
    );
    
    _listKey.currentState?.removeItem(
      index,
      (context, animation) => _buildFadeWidget(_buildItem(item), animation),
      duration: const Duration(seconds: 2),
    );

2.1.5 淡入淡出动画图

flutter Listview item flutter listview item左滑_左进左出动画_03


2.2 实现Item左进左出非线性动画

左进左出动画效果使用 SlideTransition 平移动画实现,若不熟悉该控件的用法,建议先看博客 Flutter 淡入淡出与逐渐出现动画

实现方式与实现Item淡入淡出动画差不多,只需要在ItemWidget上套一层SlideTransition即可。其它的就不详细描述了,具体的看后面列出的代码。

/// 左进左出动画 Widget
  Widget _buildSlideWidget(
    Widget child,
    Animation<double> animation,
  ) {
    return SlideTransition(
      position: Tween<Offset>(
        begin: const Offset(-1, 0),
        end: const Offset(0, 0),
      ).animate(
        /// 使用非线性动画展示
        CurvedAnimation(parent: animation, curve: Curves.elasticInOut),
      ),
      child: child,
    );
  }

效果图如下

flutter Listview item flutter listview item左滑_Item动画_04


2.3 设置初次加载时也展示动画效果

  1. 首先抓取数据
  2. for循环添加数据
void _loadData() async {
    for (int i = 0; i < 6; i++) {
      //  等待500ms
      await Future.delayed(const Duration(milliseconds: 500));
      _items.add('Item $i');
      _listKey.currentState?.insertItem(
        i,
        duration: const Duration(milliseconds: 500),
      );
    }
  }

3 完整代码

class AnimatedListPage extends StatefulWidget {
  const AnimatedListPage({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() => _AnimatedListPageState();
}

class _AnimatedListPageState extends State<AnimatedListPage> {
  /// AnimatedList 的 key  可通过AnimatedListState来添加或删除Item
  final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
  final List<String> _items = [];

  @override
  void initState() {
    super.initState();
    _loadData();
  }

  void _loadData() async {
    for (int i = 0; i < 6; i++) {
      //  等待500ms
      await Future.delayed(const Duration(milliseconds: 500));
      _items.add('Item $i');
      _listKey.currentState?.insertItem(
        i,
        duration: const Duration(milliseconds: 500),
      );
    }
  }

  /// 在列表的尾部添加Item
  void _addItem() {
    var nextItemIndex = _items.length;
    _items.add('Item $nextItemIndex');
    _listKey.currentState?.insertItem(
      nextItemIndex,
      duration: const Duration(seconds: 2),
    );
  }

  /// 从列表中间位置移除Item
  void _removeItem() {
    if (_items.isEmpty) {
      return;
    }
    int index = _items.length ~/ 2;

    /// 列表删除时,索引就不可用了,所以需要得到删除的值以及对应的Widget 来展示还在刷新的列表
    String item = _items.removeAt(index);
    _listKey.currentState?.removeItem(
      index,
      (context, animation) => _buildSlideWidget(_buildItem(item), animation),
      duration: const Duration(seconds: 2),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(
        title: const Text('AnimatedList'),
        centerTitle: true,
        actions: [
          IconButton(
            onPressed: _removeItem,
            icon: const Icon(Icons.remove),
          ),
          IconButton(
            onPressed: _addItem,
            icon: const Icon(Icons.add),
          ),
          const SizedBox(width: 8),
        ],
      ),
      body: AnimatedList(
        key: _listKey,
        initialItemCount: _items.length,
        itemBuilder: (context, index, animation) {
          var itemChild = _buildItem(_items[index]);
          return _buildSlideWidget(itemChild, animation);
        },
      ),
    );
  }

  /// 无动画效果的Item
  Widget _buildItem(String item) {
    return Container(
      height: 44.0,
      margin: const EdgeInsets.only(bottom: 1),
      color: Colors.black,
      alignment: Alignment.center,
      child: Text(item, style: const TextStyle(color: Colors.white)),
    );
  }

  /// 淡入淡出的Widget
  Widget _buildFadeWidget(
    Widget child,
    Animation<double> animation,
  ) {
    return FadeTransition(
      opacity: Tween<double>(begin: 0, end: 1).animate(animation),
      child: child,
    );
  }

  /// 左进左出动画 Widget
  Widget _buildSlideWidget(
    Widget child,
    Animation<double> animation,
  ) {
    return SlideTransition(
      position: Tween<Offset>(
        begin: const Offset(-1, 0),
        end: const Offset(0, 0),
      ).animate(
        /// 使用非线性动画展示
        CurvedAnimation(parent: animation, curve: Curves.elasticInOut),
      ),
      child: child,
    );
  }
}