Flutter卡片高度展开动画实现:点击卡片旋转箭头并展开子卡片
解决Flutter动态高度卡片的展开动画问题
哈哈,这个场景我在做Flutter项目时也遇到过!因为卡片里的内容高度不固定,直接写死动画高度肯定不行,得让Flutter先帮我们算出真实高度再做动画。给你两个靠谱的原生解决方案:
方案一:用GlobalKey获取真实高度(最灵活)
核心思路是先让目标卡片完成一次布局,获取它的真实高度,再把这个高度作为动画的目标值来驱动展开/收起效果。
步骤和代码修改:
- 先定义必要的变量和全局Key:
bool _notfall = false; late AnimationController _rotateArrowController; late Animation<double> _rotateAnimation; late AnimationController _cardAnimationController; late Animation<double> _cardAnimation; final GlobalKey _expandCardKey = GlobalKey(); // 用来获取卡片尺寸的Key double _targetCardHeight = 0;
- 在
initState里初始化动画控制器:
@override void initState() { super.initState(); // 箭头旋转动画控制器 _rotateArrowController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); _rotateAnimation = Tween<double>(begin: 0, end: 1).animate( CurvedAnimation(parent: _rotateArrowController, curve: Curves.easeInOut), ); // 卡片高度动画控制器 _cardAnimationController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); }
- 修改点击事件的逻辑,先获取高度再启动动画:
onTap: () { setState(() { _notfall = !_notfall; }); if (_notfall) { // 等待当前帧布局完成,才能获取到卡片的真实尺寸 WidgetsBinding.instance.addPostFrameCallback((_) { final RenderBox renderBox = _expandCardKey.currentContext!.findRenderObject() as RenderBox; _targetCardHeight = renderBox.size.height; // 绑定高度动画的目标值 _cardAnimation = Tween<double>(begin: 0, end: _targetCardHeight).animate( CurvedAnimation(parent: _cardAnimationController, curve: Curves.easeInOut), ); // 启动箭头旋转和卡片展开动画 _rotateArrowController.forward(); _cardAnimationController.forward(); }); } else { // 收起动画 _rotateArrowController.reverse(); _cardAnimationController.reverse(); } },
- 最后修改展开卡片的代码,用
AnimatedBuilder和ClipRect实现裁剪动画:
if (_notfall) { return AnimatedBuilder( animation: _cardAnimation, builder: (context, child) { return ClipRect( // 用动画值控制容器高度,裁剪超出部分 child: SizedBox( height: _cardAnimation.value, child: child, ), ); }, child: Card( key: _expandCardKey, // 绑定全局Key color: Colors.white70, child: Container( // 这里放你的任意Widget内容,高度会自动计算 child: Column( children: const [ Text("这是第一行内容"), Text("这是第二行内容"), // ... 更多Widget ], ), ), ), ); } else { return const SizedBox.shrink(); // 收起时隐藏 }
方案二:用SizeTransition简化实现(更简洁)
如果你不想手动处理高度计算,可以用Flutter自带的SizeTransition组件,它会自动根据子Widget的尺寸生成动画,不过需要注意它的动画轴是基于父容器的:
代码示例:
bool _notfall = false; late AnimationController _rotateArrowController; late Animation<double> _rotateAnimation; late Animation<double> _sizeAnimation; late AnimationController _sizeController; @override void initState() { super.initState(); _rotateArrowController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); _rotateAnimation = Tween<double>(begin: 0, end: 1).animate( CurvedAnimation(parent: _rotateArrowController, curve: Curves.easeInOut), ); _sizeController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); _sizeAnimation = CurvedAnimation( parent: _sizeController, curve: Curves.easeInOut, ); } // 点击事件 onTap: () { setState(() { _notfall = !_notfall; }); if (_notfall) { _rotateArrowController.forward(); _sizeController.forward(); } else { _rotateArrowController.reverse(); _sizeController.reverse(); } }, // 展开卡片的代码 if (_notfall) { return SizeTransition( sizeFactor: _sizeAnimation, axisAlignment: -1.0, // 从顶部开始展开 child: Card( color: Colors.white70, child: Container( // 你的任意Widget内容 child: Column( children: const [ Text("动态高度的内容"), // ... 更多Widget ], ), ), ), ); } else { return const SizedBox.shrink(); }
注意事项:
- 方案一更灵活,适合需要精确控制高度动画的场景;方案二更简洁,适合快速实现需求。
- 记得在页面销毁时dispose动画控制器,避免内存泄漏:
@override void dispose() { _rotateArrowController.dispose(); _cardAnimationController.dispose(); // 或者_sizeController.dispose(); super.dispose(); }
内容的提问来源于stack exchange,提问作者Anton Schrage
相关产品推荐
相关产品推荐

