From e1c3bae90b8a5b1128fb9012c5637af57903e5c5 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sun, 31 Mar 2024 18:37:25 +0200 Subject: [PATCH] #622 viewer: fixed side gesture precedence --- CHANGELOG.md | 1 + .../viewer/visual/entry_page_view.dart | 25 +++++-- plugins/aves_magnifier/lib/src/core/core.dart | 64 +++++++++++------ .../lib/src/core/gesture_detector.dart | 69 ++++++++++++++----- 4 files changed, 114 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35fe022df..1ff77ef2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ All notable changes to this project will be documented in this file. - crash when decoding large region - viewer position drift during scale +- viewer side gesture precedence (next entry by single tap vs zoom by double tap) ## [v1.10.7] - 2024-03-12 diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index 287d1d836..eb8f122d4 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -405,6 +405,7 @@ class _EntryPageViewState extends State with TickerProviderStateM controller: controller ?? _magnifierController, contentSize: displaySize ?? entry.displaySize, allowOriginalScaleBeyondRange: !isWallpaperMode, + allowDoubleTap: _allowDoubleTap, minScale: minScale, maxScale: maxScale, initialScale: viewerController.initialScale, @@ -434,22 +435,34 @@ class _EntryPageViewState extends State with TickerProviderStateM } } - void _onTap({Alignment? alignment}) { + Notification? _handleSideSingleTap(Alignment? alignment) { if (settings.viewerGestureSideTapNext && alignment != null) { final x = alignment.x; final sideRatio = _getSideRatio(); if (sideRatio != null) { const animate = false; if (x < sideRatio) { - (context.isRtl ? const ShowNextEntryNotification(animate: animate) : const ShowPreviousEntryNotification(animate: animate)).dispatch(context); - return; + return context.isRtl ? const ShowNextEntryNotification(animate: animate) : const ShowPreviousEntryNotification(animate: animate); } else if (x > 1 - sideRatio) { - (context.isRtl ? const ShowPreviousEntryNotification(animate: animate) : const ShowNextEntryNotification(animate: animate)).dispatch(context); - return; + return context.isRtl ? const ShowPreviousEntryNotification(animate: animate) : const ShowNextEntryNotification(animate: animate); } } } - const ToggleOverlayNotification().dispatch(context); + return null; + } + + void _onTap({Alignment? alignment}) => (_handleSideSingleTap(alignment) ?? const ToggleOverlayNotification()).dispatch(context); + + // side gesture handling by precedence: + // - seek in video by side double tap (if enabled) + // - go to previous/next entry by side single tap (if enabled) + // - zoom in/out by double tap + bool _allowDoubleTap(Alignment alignment) { + if (entry.isVideo && settings.videoGestureSideDoubleTapSeek) { + return true; + } + final actionNotification = _handleSideSingleTap(alignment); + return actionNotification == null; } void _onMediaCommand(MediaCommandEvent event) { diff --git a/plugins/aves_magnifier/lib/src/core/core.dart b/plugins/aves_magnifier/lib/src/core/core.dart index 5f07d13fc..0585c5331 100644 --- a/plugins/aves_magnifier/lib/src/core/core.dart +++ b/plugins/aves_magnifier/lib/src/core/core.dart @@ -36,6 +36,7 @@ class AvesMagnifier extends StatefulWidget { final bool allowOriginalScaleBeyondRange; final bool allowGestureScaleBeyondRange; + final MagnifierDoubleTapCallback? allowDoubleTap; final double panInertia; // Defines the minimum size in which the image will be allowed to assume, it is proportional to the original image size. @@ -64,6 +65,7 @@ class AvesMagnifier extends StatefulWidget { this.viewportPadding = EdgeInsets.zero, this.allowOriginalScaleBeyondRange = true, this.allowGestureScaleBeyondRange = true, + this.allowDoubleTap, this.minScale = const ScaleLevel(factor: .0), this.maxScale = const ScaleLevel(factor: double.infinity), this.initialScale = const ScaleLevel(ref: ScaleReference.contained), @@ -356,35 +358,55 @@ class _AvesMagnifierState extends State with TickerProviderStateM return Duration(milliseconds: gestureVelocity != 0 ? (animationVelocity / gestureVelocity * 1000).round() : 0); } - void onTap(TapUpDetails details) { + Alignment? _getTapAlignment(Offset viewportTapPosition) { + final boundaries = scaleBoundaries; + if (boundaries == null) return null; + + final viewportSize = boundaries.viewportSize; + return Alignment(viewportTapPosition.dx / viewportSize.width, viewportTapPosition.dy / viewportSize.height); + } + + Offset? _getChildTapPosition(Offset viewportTapPosition) { + final boundaries = scaleBoundaries; + if (boundaries == null) return null; + + return boundaries.viewportToContentPosition(controller, viewportTapPosition); + } + + void _onTapUp(TapUpDetails details) { final onTap = widget.onTap; if (onTap == null) return; - final boundaries = scaleBoundaries; - if (boundaries == null) return; - final viewportTapPosition = details.localPosition; - final viewportSize = boundaries.viewportSize; - final alignment = Alignment(viewportTapPosition.dx / viewportSize.width, viewportTapPosition.dy / viewportSize.height); - final childTapPosition = boundaries.viewportToContentPosition(controller, viewportTapPosition); - - onTap(context, controller.currentState, alignment, childTapPosition); + final alignment = _getTapAlignment(viewportTapPosition); + final childTapPosition = _getChildTapPosition(viewportTapPosition); + if (alignment != null && childTapPosition != null) { + onTap(context, controller.currentState, alignment, childTapPosition); + } } - void onDoubleTap(TapDownDetails details) { - final boundaries = scaleBoundaries; - if (boundaries == null) return; + bool _allowDoubleTap(Offset localPosition) { + final allowDoubleTap = widget.allowDoubleTap; + if (allowDoubleTap != null) { + final alignment = _getTapAlignment(localPosition); + if (alignment != null) { + return allowDoubleTap(alignment); + } + } + return true; + } - final viewportTapPosition = details.localPosition; + void _onDoubleTap(TapDownDetails details) { final onDoubleTap = widget.onDoubleTap; if (onDoubleTap != null) { - final viewportSize = boundaries.viewportSize; - final alignment = Alignment(viewportTapPosition.dx / viewportSize.width, viewportTapPosition.dy / viewportSize.height); - if (onDoubleTap(alignment) == true) return; + final alignment = _getTapAlignment(details.localPosition); + if (alignment != null && onDoubleTap(alignment)) return; } - final childTapPosition = boundaries.viewportToContentPosition(controller, viewportTapPosition); - nextScaleState(ChangeSource.gesture, childFocalPoint: childTapPosition); + final childTapPosition = _getChildTapPosition(details.localPosition); + if (childTapPosition != null) { + nextScaleState(ChangeSource.gesture, childFocalPoint: childTapPosition); + } } void animateScale(double? from, double? to) { @@ -454,8 +476,9 @@ class _AvesMagnifierState extends State with TickerProviderStateM onScaleStart: onScaleStart, onScaleUpdate: onScaleUpdate, onScaleEnd: onScaleEnd, - onTapUp: widget.onTap == null ? null : onTap, - onDoubleTap: onDoubleTap, + onTapUp: widget.onTap == null ? null : _onTapUp, + onDoubleTap: _onDoubleTap, + allowDoubleTap: _allowDoubleTap, child: Padding( padding: widget.viewportPadding, child: LayoutBuilder( @@ -533,6 +556,7 @@ typedef MagnifierTapCallback = Function( Alignment alignment, Offset childTapPosition, ); +typedef MagnifierDoubleTapPredicate = bool Function(Offset localPosition); typedef MagnifierDoubleTapCallback = bool Function(Alignment alignment); typedef MagnifierGestureScaleStartCallback = void Function(ScaleStartDetails details, bool doubleTap, ScaleBoundaries boundaries); typedef MagnifierGestureScaleUpdateCallback = bool Function(ScaleUpdateDetails details); diff --git a/plugins/aves_magnifier/lib/src/core/gesture_detector.dart b/plugins/aves_magnifier/lib/src/core/gesture_detector.dart index c737108a5..a1c38e410 100644 --- a/plugins/aves_magnifier/lib/src/core/gesture_detector.dart +++ b/plugins/aves_magnifier/lib/src/core/gesture_detector.dart @@ -1,10 +1,23 @@ -import 'package:aves_magnifier/src/core/scale_gesture_recognizer.dart'; +import 'package:aves_magnifier/aves_magnifier.dart'; import 'package:aves_magnifier/src/pan/edge_hit_detector.dart'; -import 'package:aves_magnifier/src/pan/gesture_detector_scope.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; class MagnifierGestureDetector extends StatefulWidget { + final EdgeHitDetector hitDetector; + + final void Function(ScaleStartDetails details, bool doubleTap)? onScaleStart; + final GestureScaleUpdateCallback? onScaleUpdate; + final GestureScaleEndCallback? onScaleEnd; + + final GestureTapDownCallback? onTapDown; + final GestureTapUpCallback? onTapUp; + final GestureTapDownCallback? onDoubleTap; + + final MagnifierDoubleTapPredicate? allowDoubleTap; + final HitTestBehavior? behavior; + final Widget? child; + const MagnifierGestureDetector({ super.key, required this.hitDetector, @@ -14,22 +27,11 @@ class MagnifierGestureDetector extends StatefulWidget { this.onTapDown, this.onTapUp, this.onDoubleTap, + this.allowDoubleTap, this.behavior, this.child, }); - final EdgeHitDetector hitDetector; - final void Function(ScaleStartDetails details, bool doubleTap)? onScaleStart; - final GestureScaleUpdateCallback? onScaleUpdate; - final GestureScaleEndCallback? onScaleEnd; - - final GestureTapDownCallback? onTapDown; - final GestureTapUpCallback? onTapUp; - final GestureTapDownCallback? onDoubleTap; - - final HitTestBehavior? behavior; - final Widget? child; - @override State createState() => _MagnifierGestureDetectorState(); } @@ -78,8 +80,11 @@ class _MagnifierGestureDetectorState extends State { ); } - gestures[DoubleTapGestureRecognizer] = GestureRecognizerFactoryWithHandlers( - () => DoubleTapGestureRecognizer(debugOwner: this), + gestures[MagnifierDoubleTapGestureRecognizer] = GestureRecognizerFactoryWithHandlers( + () => MagnifierDoubleTapGestureRecognizer( + debugOwner: this, + allowDoubleTap: widget.allowDoubleTap ?? (_) => true, + ), (instance) { final onDoubleTap = widget.onDoubleTap; instance @@ -87,8 +92,11 @@ class _MagnifierGestureDetectorState extends State { ..onDoubleTapDown = _onDoubleTapDown ..onDoubleTap = onDoubleTap != null ? () { - onDoubleTap(doubleTapDetails.value!); - doubleTapDetails.value = null; + final details = doubleTapDetails.value; + if (details != null) { + onDoubleTap(details); + doubleTapDetails.value = null; + } } : null; }, @@ -103,5 +111,28 @@ class _MagnifierGestureDetectorState extends State { void _onDoubleTapCancel() => doubleTapDetails.value = null; - void _onDoubleTapDown(TapDownDetails details) => doubleTapDetails.value = details; + void _onDoubleTapDown(TapDownDetails details) { + if (widget.allowDoubleTap?.call(details.localPosition) ?? true) { + doubleTapDetails.value = details; + } + } +} + +class MagnifierDoubleTapGestureRecognizer extends DoubleTapGestureRecognizer { + final MagnifierDoubleTapPredicate allowDoubleTap; + + MagnifierDoubleTapGestureRecognizer({ + super.debugOwner, + super.supportedDevices, + super.allowedButtonsFilter, + required this.allowDoubleTap, + }); + + @override + bool isPointerAllowed(PointerDownEvent event) { + if (!allowDoubleTap(event.localPosition)) { + return false; + } + return super.isPointerAllowed(event); + } }