Flutter无依赖实现卡片翻转动画 求技术支持
实现Flutter卡片3D翻转动画(无第三方依赖)
要实现卡片正反面的翻转动画,我们可以利用Flutter自带的AnimationController和Transform组件实现3D旋转效果,无需第三方包。以下是修改后的完整代码:
import 'package:flutter/material.dart'; class NotificationItemCard extends StatefulWidget { const NotificationItemCard({ Key? key, }) : super(key: key); @override State<NotificationItemCard> createState() => _NotificationItemCardState(); } class _NotificationItemCardState extends State<NotificationItemCard> with SingleTickerProviderStateMixin { late bool showCardFrontSide; late AnimationController _animationController; late Animation<double> _rotationAnimation; @override void initState() { showCardFrontSide = true; // 初始化动画控制器,时长300毫秒可按需调整 _animationController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); // 创建旋转动画,从0到π(对应180度旋转) _rotationAnimation = Tween<double>(begin: 0, end: 1).animate(CurvedAnimation( parent: _animationController, curve: Curves.easeInOut, )); super.initState(); } void onChangeView() { if (_animationController.status == AnimationStatus.completed) { _animationController.reverse(); } else if (_animationController.status == AnimationStatus.dismissed) { _animationController.forward(); } // 动画执行到一半时切换正反面(旋转90度时) _animationController.addStatusListener((status) { if (status == AnimationStatus.halfCompleted) { setState(() { showCardFrontSide = !showCardFrontSide; }); } }); } @override void dispose() { _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Stack( children: [ AnimatedBuilder( animation: _rotationAnimation, builder: (context, child) { final angle = _rotationAnimation.value * 3.14159; // π弧度对应180度 return Transform( alignment: Alignment.center, transform: Matrix4.identity() ..setEntry(3, 2, 0.001) // 添加透视效果,增强3D质感 ..rotateY(angle), child: Container( height: 140.h, decoration: BoxDecoration( border: Border.all(color: Colors.black), borderRadius: BorderRadius.circular(8), ), child: showCardFrontSide ? const NotificationCardFrontSide() : NotificationCardBackSide( onChangeView: onChangeView, ), ), ); }, ), showCardFrontSide ? Align( alignment: const Alignment(0.95, -1), child: IconButton( key: const ValueKey("IconButton"), onPressed: onChangeView, icon: const Icon(Icons.info_outline), ), ) : const SizedBox.shrink() ], ); } } class NotificationCardFrontSide extends StatelessWidget { const NotificationCardFrontSide({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Row( children: [ SizedBox( key: const ValueKey("FrontSideSizedBox"), width: 126.w, child: Center( child: CircleAvatar( radius: 50.r, ), ), ), SizedBox( key: const ValueKey("FrontSideSizedTextBox"), width: 222.w, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( "Premium Private LOBBY", style: Theme.of(context).textTheme.headlineMedium?.copyWith(overflow: TextOverflow.ellipsis), key: const ValueKey("FrontSideSizedTextBox1"), ), Text( "Prediction Deadline", style: Theme.of(context).textTheme.headlineMedium?.copyWith(overflow: TextOverflow.ellipsis), key: const ValueKey("FrontSideSizedTextBox2"), ), Text( "Premium Private LOBBY", style: Theme.of(context).textTheme.headlineMedium?.copyWith(overflow: TextOverflow.ellipsis), key: const ValueKey("FrontSideSizedTextBox3"), ), SizedBox( key: const ValueKey("FrontSideSizedButtonBox"), width: 150.w, height: 45.h, child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ SizedBox( key: const ValueKey("FrontSideButtonSizedBox"), width: 70.w, child: TextButton( onPressed: () {}, child: Text( "deny", style: Theme.of(context).textTheme.bodyMedium, ), ), ), SizedBox( width: 70.w, child: TextButton( onPressed: () {}, child: Text( "deny", style: Theme.of(context).textTheme.bodyMedium, ), ), ), ], ), ), ], ), ), ], ); } } class NotificationCardBackSide extends StatelessWidget { final VoidCallback onChangeView; const NotificationCardBackSide({ Key? key, required this.onChangeView, }) : super(key: key); Widget getTeamLogo(String image) { return CircleAvatar( backgroundColor: const Color(0xFFD9D9D9), radius: 30.r, child: Image.network( image, errorBuilder: (context, error, stackTrace) { return Text( "Error", style: Theme.of(context).textTheme.displayMedium?.copyWith( color: Colors.red, ), ); }, height: 65.h, width: 65.w, loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; return Center( child: Text( "Loading...", style: Theme.of(context).textTheme.displayMedium, ), ); }, ), ); } @override Widget build(BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ SizedBox( height: 62.h, child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ getTeamLogo(""), Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( "Premium Private LOBBY", style: Theme.of(context).textTheme.headlineMedium?.copyWith(overflow: TextOverflow.clip), key: const ValueKey("BackSideSizedText1"), ), Text( "Prediction Deadline", style: Theme.of(context).textTheme.headlineMedium?.copyWith(overflow: TextOverflow.clip), key: const ValueKey("BackSideSizedText2"), ), ], ), getTeamLogo(""), ], ), ), SizedBox( key: const ValueKey("BackSideButtonBox"), height: 30.h, width: 100.w, child: OutlinedButton( onPressed: onChangeView, child: const Text("Go Back"), key: const ValueKey("BackSideButtonText"), style: ButtonStyle( shape: MaterialStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular( 8.r, ), ), ), ), ), ) ], ); } }
核心实现要点:
动画基础配置
- 混入
SingleTickerProviderStateMixin为动画提供帧同步支持 - 用
AnimationController控制动画时长,CurvedAnimation添加缓动曲线让旋转更自然
- 混入
3D旋转效果
- 通过
Matrix4添加透视参数(setEntry(3,2,0.001)),模拟真实3D空间 - 使用
rotateY(angle)实现绕Y轴的旋转,达到卡片翻转的视觉效果
- 通过
正反面切换逻辑
- 在动画执行到一半(旋转90度)时切换卡片显示状态,避免正反面同时出现
- 通过
forward()和reverse()控制动画的正向/反向播放,实现来回翻转
资源管理
- 在
dispose()方法中释放AnimationController,防止内存泄漏
- 在
内容的提问来源于stack exchange,提问作者Chaitya Vohera
相关产品推荐
相关产品推荐

