import 'dart:math' as math; import 'package:flutter/material.dart'; class FlipCard extends StatefulWidget { final bool isFlipped; final bool isMatched; final int shakeToken; final bool shouldShake; final bool disabled; final VoidCallback? onPress; final Widget front; final Widget back; final double borderRadius; final Duration flipDuration; const FlipCard({ super.key, required this.isFlipped, required this.isMatched, required this.shakeToken, required this.shouldShake, required this.disabled, required this.onPress, required this.front, required this.back, this.borderRadius = 16, this.flipDuration = const Duration(milliseconds: 220), }); @override State createState() => _FlipCardState(); } class _FlipCardState extends State with TickerProviderStateMixin { late final AnimationController _flipController; late final AnimationController _popController; late final AnimationController _glowController; late final AnimationController _shakeController; late Animation _flip; late Animation _pop; late Animation _glow; late Animation _shake; int _prevShakeToken = 0; bool _prevMatched = false; @override void initState() { super.initState(); _prevMatched = widget.isMatched; _prevShakeToken = widget.shakeToken; _flipController = AnimationController( vsync: this, duration: widget.flipDuration, value: widget.isFlipped ? 1 : 0, ); _popController = AnimationController(vsync: this, duration: const Duration(milliseconds: 210)); _glowController = AnimationController(vsync: this, duration: const Duration(milliseconds: 340)); _shakeController = AnimationController(vsync: this, duration: const Duration(milliseconds: 180)); _flip = CurvedAnimation(parent: _flipController, curve: Curves.easeInOut); _pop = TweenSequence([ TweenSequenceItem(tween: Tween(begin: 1.0, end: 1.08), weight: 90), TweenSequenceItem(tween: Tween(begin: 1.08, end: 1.0), weight: 120), ]).animate(_popController); _glow = TweenSequence([ TweenSequenceItem(tween: Tween(begin: 0, end: 1), weight: 120), TweenSequenceItem(tween: Tween(begin: 1, end: 0), weight: 220), ]).animate(_glowController); _shake = TweenSequence([ TweenSequenceItem(tween: Tween(begin: 0, end: 8), weight: 45), TweenSequenceItem(tween: Tween(begin: 8, end: -8), weight: 45), TweenSequenceItem(tween: Tween(begin: -8, end: 8), weight: 45), TweenSequenceItem(tween: Tween(begin: 8, end: 0), weight: 45), ]).animate(_shakeController); } @override void didUpdateWidget(covariant FlipCard oldWidget) { super.didUpdateWidget(oldWidget); if (widget.isFlipped != oldWidget.isFlipped) { if (widget.isFlipped) { _flipController.forward(); } else { _flipController.reverse(); } } if (!_prevMatched && widget.isMatched) { _popController ..reset() ..forward(); _glowController ..reset() ..forward(); } _prevMatched = widget.isMatched; if (widget.shakeToken != _prevShakeToken) { _prevShakeToken = widget.shakeToken; if (widget.shouldShake) { _shakeController ..reset() ..forward(); } } } @override void dispose() { _flipController.dispose(); _popController.dispose(); _glowController.dispose(); _shakeController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return GestureDetector( onTap: widget.disabled ? null : widget.onPress, child: AnimatedBuilder( animation: Listenable.merge([ _flipController, _popController, _glowController, _shakeController, ]), builder: (context, child) { final angle = _flip.value * math.pi; final showBack = angle > math.pi / 2; final scale = _popController.isAnimating ? _pop.value : 1.0; final dx = _shakeController.isAnimating ? _shake.value : 0.0; return Transform.translate( offset: Offset(dx, 0), child: Transform.scale( scale: scale, child: Stack( fit: StackFit.expand, children: [ Transform( alignment: Alignment.center, transform: Matrix4.identity() ..setEntry(3, 2, 0.0012) ..rotateY(angle), child: ClipRRect( borderRadius: BorderRadius.circular(widget.borderRadius), child: showBack ? Transform( alignment: Alignment.center, transform: Matrix4.identity()..rotateY(math.pi), child: widget.back, ) : widget.front, ), ), IgnorePointer( child: Opacity( opacity: _glow.value, child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(widget.borderRadius), border: Border.all( color: const Color.fromRGBO(120, 255, 255, 0.9), width: 3, ), ), ), ), ), ], ), ), ); }, ), ); } }