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
相关产品推荐
相关产品推荐

