From f0bf70e3b1e37078e4ace746af975b10c41e1697 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 20 Feb 2023 17:23:26 +0100 Subject: [PATCH] #316 viewer: use fling gesture from edge to yield to outer scrollable when zoomed in --- lib/widgets/viewer/entry_viewer_stack.dart | 36 ++++++--- lib/widgets/viewer/notifications.dart | 37 +++++++--- .../viewer/overlay/thumbnail_preview.dart | 4 +- .../viewer/visual/entry_page_view.dart | 22 +++++- .../src/controller/controller_delegate.dart | 24 +++--- plugins/aves_magnifier/lib/src/core/core.dart | 74 ++++++++++++++++++- .../lib/src/core/gesture_detector.dart | 4 +- .../src/core/scale_gesture_recognizer.dart | 11 +-- plugins/aves_magnifier/lib/src/magnifier.dart | 4 + .../lib/src/pan/corner_hit_detector.dart | 69 ----------------- .../lib/src/pan/edge_hit_detector.dart | 68 +++++++++++++++++ .../lib/src/pan/gesture_detector_scope.dart | 6 ++ plugins/aves_magnifier/pubspec.lock | 8 ++ plugins/aves_magnifier/pubspec.yaml | 1 + 14 files changed, 251 insertions(+), 117 deletions(-) delete mode 100644 plugins/aves_magnifier/lib/src/pan/corner_hit_detector.dart create mode 100644 plugins/aves_magnifier/lib/src/pan/edge_hit_detector.dart diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index c157b1ae8..b805798fa 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -466,14 +466,16 @@ class _EntryViewerStackState extends State with EntryViewContr if (!_overlayVisible.value) { _overlayVisible.value = true; } + } else if (notification is PopVisualNotification) { + _popVisual(); } else if (notification is ShowInfoPageNotification) { _goToVerticalPage(infoPage); - } else if (notification is JumpToPreviousEntryNotification) { - _jumpToHorizontalPageByDelta(-1); - } else if (notification is JumpToNextEntryNotification) { - _jumpToHorizontalPageByDelta(1); - } else if (notification is JumpToEntryNotification) { - _jumpToHorizontalPageByIndex(notification.index); + } else if (notification is ShowPreviousEntryNotification) { + _goToHorizontalPageByDelta(delta: -1, animate: notification.animate); + } else if (notification is ShowNextEntryNotification) { + _goToHorizontalPageByDelta(delta: 1, animate: notification.animate); + } else if (notification is ShowEntryNotification) { + _goToHorizontalPageByIndex(page: notification.index, animate: notification.animate); } else if (notification is VideoActionNotification) { final controller = notification.controller; final action = notification.action; @@ -545,23 +547,33 @@ class _EntryViewerStackState extends State with EntryViewContr } } - void _jumpToHorizontalPageByDelta(int delta) { + void _goToHorizontalPageByDelta({required int delta, required bool animate}) { if (_horizontalPager.positions.isEmpty) return; final page = _horizontalPager.page?.round(); if (page != null) { - _jumpToHorizontalPageByIndex(page + delta); + _goToHorizontalPageByIndex(page: page + delta, animate: animate); } } - void _jumpToHorizontalPageByIndex(int target) { + Future _goToHorizontalPageByIndex({required int page, required bool animate}) async { final _collection = collection; if (_collection != null) { if (!widget.viewerController.repeat) { - target = target.clamp(0, _collection.entryCount - 1); + page = page.clamp(0, _collection.entryCount - 1); } - if (_currentEntryIndex != target) { - _horizontalPager.jumpToPage(target); + if (_currentEntryIndex != page) { + final animationDuration = animate ? context.read().viewerVerticalPageScrollAnimation : Duration.zero; + if (animationDuration > Duration.zero) { + // duration & curve should feel similar to changing page by vertical fling + await _horizontalPager.animateToPage( + page, + duration: animationDuration, + curve: Curves.easeOutQuart, + ); + } else { + _horizontalPager.jumpToPage(page); + } } } } diff --git a/lib/widgets/viewer/notifications.dart b/lib/widgets/viewer/notifications.dart index 889a82127..a8d0736ed 100644 --- a/lib/widgets/viewer/notifications.dart +++ b/lib/widgets/viewer/notifications.dart @@ -6,6 +6,9 @@ import 'package:aves/widgets/viewer/video/controller.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/widgets.dart'; +@immutable +class PopVisualNotification extends Notification {} + @immutable class ShowImageNotification extends Notification {} @@ -13,10 +16,29 @@ class ShowImageNotification extends Notification {} class ShowInfoPageNotification extends Notification {} @immutable -class TvShowLessInfoNotification extends Notification {} +class ShowPreviousEntryNotification extends Notification { + final bool animate; + + const ShowPreviousEntryNotification({required this.animate}); +} @immutable -class TvShowMoreInfoNotification extends Notification {} +class ShowNextEntryNotification extends Notification { + final bool animate; + + const ShowNextEntryNotification({required this.animate}); +} + +@immutable +class ShowEntryNotification extends Notification { + final bool animate; + final int index; + + const ShowEntryNotification({ + required this.animate, + required this.index, + }); +} @immutable class ToggleOverlayNotification extends Notification { @@ -26,17 +48,10 @@ class ToggleOverlayNotification extends Notification { } @immutable -class JumpToPreviousEntryNotification extends Notification {} +class TvShowLessInfoNotification extends Notification {} @immutable -class JumpToNextEntryNotification extends Notification {} - -@immutable -class JumpToEntryNotification extends Notification { - final int index; - - const JumpToEntryNotification({required this.index}); -} +class TvShowMoreInfoNotification extends Notification {} @immutable class VideoActionNotification extends Notification { diff --git a/lib/widgets/viewer/overlay/thumbnail_preview.dart b/lib/widgets/viewer/overlay/thumbnail_preview.dart index 962826fc9..5a0f27bb9 100644 --- a/lib/widgets/viewer/overlay/thumbnail_preview.dart +++ b/lib/widgets/viewer/overlay/thumbnail_preview.dart @@ -60,13 +60,13 @@ class _ViewerThumbnailPreviewState extends State { entryCount: entryCount, entryBuilder: (index) => 0 <= index && index < entryCount ? entries[index] : null, indexNotifier: _entryIndexNotifier, - onTap: (index) => JumpToEntryNotification(index: index).dispatch(context), + onTap: (index) => ShowEntryNotification(animate: false, index: index).dispatch(context), ); } void _onScrollerIndexChanged() => _debouncer(() { if (mounted) { - JumpToEntryNotification(index: _entryIndexNotifier.value).dispatch(context); + ShowEntryNotification(animate: false, index: _entryIndexNotifier.value).dispatch(context); } }); } diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index 0b64cd867..e5712ed24 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -401,20 +401,38 @@ class _EntryPageViewState extends State with SingleTickerProvider onScaleStart: onScaleStart, onScaleUpdate: onScaleUpdate, onScaleEnd: onScaleEnd, + onFling: _onFling, onTap: (c, s, a, p) => _onTap(alignment: a), onDoubleTap: onDoubleTap, child: child, ); } + void _onFling(AxisDirection direction) { + switch (direction) { + case AxisDirection.left: + const ShowPreviousEntryNotification(animate: true).dispatch(context); + break; + case AxisDirection.right: + const ShowNextEntryNotification(animate: true).dispatch(context); + break; + case AxisDirection.up: + PopVisualNotification().dispatch(context); + break; + case AxisDirection.down: + ShowInfoPageNotification().dispatch(context); + break; + } + } + void _onTap({Alignment? alignment}) { if (settings.viewerGestureSideTapNext && alignment != null) { final x = alignment.x; if (x < sideRatio) { - JumpToPreviousEntryNotification().dispatch(context); + const ShowPreviousEntryNotification(animate: false).dispatch(context); return; } else if (x > 1 - sideRatio) { - JumpToNextEntryNotification().dispatch(context); + const ShowNextEntryNotification(animate: false).dispatch(context); return; } } diff --git a/plugins/aves_magnifier/lib/src/controller/controller_delegate.dart b/plugins/aves_magnifier/lib/src/controller/controller_delegate.dart index 93a47d0f1..c2de57bbb 100644 --- a/plugins/aves_magnifier/lib/src/controller/controller_delegate.dart +++ b/plugins/aves_magnifier/lib/src/controller/controller_delegate.dart @@ -138,9 +138,9 @@ mixin AvesMagnifierControllerDelegate on State { controller.setScaleState(nextScaleState, source, childFocalPoint: childFocalPoint); } - CornersRange cornersX({double? scale}) { + EdgeRange getXEdges({double? scale}) { final boundaries = scaleBoundaries; - if (boundaries == null) return const CornersRange(0, 0); + if (boundaries == null) return const EdgeRange(0, 0); final _scale = scale ?? this.scale!; @@ -152,12 +152,12 @@ mixin AvesMagnifierControllerDelegate on State { final minX = ((positionX - 1).abs() / 2) * widthDiff * -1; final maxX = ((positionX + 1).abs() / 2) * widthDiff; - return CornersRange(minX, maxX); + return EdgeRange(minX, maxX); } - CornersRange cornersY({double? scale}) { + EdgeRange getYEdges({double? scale}) { final boundaries = scaleBoundaries; - if (boundaries == null) return const CornersRange(0, 0); + if (boundaries == null) return const EdgeRange(0, 0); final _scale = scale ?? this.scale!; @@ -169,7 +169,7 @@ mixin AvesMagnifierControllerDelegate on State { final minY = ((positionY - 1).abs() / 2) * heightDiff * -1; final maxY = ((positionY + 1).abs() / 2) * heightDiff; - return CornersRange(minY, maxY); + return EdgeRange(minY, maxY); } Offset clampPosition({Offset? position, double? scale}) { @@ -187,14 +187,14 @@ mixin AvesMagnifierControllerDelegate on State { var finalX = 0.0; if (screenWidth < computedWidth) { - final cornersX = this.cornersX(scale: _scale); - finalX = _position.dx.clamp(cornersX.min, cornersX.max); + final range = getXEdges(scale: _scale); + finalX = _position.dx.clamp(range.min, range.max); } var finalY = 0.0; if (screenHeight < computedHeight) { - final cornersY = this.cornersY(scale: _scale); - finalY = _position.dy.clamp(cornersY.min, cornersY.max); + final range = getYEdges(scale: _scale); + finalY = _position.dy.clamp(range.min, range.max); } return Offset(finalX, finalY); @@ -202,8 +202,8 @@ mixin AvesMagnifierControllerDelegate on State { } /// Simple class to store a min and a max value -class CornersRange { - const CornersRange(this.min, this.max); +class EdgeRange { + const EdgeRange(this.min, this.max); final double min; final double max; diff --git a/plugins/aves_magnifier/lib/src/core/core.dart b/plugins/aves_magnifier/lib/src/core/core.dart index 0ce553290..531934cd6 100644 --- a/plugins/aves_magnifier/lib/src/core/core.dart +++ b/plugins/aves_magnifier/lib/src/core/core.dart @@ -5,11 +5,14 @@ import 'package:aves_magnifier/src/controller/controller_delegate.dart'; import 'package:aves_magnifier/src/controller/state.dart'; import 'package:aves_magnifier/src/core/gesture_detector.dart'; import 'package:aves_magnifier/src/magnifier.dart'; -import 'package:aves_magnifier/src/pan/corner_hit_detector.dart'; +import 'package:aves_magnifier/src/pan/edge_hit_detector.dart'; import 'package:aves_magnifier/src/scale/scale_boundaries.dart'; import 'package:aves_magnifier/src/scale/state.dart'; import 'package:equatable/equatable.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; +import 'package:tuple/tuple.dart'; /// Internal widget in which controls all animations lifecycle, core responses /// to user gestures, updates to the controller state and mounts the entire Layout @@ -21,6 +24,7 @@ class MagnifierCore extends StatefulWidget { final MagnifierGestureScaleStartCallback? onScaleStart; final MagnifierGestureScaleUpdateCallback? onScaleUpdate; final MagnifierGestureScaleEndCallback? onScaleEnd; + final MagnifierGestureFlingCallback? onFling; final MagnifierTapCallback? onTap; final MagnifierDoubleTapCallback? onDoubleTap; final Widget child; @@ -34,6 +38,7 @@ class MagnifierCore extends StatefulWidget { this.onScaleStart, this.onScaleUpdate, this.onScaleEnd, + this.onFling, this.onTap, this.onDoubleTap, required this.child, @@ -43,7 +48,7 @@ class MagnifierCore extends StatefulWidget { State createState() => _MagnifierCoreState(); } -class _MagnifierCoreState extends State with TickerProviderStateMixin, AvesMagnifierControllerDelegate, CornerHitDetector { +class _MagnifierCoreState extends State with TickerProviderStateMixin, AvesMagnifierControllerDelegate, EdgeHitDetector { Offset? _startFocalPoint, _lastViewportFocalPosition; double? _startScale, _quickScaleLastY, _quickScaleLastDistance; late bool _dropped, _doubleTap, _quickScaleMoved; @@ -57,6 +62,8 @@ class _MagnifierCoreState extends State with TickerProviderStateM ScaleBoundaries? cachedScaleBoundaries; + static const _flingPointerKind = PointerDeviceKind.unknown; + @override void initState() { super.initState(); @@ -104,12 +111,21 @@ class _MagnifierCoreState extends State with TickerProviderStateM controller.update(position: _positionAnimation.value, source: ChangeSource.animation); } + Stopwatch? _scaleStopwatch; + VelocityTracker? _velocityTracker; + var _mayFlingLTRB = const Tuple4(false, false, false, false); + void onScaleStart(ScaleStartDetails details, bool doubleTap) { final boundaries = scaleBoundaries; if (boundaries == null) return; widget.onScaleStart?.call(details, doubleTap, boundaries); + _scaleStopwatch = Stopwatch()..start(); + _velocityTracker = VelocityTracker.withKind(_flingPointerKind); + _mayFlingLTRB = const Tuple4(true, true, true, true); + _updateMayFling(); + _startScale = scale; _startFocalPoint = details.localFocalPoint; _lastViewportFocalPosition = _startFocalPoint; @@ -130,6 +146,12 @@ class _MagnifierCoreState extends State with TickerProviderStateM _dropped |= widget.onScaleUpdate?.call(details) ?? false; if (_dropped) return; + final elapsed = _scaleStopwatch?.elapsed; + if (elapsed != null) { + _velocityTracker?.addPosition(elapsed, details.focalPoint); + } + _updateMayFling(); + double newScale; if (_doubleTap) { // quick scale, aka one finger zoom @@ -168,6 +190,29 @@ class _MagnifierCoreState extends State with TickerProviderStateM widget.onScaleEnd?.call(details); + _updateMayFling(); + final estimate = _velocityTracker?.getVelocityEstimate(); + final onFling = widget.onFling; + if (estimate != null && onFling != null) { + if (_isFlingGesture(estimate, _flingPointerKind, Axis.horizontal)) { + final left = _mayFlingLTRB.item1; + final right = _mayFlingLTRB.item3; + if (left) { + onFling(AxisDirection.left); + } else if (right) { + onFling(AxisDirection.right); + } + } else if (_isFlingGesture(estimate, _flingPointerKind, Axis.vertical)) { + final up = _mayFlingLTRB.item2; + final down = _mayFlingLTRB.item4; + if (up) { + onFling(AxisDirection.up); + } else if (down) { + onFling(AxisDirection.down); + } + } + } + final _position = controller.position; final _scale = controller.scale!; final maxScale = boundaries.maxScale; @@ -208,6 +253,31 @@ class _MagnifierCoreState extends State with TickerProviderStateM } } + void _updateMayFling() { + final xHit = getXEdgeHit(); + final yHit = getYEdgeHit(); + _mayFlingLTRB = Tuple4( + _mayFlingLTRB.item1 && xHit.hasHitMin, + _mayFlingLTRB.item2 && yHit.hasHitMin, + _mayFlingLTRB.item3 && xHit.hasHitMax, + _mayFlingLTRB.item4 && yHit.hasHitMax, + ); + } + + bool _isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind, Axis axis) { + final gestureSettings = context.read().gestureSettings; + const minVelocity = kMinFlingVelocity; + final minDistance = computeHitSlop(kind, gestureSettings); + + final pps = estimate.pixelsPerSecond; + final offset = estimate.offset; + if (axis == Axis.horizontal) { + return pps.dx.abs() > minVelocity && offset.dx.abs() > minDistance; + } else { + return pps.dy.abs() > minVelocity && offset.dy.abs() > minDistance; + } + } + Duration _getAnimationDurationForVelocity({ required Cubic curve, required Tween tween, diff --git a/plugins/aves_magnifier/lib/src/core/gesture_detector.dart b/plugins/aves_magnifier/lib/src/core/gesture_detector.dart index 76a6e1348..e18c4283f 100644 --- a/plugins/aves_magnifier/lib/src/core/gesture_detector.dart +++ b/plugins/aves_magnifier/lib/src/core/gesture_detector.dart @@ -1,5 +1,5 @@ import 'package:aves_magnifier/src/core/scale_gesture_recognizer.dart'; -import 'package:aves_magnifier/src/pan/corner_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/widgets.dart'; @@ -19,7 +19,7 @@ class MagnifierGestureDetector extends StatefulWidget { this.child, }); - final CornerHitDetector hitDetector; + final EdgeHitDetector hitDetector; final void Function(ScaleStartDetails details, bool doubleTap)? onScaleStart; final GestureScaleUpdateCallback? onScaleUpdate; final GestureScaleEndCallback? onScaleEnd; diff --git a/plugins/aves_magnifier/lib/src/core/scale_gesture_recognizer.dart b/plugins/aves_magnifier/lib/src/core/scale_gesture_recognizer.dart index eb95393b9..361db25d6 100644 --- a/plugins/aves_magnifier/lib/src/core/scale_gesture_recognizer.dart +++ b/plugins/aves_magnifier/lib/src/core/scale_gesture_recognizer.dart @@ -1,12 +1,12 @@ import 'dart:math'; import 'package:aves_magnifier/aves_magnifier.dart'; -import 'package:aves_magnifier/src/pan/corner_hit_detector.dart'; +import 'package:aves_magnifier/src/pan/edge_hit_detector.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; class MagnifierGestureRecognizer extends ScaleGestureRecognizer { - final CornerHitDetector hitDetector; + final EdgeHitDetector hitDetector; final MagnifierGestureDetectorScope scope; final ValueNotifier doubleTapDetails; @@ -104,14 +104,15 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer { } final validateAxis = scope.axis; + final canFling = scope.escapeByFling; final move = _initialFocalPoint! - _currentFocalPoint!; bool shouldMove = scope.acceptPointerEvent?.call(move) ?? false; if (!shouldMove) { if (validateAxis.length == 2) { // the image is the descendant of gesture detector(s) handling drag in both directions - final shouldMoveX = validateAxis.contains(Axis.horizontal) && hitDetector.shouldMoveX(move); - final shouldMoveY = validateAxis.contains(Axis.vertical) && hitDetector.shouldMoveY(move); + final shouldMoveX = validateAxis.contains(Axis.horizontal) && hitDetector.shouldMoveX(move, canFling); + final shouldMoveY = validateAxis.contains(Axis.vertical) && hitDetector.shouldMoveY(move, canFling); if (shouldMoveX == shouldMoveY) { // consistently can/cannot pan the image in both direction the same way shouldMove = shouldMoveX; @@ -122,7 +123,7 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer { } } else { // the image is the descendant of a gesture detector handling drag in one direction - shouldMove = validateAxis.contains(Axis.vertical) ? hitDetector.shouldMoveY(move) : hitDetector.shouldMoveX(move); + shouldMove = validateAxis.contains(Axis.vertical) ? hitDetector.shouldMoveY(move, canFling) : hitDetector.shouldMoveX(move, canFling); } } diff --git a/plugins/aves_magnifier/lib/src/magnifier.dart b/plugins/aves_magnifier/lib/src/magnifier.dart index 88c3f7586..38f5e1d02 100644 --- a/plugins/aves_magnifier/lib/src/magnifier.dart +++ b/plugins/aves_magnifier/lib/src/magnifier.dart @@ -32,6 +32,7 @@ class AvesMagnifier extends StatelessWidget { this.onScaleStart, this.onScaleUpdate, this.onScaleEnd, + this.onFling, this.onTap, this.onDoubleTap, required this.child, @@ -58,6 +59,7 @@ class AvesMagnifier extends StatelessWidget { final MagnifierGestureScaleStartCallback? onScaleStart; final MagnifierGestureScaleUpdateCallback? onScaleUpdate; final MagnifierGestureScaleEndCallback? onScaleEnd; + final MagnifierGestureFlingCallback? onFling; final MagnifierTapCallback? onTap; final MagnifierDoubleTapCallback? onDoubleTap; final Widget child; @@ -82,6 +84,7 @@ class AvesMagnifier extends StatelessWidget { onScaleStart: onScaleStart, onScaleUpdate: onScaleUpdate, onScaleEnd: onScaleEnd, + onFling: onFling, onTap: onTap, onDoubleTap: onDoubleTap, child: child, @@ -101,3 +104,4 @@ typedef MagnifierDoubleTapCallback = bool Function(Alignment alignment); typedef MagnifierGestureScaleStartCallback = void Function(ScaleStartDetails details, bool doubleTap, ScaleBoundaries boundaries); typedef MagnifierGestureScaleUpdateCallback = bool Function(ScaleUpdateDetails details); typedef MagnifierGestureScaleEndCallback = void Function(ScaleEndDetails details); +typedef MagnifierGestureFlingCallback = void Function(AxisDirection direction); diff --git a/plugins/aves_magnifier/lib/src/pan/corner_hit_detector.dart b/plugins/aves_magnifier/lib/src/pan/corner_hit_detector.dart deleted file mode 100644 index 8faa9e8a4..000000000 --- a/plugins/aves_magnifier/lib/src/pan/corner_hit_detector.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:aves_magnifier/src/controller/controller_delegate.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; - -mixin CornerHitDetector on AvesMagnifierControllerDelegate { - // the child width/height is not accurate for some image size & scale combos - // e.g. 3580.0 * 0.1005586592178771 yields 360.0 - // but 4764.0 * 0.07556675062972293 yields 360.00000000000006 - // so be sure to compare with `precisionErrorTolerance` - - _CornerHit _hitCornersX() { - final boundaries = scaleBoundaries; - if (boundaries == null) return const _CornerHit(false, false); - - final childWidth = boundaries.childSize.width * scale!; - final viewportWidth = boundaries.viewportSize.width; - if (viewportWidth + precisionErrorTolerance >= childWidth) { - return const _CornerHit(true, true); - } - final x = -position.dx; - final cornersX = this.cornersX(); - return _CornerHit(x <= cornersX.min, x >= cornersX.max); - } - - _CornerHit _hitCornersY() { - final boundaries = scaleBoundaries; - if (boundaries == null) return const _CornerHit(false, false); - - final childHeight = boundaries.childSize.height * scale!; - final viewportHeight = boundaries.viewportSize.height; - if (viewportHeight + precisionErrorTolerance >= childHeight) { - return const _CornerHit(true, true); - } - final y = -position.dy; - final cornersY = this.cornersY(); - return _CornerHit(y <= cornersY.min, y >= cornersY.max); - } - - bool shouldMoveX(Offset move) { - final hitCornersX = _hitCornersX(); - if (hitCornersX.hasHitAny && move != Offset.zero) { - if (hitCornersX.hasHitBoth) return false; - if (hitCornersX.hasHitMax) return move.dx < 0; - return move.dx > 0; - } - return true; - } - - bool shouldMoveY(Offset move) { - final hitCornersY = _hitCornersY(); - if (hitCornersY.hasHitAny && move != Offset.zero) { - if (hitCornersY.hasHitBoth) return false; - if (hitCornersY.hasHitMax) return move.dy < 0; - return move.dy > 0; - } - return true; - } -} - -class _CornerHit { - const _CornerHit(this.hasHitMin, this.hasHitMax); - - final bool hasHitMin; - final bool hasHitMax; - - bool get hasHitAny => hasHitMin || hasHitMax; - - bool get hasHitBoth => hasHitMin && hasHitMax; -} diff --git a/plugins/aves_magnifier/lib/src/pan/edge_hit_detector.dart b/plugins/aves_magnifier/lib/src/pan/edge_hit_detector.dart new file mode 100644 index 000000000..551c7c0a9 --- /dev/null +++ b/plugins/aves_magnifier/lib/src/pan/edge_hit_detector.dart @@ -0,0 +1,68 @@ +import 'package:aves_magnifier/src/controller/controller_delegate.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +mixin EdgeHitDetector on AvesMagnifierControllerDelegate { + // the child width/height is not accurate for some image size & scale combos + // e.g. 3580.0 * 0.1005586592178771 yields 360.0 + // but 4764.0 * 0.07556675062972293 yields 360.00000000000006 + // so be sure to compare with `precisionErrorTolerance` + + EdgeHit getXEdgeHit() { + final boundaries = scaleBoundaries; + if (boundaries == null) return const EdgeHit(false, false); + + final childWidth = boundaries.childSize.width * scale!; + final viewportWidth = boundaries.viewportSize.width; + if (viewportWidth + precisionErrorTolerance >= childWidth) { + return const EdgeHit(true, true); + } + final x = -position.dx; + final range = getXEdges(); + return EdgeHit(x <= range.min, x >= range.max); + } + + EdgeHit getYEdgeHit() { + final boundaries = scaleBoundaries; + if (boundaries == null) return const EdgeHit(false, false); + + final childHeight = boundaries.childSize.height * scale!; + final viewportHeight = boundaries.viewportSize.height; + if (viewportHeight + precisionErrorTolerance >= childHeight) { + return const EdgeHit(true, true); + } + final y = -position.dy; + final range = getYEdges(); + return EdgeHit(y <= range.min, y >= range.max); + } + + bool shouldMoveX(Offset move, bool canFling) { + final hit = getXEdgeHit(); + return _shouldMove(hit, move.dx) || (canFling && !hit.hasHitBoth); + } + + bool shouldMoveY(Offset move, bool canFling) { + final hit = getYEdgeHit(); + return _shouldMove(hit, move.dy) || (canFling && !hit.hasHitBoth); + } + + bool _shouldMove(EdgeHit hit, double move) { + if (hit.hasHitAny && move != 0) { + if (hit.hasHitBoth) return false; + if (hit.hasHitMax) return move < 0; + return move > 0; + } + return true; + } +} + +class EdgeHit { + const EdgeHit(this.hasHitMin, this.hasHitMax); + + final bool hasHitMin; + final bool hasHitMax; + + bool get hasHitAny => hasHitMin || hasHitMax; + + bool get hasHitBoth => hasHitMin && hasHitMax; +} diff --git a/plugins/aves_magnifier/lib/src/pan/gesture_detector_scope.dart b/plugins/aves_magnifier/lib/src/pan/gesture_detector_scope.dart index 82da68aea..0ac3689a6 100644 --- a/plugins/aves_magnifier/lib/src/pan/gesture_detector_scope.dart +++ b/plugins/aves_magnifier/lib/src/pan/gesture_detector_scope.dart @@ -14,12 +14,18 @@ class MagnifierGestureDetectorScope extends InheritedWidget { // <1: less reactive but gives the most leeway to other recognizers // 1: will not be able to compete with a `HorizontalDragGestureRecognizer` up the widget tree final double touchSlopFactor; + + // when zoomed in and hitting an edge, allow using a fling gesture to go to the previous/next page, + // instead of yielding to the outer scrollable right away + final bool escapeByFling; + final bool? Function(Offset move)? acceptPointerEvent; const MagnifierGestureDetectorScope({ super.key, required this.axis, this.touchSlopFactor = .8, + this.escapeByFling = true, this.acceptPointerEvent, required Widget child, }) : super(child: child); diff --git a/plugins/aves_magnifier/pubspec.lock b/plugins/aves_magnifier/pubspec.lock index e07b21f96..bb225830d 100644 --- a/plugins/aves_magnifier/pubspec.lock +++ b/plugins/aves_magnifier/pubspec.lock @@ -91,6 +91,14 @@ packages: description: flutter source: sdk version: "0.0.99" + tuple: + dependency: "direct main" + description: + name: tuple + sha256: "0ea99cd2f9352b2586583ab2ce6489d1f95a5f6de6fb9492faaf97ae2060f0aa" + url: "https://pub.dev" + source: hosted + version: "2.0.1" vector_math: dependency: transitive description: diff --git a/plugins/aves_magnifier/pubspec.yaml b/plugins/aves_magnifier/pubspec.yaml index bf57a7c34..3ef32e0bd 100644 --- a/plugins/aves_magnifier/pubspec.yaml +++ b/plugins/aves_magnifier/pubspec.yaml @@ -10,6 +10,7 @@ dependencies: sdk: flutter equatable: provider: + tuple: dev_dependencies: flutter_lints: