#316 viewer: use fling gesture from edge to yield to outer scrollable when zoomed in
This commit is contained in:
parent
228a36fcf0
commit
f0bf70e3b1
14 changed files with 251 additions and 117 deletions
|
@ -466,14 +466,16 @@ class _EntryViewerStackState extends State<EntryViewerStack> 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<EntryViewerStack> 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<void> _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 != page) {
|
||||
final animationDuration = animate ? context.read<DurationsData>().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);
|
||||
}
|
||||
if (_currentEntryIndex != target) {
|
||||
_horizontalPager.jumpToPage(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -60,13 +60,13 @@ class _ViewerThumbnailPreviewState extends State<ViewerThumbnailPreview> {
|
|||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -401,20 +401,38 @@ class _EntryPageViewState extends State<EntryPageView> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -138,9 +138,9 @@ mixin AvesMagnifierControllerDelegate on State<MagnifierCore> {
|
|||
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<MagnifierCore> {
|
|||
|
||||
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<MagnifierCore> {
|
|||
|
||||
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<MagnifierCore> {
|
|||
|
||||
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<MagnifierCore> {
|
|||
}
|
||||
|
||||
/// 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;
|
||||
|
|
|
@ -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<StatefulWidget> createState() => _MagnifierCoreState();
|
||||
}
|
||||
|
||||
class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateMixin, AvesMagnifierControllerDelegate, CornerHitDetector {
|
||||
class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateMixin, AvesMagnifierControllerDelegate, EdgeHitDetector {
|
||||
Offset? _startFocalPoint, _lastViewportFocalPosition;
|
||||
double? _startScale, _quickScaleLastY, _quickScaleLastDistance;
|
||||
late bool _dropped, _doubleTap, _quickScaleMoved;
|
||||
|
@ -57,6 +62,8 @@ class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateM
|
|||
|
||||
ScaleBoundaries? cachedScaleBoundaries;
|
||||
|
||||
static const _flingPointerKind = PointerDeviceKind.unknown;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
@ -104,12 +111,21 @@ class _MagnifierCoreState extends State<MagnifierCore> 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<MagnifierCore> 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<MagnifierCore> 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<MagnifierCore> 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<MediaQueryData>().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<Offset> tween,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<TapDownDetails?> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
68
plugins/aves_magnifier/lib/src/pan/edge_hit_detector.dart
Normal file
68
plugins/aves_magnifier/lib/src/pan/edge_hit_detector.dart
Normal file
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -10,6 +10,7 @@ dependencies:
|
|||
sdk: flutter
|
||||
equatable:
|
||||
provider:
|
||||
tuple:
|
||||
|
||||
dev_dependencies:
|
||||
flutter_lints:
|
||||
|
|
Loading…
Reference in a new issue