fix(player): 改用墙钟驱动飘心动画

This commit is contained in:
Zhou Weidong committed 2026-10-26 19:07:56 +08:00
1 parent c0f65042e6
commit 978c309cd7
6 files changed
+235 -100

No files matched your search

@@ -1,5 +1,7 @@
// Author: fruit.com.cn
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@@ -41,53 +43,84 @@ class PlayerDoubleTapHeartBurst extends StatefulWidget {
required this.burstId,
required this.onCompleted,
this.origin = Offset.zero,
this.now,
});
final int burstId;
final VoidCallback onCompleted;
final Offset origin;
final DateTime Function()? now;
@override
State<PlayerDoubleTapHeartBurst> createState() =>
_PlayerDoubleTapHeartBurstState();
}
class _PlayerDoubleTapHeartBurstState extends State<PlayerDoubleTapHeartBurst>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
class _PlayerDoubleTapHeartBurstState extends State<PlayerDoubleTapHeartBurst> {
Timer? _timer;
late final DateTime _startAt;
late final int _totalMs;
final Stopwatch _lifetime = Stopwatch();
int _elapsedMs = 0;
int _tickCount = 0;
bool _completed = false;
@override
void initState() {
super.initState();
_lifetime.start();
final total = _hearts
_startAt = _now();
_totalMs = _hearts
.map((heart) => heart.delay + _heartDurationMs)
.reduce((left, right) => left > right ? left : right);
if (kDebugMode) {
debugPrint(
'[player] phase=heart-burst-start '
'id=${widget.burstId} count=${_hearts.length} '
'configuredMs=$total at=${widget.origin.dx.toStringAsFixed(1)},'
'configuredMs=$_totalMs at=${widget.origin.dx.toStringAsFixed(1)},'
'${widget.origin.dy.toStringAsFixed(1)}',
);
}
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: total),
)..addStatusListener(_onStatus);
_controller.forward();
_timer = Timer.periodic(
const Duration(milliseconds: 16),
(_) => _handleTick(),
);
}
void _onStatus(AnimationStatus status) {
if (status != AnimationStatus.completed || _completed) return;
DateTime _now() => widget.now?.call() ?? DateTime.now();
void _handleTick() {
if (!mounted || _completed) return;
final rawElapsedMs = _now().difference(_startAt).inMilliseconds;
final elapsedMs = rawElapsedMs < 0
? 0
: rawElapsedMs > _totalMs
? _totalMs
: rawElapsedMs;
_elapsedMs = elapsedMs;
_tickCount++;
if (kDebugMode && _tickCount % 10 == 0) {
debugPrint(
'[player] phase=heart-tick id=${widget.burstId} '
'elapsedMs=$_elapsedMs',
);
}
if (_elapsedMs >= _totalMs) {
_complete();
return;
}
setState(() {});
}
void _complete() {
if (_completed) return;
_completed = true;
_timer?.cancel();
if (kDebugMode) {
debugPrint(
'[player] phase=heart-burst-complete '
'id=${widget.burstId} '
'configuredMs=${_controller.duration?.inMilliseconds ?? 0}',
'configuredMs=$_totalMs',
);
}
widget.onCompleted();
@@ -96,9 +129,7 @@ class _PlayerDoubleTapHeartBurstState extends State<PlayerDoubleTapHeartBurst>
@override
void dispose() {
if (kDebugMode) {
final elapsedMs = _completed
? _controller.duration?.inMilliseconds ?? 0
: _lifetime.elapsedMilliseconds;
final elapsedMs = _completed ? _totalMs : _elapsedMs;
debugPrint(
'[player] phase=heart-burst-dispose '
'id=${widget.burstId} elapsedMs=$elapsedMs '
@@ -114,44 +145,33 @@ class _PlayerDoubleTapHeartBurstState extends State<PlayerDoubleTapHeartBurst>
}
}
_lifetime.stop();
_controller
..removeStatusListener(_onStatus)
..dispose();
_timer?.cancel();
_timer = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
return IgnorePointer(
child: AnimatedBuilder(
animation: _controller,
builder: (context, _) {
final elapsed =
_controller.value * _controller.duration!.inMilliseconds;
return Stack(
fit: StackFit.expand,
clipBehavior: Clip.none,
children: [
for (var index = 0; index < _hearts.length; index++)
_heart(_hearts[index], elapsed, index),
if (kDebugMode)
Positioned(
left: widget.origin.dx + 8,
top: widget.origin.dy + 10,
child: IgnorePointer(
child: Text(
'${elapsed.round()}ms',
key: const Key('heart-burst-elapsed-debug'),
style: const TextStyle(
color: Colors.white60,
fontSize: 11,
),
),
),
child: Stack(
fit: StackFit.expand,
clipBehavior: Clip.none,
children: [
for (var index = 0; index < _hearts.length; index++)
_heart(_hearts[index], _elapsedMs.toDouble(), index),
if (kDebugMode)
Positioned(
left: widget.origin.dx + 8,
top: widget.origin.dy + 10,
child: IgnorePointer(
child: Text(
'${_elapsedMs}ms',
key: const Key('heart-burst-elapsed-debug'),
style: const TextStyle(color: Colors.white60, fontSize: 11),
),
],
);
},
),
),
],
),
);
}
@@ -176,6 +176,7 @@ class _PlayerTapGestureRegionState extends State<PlayerTapGestureRegion> {
key: ValueKey('heart-overlay-$id'),
burstId: id,
origin: origin,
now: widget.now,
onCompleted: removeEntry,
),
),