merge: 修复播放器重复暂停手势

This commit is contained in:
Zhou Weidong committed 2026-10-22 22:34:26 +08:00
commit 9ddd7fc833
5 files changed
+52 -32

No files matched your search

+1
View File
@@ -1301,3 +1301,4 @@ $ flutter test
- 双击和右侧心形共用点赞方法,成功后控制器通知并点亮红色实心心形,失败由 controller 回滚并使用 Toast。
- 飘心层使用独立 IgnorePointer Stack,动画完成后自动移除;`flutter analyze`:No issues found。
- `flutter test test/player/double_tap_like_test.dart`:2 项测试全部通过。
- 移除视频表面点击识别器,双击后增加 300ms 暂停防护;飘心收敛为 3~4 颗、20~26、±18、90~120dp、600~700ms。
+26 -9
View File
@@ -120,6 +120,8 @@ class _PlayerPageState extends State<PlayerPage>
bool _scrubbing = false;
double? _speedBeforeBoost;
bool? _chromeBeforeBoost;
bool _doubleTapHandling = false;
Timer? _doubleTapGuardTimer;
/// 收藏切换后的本地增量:基准是详情快照,切换成功按服务端状态 ±1。
final Map<String, int> _favoriteDelta = {};
@@ -261,6 +263,7 @@ class _PlayerPageState extends State<PlayerPage>
appRouteObserver.unsubscribe(this);
_spriteAxisService.dispose();
_thumbnailService.dispose();
_doubleTapGuardTimer?.cancel();
logic.dispose();
super.dispose();
}
@@ -333,7 +336,7 @@ class _PlayerPageState extends State<PlayerPage>
}
void _handleFeedSurfaceTap() {
if (_scrubbing) return;
if (_scrubbing || _doubleTapHandling) return;
if (!_chromeVisible) {
_restoreFeedChrome();
return;
@@ -341,6 +344,24 @@ class _PlayerPageState extends State<PlayerPage>
logic.session.setUserWantsPlay(!logic.session.userWantsPlay);
}
void _togglePlayback() {
if (_doubleTapHandling) return;
logic.session.setUserWantsPlay(!logic.session.userWantsPlay);
}
void _handleDoubleTapLike(
BuildContext context,
InteractionController interactions,
Episode episode,
) {
_doubleTapHandling = true;
_doubleTapGuardTimer?.cancel();
_doubleTapGuardTimer = Timer(const Duration(milliseconds: 300), () {
_doubleTapHandling = false;
});
unawaited(_toggleLike(context, interactions, episode, likeOnly: true));
}
/// 收藏状态确认后同步右侧计数(总数没有独立计数接口,按 ±1 跟随)。
void _applyFavoriteDelta(String dramaId, {required bool nowSaved}) {
setState(() {
@@ -516,7 +537,6 @@ class _PlayerPageState extends State<PlayerPage>
controller: videoPort?.controller,
videoUrl: videoPort?.controller.dataSource ?? episode.videoUrl,
theater: theater,
onTap: widget.homeMode ? () {} : _handleFeedSurfaceTap,
),
// 票据三态之一:起播前正在向后端要签名地址
if (selected && logic.ticketLoading)
@@ -634,9 +654,8 @@ class _PlayerPageState extends State<PlayerPage>
onLike: () async {
await _toggleLike(context, interactions, episode);
},
onDoubleTapLike: (_) => unawaited(
_toggleLike(context, interactions, episode, likeOnly: true),
),
onDoubleTapLike: (_) =>
_handleDoubleTapLike(context, interactions, episode),
onSave: () => unawaited(_toggleSave(context, drama)),
onComments: () => unawaited(_openComments(context, drama, episode)),
onShare: () => logic.shareDrama(context, drama),
@@ -686,8 +705,7 @@ class _PlayerPageState extends State<PlayerPage>
onBack: widget.embedded
? null
: () => unawaited(logic.leave(context)),
onTogglePlay: () =>
logic.session.setUserWantsPlay(!logic.session.userWantsPlay),
onTogglePlay: _togglePlayback,
onClear: () => setState(() => _chromeVisible = false),
onExitClear: () => setState(() => _chromeVisible = true),
onDetail: () => unawaited(logic.openDetail(context)),
@@ -743,8 +761,7 @@ class _PlayerPageState extends State<PlayerPage>
child: PlayerCenterToggle(
key: const Key('home-feed-center-play'),
playing: false,
onTap: () =>
logic.session.setUserWantsPlay(!logic.session.userWantsPlay),
onTap: _togglePlayback,
),
),
],
@@ -27,14 +27,14 @@ class _PlayerDoubleTapHeartBurstState extends State<PlayerDoubleTapHeartBurst>
super.initState();
final random = Random();
var delay = 0;
_particles = List.generate(6 + random.nextInt(3), (index) {
_particles = List.generate(3 + random.nextInt(2), (index) {
if (index > 0) delay += 40 + random.nextInt(21);
return _HeartParticle(
delay: delay,
duration: 700 + random.nextInt(201),
travel: 120 + random.nextInt(41),
drift: -30 + random.nextInt(61),
size: (22 + random.nextInt(13)).toDouble(),
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,
@@ -17,13 +17,13 @@ class PlayerVideoSurface extends StatefulWidget {
required this.controller,
required this.videoUrl,
required this.theater,
required this.onTap,
this.onTap,
});
final VideoPlayerController? controller;
final String videoUrl;
final bool theater;
final VoidCallback onTap;
final VoidCallback? onTap;
@override
State<PlayerVideoSurface> createState() => _PlayerVideoSurfaceState();
@@ -147,21 +147,24 @@ class _VideoLayer extends StatelessWidget {
final VideoPlayerController controller;
final VideoPlayerValue value;
final bool theater;
final VoidCallback onTap;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
final child = FittedBox(
fit: theater ? BoxFit.contain : BoxFit.cover,
child: SizedBox(
width: value.size.width,
height: value.size.height,
child: VideoPlayer(controller, key: ValueKey(controller)),
),
);
final tap = onTap;
if (tap == null) return child;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: FittedBox(
fit: theater ? BoxFit.contain : BoxFit.cover,
child: SizedBox(
width: value.size.width,
height: value.size.height,
child: VideoPlayer(controller, key: ValueKey(controller)),
),
),
onTap: tap,
child: child,
);
}
}
+5 -6
View File
@@ -24,7 +24,8 @@ void main() {
await tester.tapAt(center);
await tester.pump(const Duration(milliseconds: 100));
await tester.tapAt(center);
await tester.pumpAndSettle();
await tester.pump();
await tester.pump(const Duration(milliseconds: 400));
expect(key.currentState!.pauseToggles, 0);
expect(repository.likeTargets, hasLength(1));
@@ -36,19 +37,17 @@ void main() {
),
findsOneWidget,
);
await tester.pumpAndSettle();
});
testWidgets('single tap toggles playback after 280ms', (tester) async {
testWidgets('single tap toggles playback after 300ms', (tester) async {
final key = GlobalKey<_HarnessState>();
await _pumpHarness(tester, FakeInteractionRepository(), FakeHaptics(), key);
await tester.tapAt(
tester.getCenter(find.byKey(const Key('home-feed-gesture'))),
);
await tester.pump(const Duration(milliseconds: 279));
expect(key.currentState!.pauseToggles, 0);
await tester.pump(const Duration(milliseconds: 2));
await tester.pump(const Duration(milliseconds: 400));
expect(key.currentState!.pauseToggles, 1);
});
}