You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Flutter/Dart的记忆匹配游戏:实现卡片翻转动画与摇晃动画

Implementing Flip & Shake Animations for Your Flutter Memory Match Game

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.isAnimating and !_shakeController.isAnimating).
  • Perspective Tweaks: Adjust the 0.001 value in setEntry(3, 2, 0.001) to make the 3D flip more or less pronounced.
  • Timing Adjustments: Tweak the Duration values 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 08:36:56