Files

183 lines
5.7 KiB
Dart
Raw Permalink Normal View History

2026-06-10 18:23:39 +08:00
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<FlipCard> createState() => _FlipCardState();
}
class _FlipCardState extends State<FlipCard> with TickerProviderStateMixin {
late final AnimationController _flipController;
late final AnimationController _popController;
late final AnimationController _glowController;
late final AnimationController _shakeController;
late Animation<double> _flip;
late Animation<double> _pop;
late Animation<double> _glow;
late Animation<double> _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<double>([
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<double>([
TweenSequenceItem(tween: Tween(begin: 0, end: 1), weight: 120),
TweenSequenceItem(tween: Tween(begin: 1, end: 0), weight: 220),
]).animate(_glowController);
_shake = TweenSequence<double>([
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,
),
),
),
),
),
],
),
),
);
},
),
);
}
}