Flutter中如何为自定义Expansion Tile添加展开/收起动画?
自定义ExpansionTile添加展开/收起动画方案
我实现了一个自定义ExpansionTile组件,功能完全正常,但希望为其添加展开/收起时的动画效果。尝试使用AnimatedContainer但未达到预期效果,由于对Flutter动画相关知识不太熟悉,恳请提供相关方向的帮助。
当前代码
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData( primaryColor: Color.fromARGB(255, 196, 196, 196), ), home: HomePage(), ); } } class HomePage extends StatefulWidget { const HomePage({super.key}); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { int active = 1; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('App bar')), body: Column( children: [ const SizedBox( height: 20, ), CustomExpansionTile(), ], ), ); } } class CustomExpansionTile extends StatefulWidget { const CustomExpansionTile({super.key}); @override State<CustomExpansionTile> createState() => _CustomExpansionTileState(); } class _CustomExpansionTileState extends State<CustomExpansionTile> { bool isExpanded = false; @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( color: Theme.of(context).primaryColor, borderRadius: isExpanded ? BorderRadius.circular(10) : BorderRadius.circular( 100, ), border: Border.all(width: 2, color: Theme.of(context).primaryColor), ), child: Column( children: [ GestureDetector( onTap: () { setState(() { isExpanded = !isExpanded; }); }, child: Container( decoration: BoxDecoration( color: Theme.of(context).primaryColor, borderRadius: isExpanded ? const BorderRadius.only( topRight: Radius.circular(10), topLeft: Radius.circular(10), ) : BorderRadius.circular(100), ), child: Row( children: [ Expanded( child: Text( 'Title', textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium!.copyWith( fontWeight: isExpanded ? FontWeight.w500 : FontWeight.normal, overflow: TextOverflow.ellipsis, ), maxLines: 1, ), ), Container( padding: const EdgeInsets.all(4.5), decoration: BoxDecoration( color: isExpanded ? Theme.of(context).primaryColor : Colors.white, shape: BoxShape.circle, ), child: Icon( isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, color: Colors.black, ), ) ], ), ), ), if (isExpanded) Container( width: double.infinity, padding: const EdgeInsets.only( top: 10, bottom: 20, right: 10, left: 10), decoration: BoxDecoration( color: Theme.of(context) .primaryColor, // this is important so that GestureDetector takes in the padding in account borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(10), bottomRight: Radius.circular(10), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Wrap( spacing: 10, runSpacing: 10, children: const [ Text('child 1'), Text('child 2'), Text('child 3'), ], ), const SizedBox( height: 20, ), ], ), ), ], ), ); } }
当前效果

解决方案
关键修改点
- 外层容器圆角动画:将外层
Container替换为AnimatedContainer,实现圆角从圆形到普通圆角的平滑过渡 - 箭头旋转动画:用
AnimatedRotation包裹箭头图标,实现展开/收起时的旋转效果 - 内容区域展开动画:用
AnimatedSize包裹展开内容,实现高度从0到自适应的平滑变化,避免内容突兀显示/隐藏
修改后的完整代码
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData( primaryColor: Color.fromARGB(255, 196, 196, 196), ), home: HomePage(), ); } } class HomePage extends StatefulWidget { const HomePage({super.key}); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { int active = 1; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('App bar')), body: Column( children: [ const SizedBox(height: 20), CustomExpansionTile(), ], ), ); } } class CustomExpansionTile extends StatefulWidget { const CustomExpansionTile({super.key}); @override State<CustomExpansionTile> createState() => _CustomExpansionTileState(); } class _CustomExpansionTileState extends State<CustomExpansionTile> { bool isExpanded = false; final Duration _animationDuration = Duration(milliseconds: 300); @override Widget build(BuildContext context) { return AnimatedContainer( duration: _animationDuration, decoration: BoxDecoration( color: Theme.of(context).primaryColor, borderRadius: isExpanded ? BorderRadius.circular(10) : BorderRadius.circular(100), border: Border.all(width: 2, color: Theme.of(context).primaryColor), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ GestureDetector( onTap: () => setState(() => isExpanded = !isExpanded), child: Container( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: Theme.of(context).primaryColor, borderRadius: isExpanded ? BorderRadius.only( topLeft: Radius.circular(10), topRight: Radius.circular(10), ) : BorderRadius.circular(100), ), child: Row( children: [ Expanded( child: Text( 'Title', textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium!.copyWith( fontWeight: isExpanded ? FontWeight.w500 : FontWeight.normal, overflow: TextOverflow.ellipsis, ), maxLines: 1, ), ), AnimatedRotation( duration: _animationDuration, turns: isExpanded ? 0.5 : 0.0, child: Container( padding: const EdgeInsets.all(4.5), decoration: BoxDecoration( color: isExpanded ? Theme.of(context).primaryColor : Colors.white, shape: BoxShape.circle, ), child: Icon( Icons.keyboard_arrow_down, color: Colors.black, ), ), ), ], ), ), ), AnimatedSize( duration: _animationDuration, curve: Curves.easeInOut, child: isExpanded ? Container( width: double.infinity, padding: const EdgeInsets.only( top: 10, bottom: 20, left: 16, right: 16, ), decoration: BoxDecoration( color: Theme.of(context).primaryColor, borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(10), bottomRight: Radius.circular(10), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Wrap( spacing: 10, runSpacing: 10, children: const [ Text('child 1'), Text('child 2'), Text('child 3'), ], ), const SizedBox(height: 20), ], ), ) : SizedBox.shrink(), ), ], ), ); } }
说明
- 统一设置
_animationDuration变量,方便统一调整动画速度 - 箭头通过旋转实现上下切换,无需切换图标,过渡更自然
AnimatedSize自动计算内容高度,实现平滑的展开/收起过渡- 外层
AnimatedContainer负责圆角的平滑变化,与内容动画同步
内容的提问来源于stack exchange,提问作者dipansh
相关产品推荐
相关产品推荐

