// Author: fruit.com.cn import 'dart:math'; import 'package:flutter/material.dart'; import '../../../core/theme/app_colors.dart'; class PlayerDoubleTapHeartBurst extends StatefulWidget { const PlayerDoubleTapHeartBurst({super.key, required this.onCompleted}); final VoidCallback onCompleted; @override State createState() => _PlayerDoubleTapHeartBurstState(); } class _PlayerDoubleTapHeartBurstState extends State with SingleTickerProviderStateMixin { late final AnimationController _controller; late final List<_HeartParticle> _particles; bool _completed = false; @override void initState() { super.initState(); final random = Random(); var delay = 0; _particles = List.generate(3 + random.nextInt(2), (index) { if (index > 0) delay += 40 + random.nextInt(21); return _HeartParticle( delay: delay, duration: 600 + random.nextInt(101), travel: 90 + random.nextInt(31), drift: -18 + random.nextInt(37), size: (20 + random.nextInt(7)).toDouble(), color: random.nextBool() ? AppColors.doubleTapHeartRed : AppColors.doubleTapHeartPink, ); }); final total = _particles .map((particle) => particle.delay + particle.duration) .reduce(max); _controller = AnimationController( vsync: this, duration: Duration(milliseconds: total), )..addStatusListener(_onStatus); _controller.forward(); } void _onStatus(AnimationStatus status) { if (status != AnimationStatus.completed || _completed) return; _completed = true; widget.onCompleted(); } @override void dispose() { _controller ..removeStatusListener(_onStatus) ..dispose(); super.dispose(); } @override Widget build(BuildContext context) { return IgnorePointer( child: SizedBox.shrink( child: AnimatedBuilder( animation: _controller, builder: (context, _) { final elapsed = _controller.value * _controller.duration!.inMilliseconds; return Stack( clipBehavior: Clip.none, children: [ for (final particle in _particles) _particle(particle, elapsed), ], ); }, ), ), ); } Widget _particle(_HeartParticle particle, double elapsed) { final progress = ((elapsed - particle.delay) / particle.duration).clamp( 0.0, 1.0, ); if (progress <= 0) return const SizedBox.shrink(); final move = Curves.easeOutCubic.transform(progress); final scale = progress < 0.3 ? 0.55 + (1.15 - 0.55) * (progress / 0.3) : 1.15 - (1.15 - 0.2) * ((progress - 0.3) / 0.7); return Positioned( left: particle.drift * move - particle.size / 2, top: -particle.travel * move, child: Opacity( opacity: 1 - Curves.easeIn.transform(progress), child: Transform.scale( scale: scale, child: Icon( Icons.favorite_rounded, color: particle.color, size: particle.size, ), ), ), ); } } class _HeartParticle { const _HeartParticle({ required this.delay, required this.duration, required this.travel, required this.drift, required this.size, required this.color, }); final int delay; final int duration; final int travel; final int drift; final double size; final Color color; }