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

Flutter基于AnimationController实现带过渡动画的可展开列表

Flutter 带动画的折叠展开列表实现方案

实现思路

我们可以借助Flutter自带的动画体系配合Stack/AnimatedPositioned实现需求效果,核心逻辑拆分如下:

  • 用AnimationController控制整个动画进度,时长建议设置在300~500ms保证过渡流畅
  • 点击列表项时记录当前点击的item索引,给所有列表项绑定位置动画:
    • 索引小于点击项的item:Y轴偏移量逐渐变为负的自身高度,最终移出屏幕顶部
    • 索引等于点击项的item:Y轴偏移量逐渐变为0(固定在屏幕顶部),同时高度从原列表项高度逐渐扩展到屏幕高度,同步渐显子树内容
    • 索引大于点击项的item:Y轴偏移量逐渐变为正的屏幕高度,最终移出屏幕底部
  • 动画结束后隐藏所有主列表项,仅渲染被点击项的子组件树
  • 若需要支持退回原列表,给子组件树加返回逻辑,反向执行上述动画即可

完整可运行示例代码

import 'package:flutter/material.dart';

void main() => runApp(const ExpandableListApp());

class ExpandableListApp extends StatelessWidget {
  const ExpandableListApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const AnimatedExpandableList(),
    );
  }
}

class AnimatedExpandableList extends StatefulWidget {
  const AnimatedExpandableList({super.key});

  @override
  State<AnimatedExpandableList> createState() => _AnimatedExpandableListState();
}

class _AnimatedExpandableListState extends State<AnimatedExpandableList> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  int? _tappedIndex;
  final double _itemHeight = 60;
  final List<String> _mainItems = ['列表项1', '列表项2', '列表项3', '列表项4'];

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 400),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _onItemTap(int index) {
    setState(() => _tappedIndex = index);
    _controller.forward(from: 0);
  }

  void _onBackTap() {
    _controller.reverse().then((_) => setState(() => _tappedIndex = null));
  }

  @override
  Widget build(BuildContext context) {
    final screenHeight = MediaQuery.of(context).size.height;
    return Scaffold(
      appBar: AppBar(title: const Text('动画展开列表示例')),
      body: _tappedIndex == null
          ? ListView.builder(
              itemCount: _mainItems.length,
              itemBuilder: (context, index) => GestureDetector(
                onTap: () => _onItemTap(index),
                child: Container(
                  height: _itemHeight,
                  margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
                  decoration: BoxDecoration(
                    color: Colors.blue[100],
                    borderRadius: BorderRadius.circular(8),
                  ),
                  child: Center(child: Text(_mainItems[index], style: const TextStyle(fontSize: 16))),
                ),
              ),
            )
          : AnimatedBuilder(
              animation: _controller,
              builder: (context, child) {
                return Stack(
                  children: List.generate(_mainItems.length, (index) {
                    double offsetY;
                    if (index < _tappedIndex!) {
                      // 点击项上方的元素向上移出
                      offsetY = -_controller.value * (index + 1) * (_itemHeight + 16);
                    } else if (index == _tappedIndex!) {
                      // 点击项位置固定,高度扩展
                      offsetY = 0;
                      return Positioned(
                        top: offsetY,
                        left: 16,
                        right: 16,
                        height: _itemHeight + _controller.value * (screenHeight - _itemHeight - kToolbarHeight - 32),
                        child: Container(
                          decoration: BoxDecoration(
                            color: Colors.blue[100],
                            borderRadius: BorderRadius.circular(8),
                          ),
                          child: Stack(
                            children: [
                              // 原列表项内容逐渐消失
                              Opacity(
                                opacity: 1 - _controller.value,
                                child: Center(child: Text(_mainItems[index], style: const TextStyle(fontSize: 16))),
                              ),
                              // 子树内容逐渐显示
                              Opacity(
                                opacity: _controller.value,
                                child: Column(
                                  children: [
                                    const SizedBox(height: 16),
                                    Row(
                                      children: [
                                        IconButton(
                                          onPressed: _onBackTap,
                                          icon: const Icon(Icons.arrow_back),
                                        ),
                                        Text('${_mainItems[index]}的子页面', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                                      ],
                                    ),
                                    const Expanded(child: Center(child: Text('这里是当前列表项的子组件树内容'))),
                                  ],
                                ),
                              )
                            ],
                          ),
                        ),
                      );
                    } else {
                      // 点击项下方的元素向下移出
                      offsetY = _controller.value * screenHeight + index * (_itemHeight + 16);
                    }
                    return Positioned(
                      top: offsetY + index * (_itemHeight + 16),
                      left: 16,
                      right: 16,
                      height: _itemHeight,
                      child: Container(
                        decoration: BoxDecoration(
                          color: Colors.blue[100],
                          borderRadius: BorderRadius.circular(8),
                        ),
                        child: Center(child: Text(_mainItems[index], style: const TextStyle(fontSize: 16))),
                      ),
                    );
                  }),
                );
              },
            ),
    );
  }
}

效果说明

运行代码后默认展示4个主列表项,点击任意一项即可触发需求描述的动画效果:上方列表项上移消失、下方列表项下移消失,被点击的项平滑展开为全屏子页面,点击子页面左上角返回按钮即可反向动画回到原列表。你可以根据自己的业务需求调整动画时长、列表项样式、子组件树的内容布局。

内容的提问来源于stack exchange,提问作者Franci Gjeci

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.27 03:06:04