#622 viewer: fixed side gesture precedence

This commit is contained in:
Thibault Deckers 2024-03-31 18:37:25 +02:00
parent 46aef919be
commit e1c3bae90b
4 changed files with 114 additions and 45 deletions

View file

@ -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)
## <a id="v1.10.7"></a>[v1.10.7] - 2024-03-12

View file

@ -405,6 +405,7 @@ class _EntryPageViewState extends State<EntryPageView> 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<EntryPageView> 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) {

View file

@ -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<AvesMagnifier> 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<AvesMagnifier> 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);

View file

@ -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<MagnifierGestureDetector> createState() => _MagnifierGestureDetectorState();
}
@ -78,8 +80,11 @@ class _MagnifierGestureDetectorState extends State<MagnifierGestureDetector> {
);
}
gestures[DoubleTapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
() => DoubleTapGestureRecognizer(debugOwner: this),
gestures[MagnifierDoubleTapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<MagnifierDoubleTapGestureRecognizer>(
() => MagnifierDoubleTapGestureRecognizer(
debugOwner: this,
allowDoubleTap: widget.allowDoubleTap ?? (_) => true,
),
(instance) {
final onDoubleTap = widget.onDoubleTap;
instance
@ -87,8 +92,11 @@ class _MagnifierGestureDetectorState extends State<MagnifierGestureDetector> {
..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<MagnifierGestureDetector> {
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);
}
}