fix(player): 优化双击飘心轨迹
This commit is contained in:
1 parent
bea50ab32f
commit
94b37ef00f
7 files changed
+89
-16
No files matched your search
@@ -130,6 +130,7 @@ class _HomeFeedOverlayState extends State<HomeFeedOverlay> {
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: PlayerTapGestureRegion(
|
||||
key: const ValueKey('home-feed-tap-gesture-region'),
|
||||
gestureKey: const Key('home-feed-gesture'),
|
||||
episodeId: widget.episode.id,
|
||||
enabled: !_scrubbing,
|
||||
|
||||
@@ -1,33 +1,34 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
|
||||
const int _heartDurationMs = 1300;
|
||||
const int _lastHeartDelayMs = 200;
|
||||
const double _heartPopPortion = 0.18;
|
||||
const int _heartDurationMs = 1900;
|
||||
const int _lastHeartDelayMs = 260;
|
||||
const double _heartPopPortion = 0.12;
|
||||
const double _heartStartScale = 0.7;
|
||||
const double _heartHoldPortion = 0.65;
|
||||
const double _heartHoldPortion = 0.45;
|
||||
|
||||
const List<_HeartParticle> _hearts = [
|
||||
_HeartParticle(
|
||||
delay: 0,
|
||||
travel: 175,
|
||||
travel: 120,
|
||||
drift: 0,
|
||||
size: 44,
|
||||
color: AppColors.doubleTapHeartRed,
|
||||
),
|
||||
_HeartParticle(
|
||||
delay: 110,
|
||||
travel: 150,
|
||||
delay: 140,
|
||||
travel: 120,
|
||||
drift: -32,
|
||||
size: 34,
|
||||
color: AppColors.doubleTapHeartPink,
|
||||
),
|
||||
_HeartParticle(
|
||||
delay: _lastHeartDelayMs,
|
||||
travel: 165,
|
||||
travel: 120,
|
||||
drift: 30,
|
||||
size: 38,
|
||||
color: AppColors.doubleTapHeartRed,
|
||||
@@ -36,7 +37,11 @@ const List<_HeartParticle> _hearts = [
|
||||
|
||||
/// 双击点赞时,在点击位置飘出一小簇心。
|
||||
class PlayerDoubleTapHeartBurst extends StatefulWidget {
|
||||
const PlayerDoubleTapHeartBurst({super.key, required this.onCompleted});
|
||||
const PlayerDoubleTapHeartBurst({
|
||||
super.key,
|
||||
required this.burstId,
|
||||
required this.onCompleted,
|
||||
});
|
||||
|
||||
static const int particleCount = 3;
|
||||
static const int singleHeartDurationMs = _heartDurationMs;
|
||||
@@ -44,6 +49,7 @@ class PlayerDoubleTapHeartBurst extends StatefulWidget {
|
||||
milliseconds: _lastHeartDelayMs + _heartDurationMs,
|
||||
);
|
||||
|
||||
final int burstId;
|
||||
final VoidCallback onCompleted;
|
||||
|
||||
@override
|
||||
@@ -54,11 +60,17 @@ class PlayerDoubleTapHeartBurst extends StatefulWidget {
|
||||
class _PlayerDoubleTapHeartBurstState extends State<PlayerDoubleTapHeartBurst>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
final Stopwatch _lifetime = Stopwatch();
|
||||
bool _completed = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_lifetime.start();
|
||||
if (kDebugMode) {
|
||||
debugPrint('[player] phase=heart-burst-init id=${widget.burstId}');
|
||||
debugPrint('[player] phase=heart-burst-start id=${widget.burstId}');
|
||||
}
|
||||
final total = _hearts
|
||||
.map((heart) => heart.delay + _heartDurationMs)
|
||||
.reduce((left, right) => left > right ? left : right);
|
||||
@@ -72,11 +84,26 @@ class _PlayerDoubleTapHeartBurstState extends State<PlayerDoubleTapHeartBurst>
|
||||
void _onStatus(AnimationStatus status) {
|
||||
if (status != AnimationStatus.completed || _completed) return;
|
||||
_completed = true;
|
||||
if (kDebugMode) {
|
||||
final elapsedMs = _controller.duration?.inMilliseconds ?? 0;
|
||||
debugPrint(
|
||||
'[player] phase=heart-burst-complete '
|
||||
'id=${widget.burstId} elapsedMs=$elapsedMs',
|
||||
);
|
||||
}
|
||||
widget.onCompleted();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'[player] phase=heart-burst-dispose '
|
||||
'id=${widget.burstId} elapsedMs=${_lifetime.elapsedMilliseconds} '
|
||||
'completed=${_completed ? 1 : 0}',
|
||||
);
|
||||
}
|
||||
_lifetime.stop();
|
||||
_controller
|
||||
..removeStatusListener(_onStatus)
|
||||
..dispose();
|
||||
@@ -111,14 +138,17 @@ class _PlayerDoubleTapHeartBurstState extends State<PlayerDoubleTapHeartBurst>
|
||||
1.0,
|
||||
);
|
||||
if (progress <= 0) return const SizedBox.shrink();
|
||||
final move = Curves.easeOutCubic.transform(progress);
|
||||
final move = Curves.easeOutSine.transform(progress);
|
||||
final scale = progress < _heartPopPortion
|
||||
? _heartStartScale +
|
||||
(1 - _heartStartScale) * (progress / _heartPopPortion)
|
||||
: 1.0;
|
||||
final opacity = progress < _heartHoldPortion
|
||||
? 1.0
|
||||
: 1 - ((progress - _heartHoldPortion) / (1 - _heartHoldPortion));
|
||||
: 1 -
|
||||
Curves.easeInOutSine.transform(
|
||||
(progress - _heartHoldPortion) / (1 - _heartHoldPortion),
|
||||
);
|
||||
return Positioned(
|
||||
left: heart.drift * move - heart.size / 2,
|
||||
top: -heart.size / 2 - heart.travel * move,
|
||||
|
||||
@@ -59,6 +59,7 @@ class PlayerPlaybackOverlay extends StatelessWidget {
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: PlayerTapGestureRegion(
|
||||
key: const ValueKey('player-tap-gesture-region'),
|
||||
gestureKey: const Key('player-video-tap'),
|
||||
episodeId: data.episode.id,
|
||||
enabled: !scrubbing,
|
||||
|
||||
@@ -93,6 +93,7 @@ class _PlayerTapGestureRegionState extends State<PlayerTapGestureRegion> {
|
||||
top: burst.position.dy - 10,
|
||||
child: PlayerDoubleTapHeartBurst(
|
||||
key: ValueKey(burst.id),
|
||||
burstId: burst.id,
|
||||
onCompleted: () => _removeHeartBurst(burst.id),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user