fix: 修复播放器重复暂停手势
This commit is contained in:
1 parent
76c4b528d3
commit
5794dd4e4d
5 files changed
+52
-32
No files matched your search
@@ -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。
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user