From c761ee013d0c309992aa07d94c0bbae282352179 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Fri, 4 Mar 2022 12:24:39 +0900 Subject: [PATCH] video: fixed gesture handling --- lib/widgets/common/magnifier/core/core.dart | 10 +- lib/widgets/common/magnifier/magnifier.dart | 7 + lib/widgets/viewer/video_action_delegate.dart | 4 +- .../viewer/visual/entry_page_view.dart | 150 ++++++++++-------- 4 files changed, 102 insertions(+), 69 deletions(-) diff --git a/lib/widgets/common/magnifier/core/core.dart b/lib/widgets/common/magnifier/core/core.dart index da4ad3c5e..495a857f6 100644 --- a/lib/widgets/common/magnifier/core/core.dart +++ b/lib/widgets/common/magnifier/core/core.dart @@ -19,6 +19,7 @@ class MagnifierCore extends StatefulWidget { final bool applyScale; final double panInertia; final MagnifierTapCallback? onTap; + final MagnifierDoubleTapCallback? onDoubleTap; final Widget child; const MagnifierCore({ @@ -27,7 +28,8 @@ class MagnifierCore extends StatefulWidget { required this.scaleStateCycle, required this.applyScale, this.panInertia = .2, - required this.onTap, + this.onTap, + this.onDoubleTap, required this.child, }) : super(key: key); @@ -204,6 +206,12 @@ class _MagnifierCoreState extends State with TickerProviderStateM void onDoubleTap(TapDownDetails details) { final viewportTapPosition = details.localPosition; + if (widget.onDoubleTap != null) { + final viewportSize = scaleBoundaries.viewportSize; + final alignment = Alignment(viewportTapPosition.dx / viewportSize.width, viewportTapPosition.dy / viewportSize.height); + if (widget.onDoubleTap?.call(alignment) == true) return; + } + final childTapPosition = scaleBoundaries.viewportToChildPosition(controller, viewportTapPosition); nextScaleState(ChangeSource.gesture, childFocalPoint: childTapPosition); } diff --git a/lib/widgets/common/magnifier/magnifier.dart b/lib/widgets/common/magnifier/magnifier.dart index 36ab155cd..0661d557b 100644 --- a/lib/widgets/common/magnifier/magnifier.dart +++ b/lib/widgets/common/magnifier/magnifier.dart @@ -29,6 +29,7 @@ class Magnifier extends StatelessWidget { this.scaleStateCycle = defaultScaleStateCycle, this.applyScale = true, this.onTap, + this.onDoubleTap, required this.child, }) : super(key: key); @@ -49,6 +50,7 @@ class Magnifier extends StatelessWidget { final ScaleStateCycle scaleStateCycle; final bool applyScale; final MagnifierTapCallback? onTap; + final MagnifierDoubleTapCallback? onDoubleTap; final Widget child; @override @@ -68,6 +70,7 @@ class Magnifier extends StatelessWidget { scaleStateCycle: scaleStateCycle, applyScale: applyScale, onTap: onTap, + onDoubleTap: onDoubleTap, child: child, ); }, @@ -81,3 +84,7 @@ typedef MagnifierTapCallback = Function( MagnifierState state, Offset childTapPosition, ); + +typedef MagnifierDoubleTapCallback = bool Function( + Alignment alignment, +); diff --git a/lib/widgets/viewer/video_action_delegate.dart b/lib/widgets/viewer/video_action_delegate.dart index d413821e7..4cc7b46c5 100644 --- a/lib/widgets/viewer/video_action_delegate.dart +++ b/lib/widgets/viewer/video_action_delegate.dart @@ -57,10 +57,10 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix _togglePlayPause(context, controller); break; case EntryAction.videoReplay10: - if (controller.isReady) controller.seekTo(controller.currentPosition - 10000); + controller.seekTo(controller.currentPosition - 10000); break; case EntryAction.videoSkip10: - if (controller.isReady) controller.seekTo(controller.currentPosition + 10000); + controller.seekTo(controller.currentPosition + 10000); break; case EntryAction.open: final entry = controller.entry; diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index cf4dc87a9..f89ff0c79 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -30,6 +30,7 @@ import 'package:collection/collection.dart'; import 'package:decorated_icon/decorated_icon.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:tuple/tuple.dart'; class EntryPageView extends StatefulWidget { final AvesEntry mainEntry, pageEntry; @@ -173,7 +174,7 @@ class _EntryPageViewState extends State { } Widget _buildSvgView() { - var child = _buildMagnifier( + return _buildMagnifier( maxScale: const ScaleLevel(factor: 25), scaleStateCycle: _vectorScaleStateCycle, applyScale: false, @@ -186,25 +187,25 @@ class _EntryPageViewState extends State { ), ), ); - return child; } Widget _buildVideoView() { final videoController = context.read().getController(entry); if (videoController == null) return const SizedBox(); - Positioned _buildDoubleTapDetector( - EntryAction action, { - double widthFactor = 1, - AlignmentGeometry alignment = Alignment.center, - IconData? Function()? icon, - }) { - return Positioned.fill( - child: FractionallySizedBox( - alignment: alignment, - widthFactor: widthFactor, - child: GestureDetector( - onDoubleTap: () { + return ValueListenableBuilder( + valueListenable: videoController.sarNotifier, + builder: (context, sar, child) { + final videoDisplaySize = entry.videoDisplaySize(sar); + + return Selector>( + selector: (context, s) => Tuple2(s.videoGestureDoubleTapTogglePlay, s.videoGestureSideDoubleTapSeek), + builder: (context, s, child) { + final playGesture = s.item1; + final seekGesture = s.item2; + final useActionGesture = playGesture || seekGesture; + + void _applyAction(EntryAction action, {IconData? Function()? icon}) { _actionFeedbackChildNotifier.value = DecoratedIcon( icon?.call() ?? action.getIconData(), size: 48, @@ -219,67 +220,81 @@ class _EntryPageViewState extends State { controller: videoController, action: action, ).dispatch(context); - }, - ), - ), - ); - } + } - return ValueListenableBuilder( - valueListenable: videoController.sarNotifier, - builder: (context, sar, child) { - final videoDisplaySize = entry.videoDisplaySize(sar); - final playGesture = settings.videoGestureDoubleTapTogglePlay; - final seekGesture = settings.videoGestureSideDoubleTapSeek; - final useActionGesture = playGesture || seekGesture; - return Stack( - fit: StackFit.expand, - children: [ - Stack( + MagnifierDoubleTapCallback? _onDoubleTap = useActionGesture + ? (alignment) { + final x = alignment.x; + if (seekGesture) { + if (x < .25) { + _applyAction(EntryAction.videoReplay10); + return true; + } else if (x > .75) { + _applyAction(EntryAction.videoSkip10); + return true; + } + } + if (playGesture) { + _applyAction( + EntryAction.videoTogglePlay, + icon: () => videoController.isPlaying ? AIcons.pause : AIcons.play, + ); + return true; + } + return false; + } + : null; + + return Stack( + fit: StackFit.expand, children: [ - _buildMagnifier( - displaySize: videoDisplaySize, - child: VideoView( - entry: entry, - controller: videoController, - ), - ), - VideoSubtitles( - controller: videoController, - viewStateNotifier: _viewStateNotifier, - ), - if (settings.videoShowRawTimedText) - VideoSubtitles( - controller: videoController, - viewStateNotifier: _viewStateNotifier, - debugMode: true, - ), - if (playGesture) - _buildDoubleTapDetector( - EntryAction.videoTogglePlay, - icon: () => videoController.isPlaying ? AIcons.pause : AIcons.play, - ), - if (seekGesture) ...[ - _buildDoubleTapDetector(EntryAction.videoReplay10, widthFactor: .25, alignment: Alignment.centerLeft), - _buildDoubleTapDetector(EntryAction.videoSkip10, widthFactor: .25, alignment: Alignment.centerRight), - ], - if (useActionGesture) - ValueListenableBuilder( - valueListenable: _actionFeedbackChildNotifier, - builder: (context, feedbackChild, child) => ActionFeedback( - child: feedbackChild, + Stack( + children: [ + _buildMagnifier( + displaySize: videoDisplaySize, + onDoubleTap: _onDoubleTap, + child: VideoView( + entry: entry, + controller: videoController, + ), ), - ), + VideoSubtitles( + controller: videoController, + viewStateNotifier: _viewStateNotifier, + ), + if (settings.videoShowRawTimedText) + VideoSubtitles( + controller: videoController, + viewStateNotifier: _viewStateNotifier, + debugMode: true, + ), + if (useActionGesture) + ValueListenableBuilder( + valueListenable: _actionFeedbackChildNotifier, + builder: (context, feedbackChild, child) => ActionFeedback( + child: feedbackChild, + ), + ), + ], + ), + _buildVideoCover( + videoController: videoController, + videoDisplaySize: videoDisplaySize, + onDoubleTap: _onDoubleTap, + ), ], - ), - _buildVideoCover(videoController, videoDisplaySize), - ], + ); + }, ); }, ); } - StreamBuilder _buildVideoCover(AvesVideoController videoController, Size videoDisplaySize) { + StreamBuilder _buildVideoCover({ + required AvesVideoController videoController, + required Size videoDisplaySize, + required MagnifierDoubleTapCallback? onDoubleTap, + }) { // fade out image to ease transition with the player return StreamBuilder( stream: videoController.statusStream, @@ -307,6 +322,7 @@ class _EntryPageViewState extends State { return _buildMagnifier( controller: coverController, displaySize: coverSize, + onDoubleTap: onDoubleTap, child: Image( image: videoCoverUriImage, ), @@ -342,6 +358,7 @@ class _EntryPageViewState extends State { ScaleLevel maxScale = maxScale, ScaleStateCycle scaleStateCycle = defaultScaleStateCycle, bool applyScale = true, + MagnifierDoubleTapCallback? onDoubleTap, required Widget child, }) { return Magnifier( @@ -355,6 +372,7 @@ class _EntryPageViewState extends State { scaleStateCycle: scaleStateCycle, applyScale: applyScale, onTap: (c, d, s, o) => _onTap(), + onDoubleTap: onDoubleTap, child: child, ); }