From 5794dd4e4d0c482792a969ebf170eafec505c573 Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Thu, 22 Oct 2026 22:34:18 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=92=AD=E6=94=BE?= =?UTF-8?q?=E5=99=A8=E9=87=8D=E5=A4=8D=E6=9A=82=E5=81=9C=E6=89=8B=E5=8A=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plans/PROGRESS.md | 1 + lib/pages/player/player_view.dart | 35 ++++++++++++++----- .../widgets/player_double_tap_hearts.dart | 10 +++--- .../player/widgets/player_video_surface.dart | 27 +++++++------- test/player/double_tap_like_test.dart | 11 +++--- 5 files changed, 52 insertions(+), 32 deletions(-) diff --git a/docs/plans/PROGRESS.md b/docs/plans/PROGRESS.md index 70a35d1..2739e2c 100644 --- a/docs/plans/PROGRESS.md +++ b/docs/plans/PROGRESS.md @@ -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。 diff --git a/lib/pages/player/player_view.dart b/lib/pages/player/player_view.dart index f1f6d95..894cd70 100644 --- a/lib/pages/player/player_view.dart +++ b/lib/pages/player/player_view.dart @@ -120,6 +120,8 @@ class _PlayerPageState extends State bool _scrubbing = false; double? _speedBeforeBoost; bool? _chromeBeforeBoost; + bool _doubleTapHandling = false; + Timer? _doubleTapGuardTimer; /// 收藏切换后的本地增量:基准是详情快照,切换成功按服务端状态 ±1。 final Map _favoriteDelta = {}; @@ -261,6 +263,7 @@ class _PlayerPageState extends State appRouteObserver.unsubscribe(this); _spriteAxisService.dispose(); _thumbnailService.dispose(); + _doubleTapGuardTimer?.cancel(); logic.dispose(); super.dispose(); } @@ -333,7 +336,7 @@ class _PlayerPageState extends State } void _handleFeedSurfaceTap() { - if (_scrubbing) return; + if (_scrubbing || _doubleTapHandling) return; if (!_chromeVisible) { _restoreFeedChrome(); return; @@ -341,6 +344,24 @@ class _PlayerPageState extends State 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 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 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 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 child: PlayerCenterToggle( key: const Key('home-feed-center-play'), playing: false, - onTap: () => - logic.session.setUserWantsPlay(!logic.session.userWantsPlay), + onTap: _togglePlayback, ), ), ], diff --git a/lib/pages/player/widgets/player_double_tap_hearts.dart b/lib/pages/player/widgets/player_double_tap_hearts.dart index 6fc0e9c..68de8b9 100644 --- a/lib/pages/player/widgets/player_double_tap_hearts.dart +++ b/lib/pages/player/widgets/player_double_tap_hearts.dart @@ -27,14 +27,14 @@ class _PlayerDoubleTapHeartBurstState extends State 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, diff --git a/lib/pages/player/widgets/player_video_surface.dart b/lib/pages/player/widgets/player_video_surface.dart index 1636f31..670ccce 100644 --- a/lib/pages/player/widgets/player_video_surface.dart +++ b/lib/pages/player/widgets/player_video_surface.dart @@ -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 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, ); } } diff --git a/test/player/double_tap_like_test.dart b/test/player/double_tap_like_test.dart index cd5ecb3..3a44f04 100644 --- a/test/player/double_tap_like_test.dart +++ b/test/player/double_tap_like_test.dart @@ -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); }); }