基于Flutter/Dart的记忆匹配游戏:实现卡片翻转动画与摇晃动画
Got it, let's tackle those animations to make your memory game feel way more interactive and polished! Since you've already nailed the core game logic, adding these visual flourishes is straightforward with Flutter's animation system. Here's a step-by-step breakdown for both required effects:
1. Card Flip Animation on Tap
We'll use a RotationY transform paired with an AnimationController to create a smooth 3D flip effect, toggling between the card's hidden front and revealed back.
Step 1: Set Up Animation Controllers in Your Card Widget
First, turn your card into a StatefulWidget to manage animation state:
class MemoryCard extends StatefulWidget { final String image; final bool isMatched; final VoidCallback onTap; const MemoryCard({ super.key, required this.image, required this.isMatched, required this.onTap, }); @override State<MemoryCard> createState() => _MemoryCardState(); } class _MemoryCardState extends State<MemoryCard> with SingleTickerProviderStateMixin { late AnimationController _flipController; late Animation<double> _flipAnimation; bool _isFlipped = false; @override void initState() { super.initState(); // Initialize flip controller (500ms duration for smooth transition) _flipController = AnimationController( duration: const Duration(milliseconds: 500), vsync: this, ); // Rotate from 0 to π radians (180 degrees) for full flip _flipAnimation = Tween<double>(begin: 0, end: pi).animate( CurvedAnimation(parent: _flipController, curve: Curves.easeInOut), ); } @override void dispose() { _flipController.dispose(); super.dispose(); } // Toggle flip animation between front and back void flip() { _isFlipped ? _flipController.reverse() : _flipController.forward(); setState(() => _isFlipped = !_isFlipped); } // ... rest of the widget code }
Step 2: Build the Animated Card UI
Use AnimatedBuilder to apply the rotation transform, and switch between front/back based on the animation state:
@override Widget build(BuildContext context) { return GestureDetector( onTap: () { // Block taps during animation or if card is already matched if (!widget.isMatched && !_flipController.isAnimating) { widget.onTap(); flip(); } }, child: AnimatedBuilder( animation: _flipAnimation, builder: (context, child) { final isFrontVisible = _flipAnimation.value < pi / 2; return Transform( alignment: Alignment.center, transform: Matrix4.identity() ..setEntry(3, 2, 0.001) // Add subtle 3D perspective ..rotateY(_flipAnimation.value), child: isFrontVisible ? _buildCardFront() // Your default hidden card design : Transform( alignment: Alignment.center, transform: Matrix4.identity()..rotateY(pi), child: _buildCardBack(), // Revealed card with image ), ); }, ), ); } Widget _buildCardFront() { return Container( decoration: BoxDecoration( color: Colors.blueAccent, borderRadius: BorderRadius.circular(8), ), // Add your front card styling (e.g., icon, pattern) ); } Widget _buildCardBack() { return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), image: DecorationImage( image: AssetImage(widget.image), fit: BoxFit.cover, ), ), ); }
2. Shake Animation + Flip Back for Mismatched Cards
When two cards don't match, we'll first play a quick shake animation, then flip the cards back to their hidden state. We'll add a second AnimationController for the shake effect.
Step 1: Add Shake Animation to the Card Widget
Update the _MemoryCardState to include the shake controller:
class _MemoryCardState extends State<MemoryCard> with SingleTickerProviderStateMixin { late AnimationController _flipController; late Animation<double> _flipAnimation; late AnimationController _shakeController; late Animation<double> _shakeAnimation; bool _isFlipped = false; @override void initState() { super.initState(); // Flip controller (same as before) _flipController = AnimationController( duration: const Duration(milliseconds: 500), vsync: this, ); _flipAnimation = Tween<double>(begin: 0, end: pi).animate( CurvedAnimation(parent: _flipController, curve: Curves.easeInOut), ); // Shake controller (fast left-right rotation) _shakeController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); // Shake between -10° and 10° (converted to radians) _shakeAnimation = Tween<double>(begin: -0.17, end: 0.17).animate( CurvedAnimation(parent: _shakeController, curve: Curves.bounceInOut), )..addStatusListener((status) { // Reverse animation to create back-and-forth shake if (status == AnimationStatus.completed) { _shakeController.reverse(); } else if (status == AnimationStatus.dismissed) { _shakeController.forward(); } }); } @override void dispose() { _flipController.dispose(); _shakeController.dispose(); super.dispose(); } // Trigger shake, then flip back to hidden state Future<void> shakeAndFlipBack() async { // Run shake twice (600ms total) _shakeController.forward(); await Future.delayed(const Duration(milliseconds: 600)); _shakeController.stop(); // Flip back to front await _flipController.reverse(); setState(() => _isFlipped = false); } // ... rest of the code }
Step 2: Apply Shake Transform to the Card
Update the AnimatedBuilder to include the shake rotation:
@override Widget build(BuildContext context) { return GestureDetector( onTap: () { if (!widget.isMatched && !_flipController.isAnimating && !_shakeController.isAnimating) { widget.onTap(); flip(); } }, child: AnimatedBuilder( animation: Listenable.merge([_flipAnimation, _shakeAnimation]), builder: (context, child) { final isFrontVisible = _flipAnimation.value < pi / 2; return Transform( alignment: Alignment.center, transform: Matrix4.identity() ..setEntry(3, 2, 0.001) ..rotateY(_flipAnimation.value) ..rotateZ(_shakeAnimation.value), // Add shake effect child: isFrontVisible ? _buildCardFront() : Transform( alignment: Alignment.center, transform: Matrix4.identity()..rotateY(pi), child: _buildCardBack(), ), ); }, ), ); }
Step 3: Trigger Animation from Game Logic
In your parent game widget, call shakeAndFlipBack() on both selected cards when a mismatch is detected:
// Example mismatch handling in your game state void handleMismatch() { setState(() { _selectedCards[0].shakeAndFlipBack(); _selectedCards[1].shakeAndFlipBack(); _selectedCards.clear(); }); }
Key Tips
- Animation Locking: Always disable taps during animations to prevent broken state (check
!_flipController.isAnimatingand!_shakeController.isAnimating). - Perspective Tweaks: Adjust the
0.001value insetEntry(3, 2, 0.001)to make the 3D flip more or less pronounced. - Timing Adjustments: Tweak the
Durationvalues for controllers to match your game's vibe—faster animations feel snappier, slower ones more deliberate.
These animations should slot right into your existing logic. Feel free to tweak colors, curves, or durations to match your game's style!
内容的提问来源于stack exchange,提问作者Rajesh Patil

