#622 viewer: fixed side gesture precedence
This commit is contained in:
parent
46aef919be
commit
e1c3bae90b
4 changed files with 114 additions and 45 deletions
|
@ -21,6 +21,7 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
- crash when decoding large region
|
- crash when decoding large region
|
||||||
- viewer position drift during scale
|
- 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
|
## <a id="v1.10.7"></a>[v1.10.7] - 2024-03-12
|
||||||
|
|
||||||
|
|
|
@ -405,6 +405,7 @@ class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateM
|
||||||
controller: controller ?? _magnifierController,
|
controller: controller ?? _magnifierController,
|
||||||
contentSize: displaySize ?? entry.displaySize,
|
contentSize: displaySize ?? entry.displaySize,
|
||||||
allowOriginalScaleBeyondRange: !isWallpaperMode,
|
allowOriginalScaleBeyondRange: !isWallpaperMode,
|
||||||
|
allowDoubleTap: _allowDoubleTap,
|
||||||
minScale: minScale,
|
minScale: minScale,
|
||||||
maxScale: maxScale,
|
maxScale: maxScale,
|
||||||
initialScale: viewerController.initialScale,
|
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) {
|
if (settings.viewerGestureSideTapNext && alignment != null) {
|
||||||
final x = alignment.x;
|
final x = alignment.x;
|
||||||
final sideRatio = _getSideRatio();
|
final sideRatio = _getSideRatio();
|
||||||
if (sideRatio != null) {
|
if (sideRatio != null) {
|
||||||
const animate = false;
|
const animate = false;
|
||||||
if (x < sideRatio) {
|
if (x < sideRatio) {
|
||||||
(context.isRtl ? const ShowNextEntryNotification(animate: animate) : const ShowPreviousEntryNotification(animate: animate)).dispatch(context);
|
return context.isRtl ? const ShowNextEntryNotification(animate: animate) : const ShowPreviousEntryNotification(animate: animate);
|
||||||
return;
|
|
||||||
} else if (x > 1 - sideRatio) {
|
} else if (x > 1 - sideRatio) {
|
||||||
(context.isRtl ? const ShowPreviousEntryNotification(animate: animate) : const ShowNextEntryNotification(animate: animate)).dispatch(context);
|
return context.isRtl ? const ShowPreviousEntryNotification(animate: animate) : const ShowNextEntryNotification(animate: animate);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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) {
|
void _onMediaCommand(MediaCommandEvent event) {
|
||||||
|
|
|
@ -36,6 +36,7 @@ class AvesMagnifier extends StatefulWidget {
|
||||||
|
|
||||||
final bool allowOriginalScaleBeyondRange;
|
final bool allowOriginalScaleBeyondRange;
|
||||||
final bool allowGestureScaleBeyondRange;
|
final bool allowGestureScaleBeyondRange;
|
||||||
|
final MagnifierDoubleTapCallback? allowDoubleTap;
|
||||||
final double panInertia;
|
final double panInertia;
|
||||||
|
|
||||||
// Defines the minimum size in which the image will be allowed to assume, it is proportional to the original image size.
|
// 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.viewportPadding = EdgeInsets.zero,
|
||||||
this.allowOriginalScaleBeyondRange = true,
|
this.allowOriginalScaleBeyondRange = true,
|
||||||
this.allowGestureScaleBeyondRange = true,
|
this.allowGestureScaleBeyondRange = true,
|
||||||
|
this.allowDoubleTap,
|
||||||
this.minScale = const ScaleLevel(factor: .0),
|
this.minScale = const ScaleLevel(factor: .0),
|
||||||
this.maxScale = const ScaleLevel(factor: double.infinity),
|
this.maxScale = const ScaleLevel(factor: double.infinity),
|
||||||
this.initialScale = const ScaleLevel(ref: ScaleReference.contained),
|
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);
|
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;
|
final onTap = widget.onTap;
|
||||||
if (onTap == null) return;
|
if (onTap == null) return;
|
||||||
|
|
||||||
final boundaries = scaleBoundaries;
|
|
||||||
if (boundaries == null) return;
|
|
||||||
|
|
||||||
final viewportTapPosition = details.localPosition;
|
final viewportTapPosition = details.localPosition;
|
||||||
final viewportSize = boundaries.viewportSize;
|
final alignment = _getTapAlignment(viewportTapPosition);
|
||||||
final alignment = Alignment(viewportTapPosition.dx / viewportSize.width, viewportTapPosition.dy / viewportSize.height);
|
final childTapPosition = _getChildTapPosition(viewportTapPosition);
|
||||||
final childTapPosition = boundaries.viewportToContentPosition(controller, viewportTapPosition);
|
if (alignment != null && childTapPosition != null) {
|
||||||
|
onTap(context, controller.currentState, alignment, childTapPosition);
|
||||||
onTap(context, controller.currentState, alignment, childTapPosition);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onDoubleTap(TapDownDetails details) {
|
bool _allowDoubleTap(Offset localPosition) {
|
||||||
final boundaries = scaleBoundaries;
|
final allowDoubleTap = widget.allowDoubleTap;
|
||||||
if (boundaries == null) return;
|
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;
|
final onDoubleTap = widget.onDoubleTap;
|
||||||
if (onDoubleTap != null) {
|
if (onDoubleTap != null) {
|
||||||
final viewportSize = boundaries.viewportSize;
|
final alignment = _getTapAlignment(details.localPosition);
|
||||||
final alignment = Alignment(viewportTapPosition.dx / viewportSize.width, viewportTapPosition.dy / viewportSize.height);
|
if (alignment != null && onDoubleTap(alignment)) return;
|
||||||
if (onDoubleTap(alignment) == true) return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final childTapPosition = boundaries.viewportToContentPosition(controller, viewportTapPosition);
|
final childTapPosition = _getChildTapPosition(details.localPosition);
|
||||||
nextScaleState(ChangeSource.gesture, childFocalPoint: childTapPosition);
|
if (childTapPosition != null) {
|
||||||
|
nextScaleState(ChangeSource.gesture, childFocalPoint: childTapPosition);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void animateScale(double? from, double? to) {
|
void animateScale(double? from, double? to) {
|
||||||
|
@ -454,8 +476,9 @@ class _AvesMagnifierState extends State<AvesMagnifier> with TickerProviderStateM
|
||||||
onScaleStart: onScaleStart,
|
onScaleStart: onScaleStart,
|
||||||
onScaleUpdate: onScaleUpdate,
|
onScaleUpdate: onScaleUpdate,
|
||||||
onScaleEnd: onScaleEnd,
|
onScaleEnd: onScaleEnd,
|
||||||
onTapUp: widget.onTap == null ? null : onTap,
|
onTapUp: widget.onTap == null ? null : _onTapUp,
|
||||||
onDoubleTap: onDoubleTap,
|
onDoubleTap: _onDoubleTap,
|
||||||
|
allowDoubleTap: _allowDoubleTap,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: widget.viewportPadding,
|
padding: widget.viewportPadding,
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
|
@ -533,6 +556,7 @@ typedef MagnifierTapCallback = Function(
|
||||||
Alignment alignment,
|
Alignment alignment,
|
||||||
Offset childTapPosition,
|
Offset childTapPosition,
|
||||||
);
|
);
|
||||||
|
typedef MagnifierDoubleTapPredicate = bool Function(Offset localPosition);
|
||||||
typedef MagnifierDoubleTapCallback = bool Function(Alignment alignment);
|
typedef MagnifierDoubleTapCallback = bool Function(Alignment alignment);
|
||||||
typedef MagnifierGestureScaleStartCallback = void Function(ScaleStartDetails details, bool doubleTap, ScaleBoundaries boundaries);
|
typedef MagnifierGestureScaleStartCallback = void Function(ScaleStartDetails details, bool doubleTap, ScaleBoundaries boundaries);
|
||||||
typedef MagnifierGestureScaleUpdateCallback = bool Function(ScaleUpdateDetails details);
|
typedef MagnifierGestureScaleUpdateCallback = bool Function(ScaleUpdateDetails details);
|
||||||
|
|
|
@ -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/edge_hit_detector.dart';
|
||||||
import 'package:aves_magnifier/src/pan/gesture_detector_scope.dart';
|
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
class MagnifierGestureDetector extends StatefulWidget {
|
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({
|
const MagnifierGestureDetector({
|
||||||
super.key,
|
super.key,
|
||||||
required this.hitDetector,
|
required this.hitDetector,
|
||||||
|
@ -14,22 +27,11 @@ class MagnifierGestureDetector extends StatefulWidget {
|
||||||
this.onTapDown,
|
this.onTapDown,
|
||||||
this.onTapUp,
|
this.onTapUp,
|
||||||
this.onDoubleTap,
|
this.onDoubleTap,
|
||||||
|
this.allowDoubleTap,
|
||||||
this.behavior,
|
this.behavior,
|
||||||
this.child,
|
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
|
@override
|
||||||
State<MagnifierGestureDetector> createState() => _MagnifierGestureDetectorState();
|
State<MagnifierGestureDetector> createState() => _MagnifierGestureDetectorState();
|
||||||
}
|
}
|
||||||
|
@ -78,8 +80,11 @@ class _MagnifierGestureDetectorState extends State<MagnifierGestureDetector> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
gestures[DoubleTapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
|
gestures[MagnifierDoubleTapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<MagnifierDoubleTapGestureRecognizer>(
|
||||||
() => DoubleTapGestureRecognizer(debugOwner: this),
|
() => MagnifierDoubleTapGestureRecognizer(
|
||||||
|
debugOwner: this,
|
||||||
|
allowDoubleTap: widget.allowDoubleTap ?? (_) => true,
|
||||||
|
),
|
||||||
(instance) {
|
(instance) {
|
||||||
final onDoubleTap = widget.onDoubleTap;
|
final onDoubleTap = widget.onDoubleTap;
|
||||||
instance
|
instance
|
||||||
|
@ -87,8 +92,11 @@ class _MagnifierGestureDetectorState extends State<MagnifierGestureDetector> {
|
||||||
..onDoubleTapDown = _onDoubleTapDown
|
..onDoubleTapDown = _onDoubleTapDown
|
||||||
..onDoubleTap = onDoubleTap != null
|
..onDoubleTap = onDoubleTap != null
|
||||||
? () {
|
? () {
|
||||||
onDoubleTap(doubleTapDetails.value!);
|
final details = doubleTapDetails.value;
|
||||||
doubleTapDetails.value = null;
|
if (details != null) {
|
||||||
|
onDoubleTap(details);
|
||||||
|
doubleTapDetails.value = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
},
|
},
|
||||||
|
@ -103,5 +111,28 @@ class _MagnifierGestureDetectorState extends State<MagnifierGestureDetector> {
|
||||||
|
|
||||||
void _onDoubleTapCancel() => doubleTapDetails.value = null;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue