Files
ReelShort/lib/pages/player/widgets/player_double_tap_hearts.dart
T

133 lines
3.5 KiB
Dart

// 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<PlayerDoubleTapHeartBurst> createState() =>
_PlayerDoubleTapHeartBurstState();
}
class _PlayerDoubleTapHeartBurstState extends State<PlayerDoubleTapHeartBurst>
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;
}