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);
+ }
}