mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-05-31 08:08:19 +08:00
opt image viewer gesture
Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
@@ -1,26 +1,29 @@
|
|||||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
|
|
||||||
class CustomHorizontalDragGestureRecognizer
|
mixin InitialPositionMixin on GestureRecognizer {
|
||||||
extends HorizontalDragGestureRecognizer {
|
|
||||||
CustomHorizontalDragGestureRecognizer({
|
|
||||||
super.debugOwner,
|
|
||||||
super.supportedDevices,
|
|
||||||
super.allowedButtonsFilter,
|
|
||||||
});
|
|
||||||
|
|
||||||
Offset? _initialPosition;
|
Offset? _initialPosition;
|
||||||
Offset? get initialPosition => _initialPosition;
|
Offset? get initialPosition => _initialPosition;
|
||||||
|
|
||||||
@override
|
|
||||||
DeviceGestureSettings get gestureSettings => _gestureSettings;
|
|
||||||
final _gestureSettings = DeviceGestureSettings(touchSlop: touchSlopH);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void addAllowedPointer(PointerDownEvent event) {
|
void addAllowedPointer(PointerDownEvent event) {
|
||||||
super.addAllowedPointer(event);
|
super.addAllowedPointer(event);
|
||||||
_initialPosition = event.position;
|
_initialPosition = event.position;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomHorizontalDragGestureRecognizer
|
||||||
|
extends HorizontalDragGestureRecognizer
|
||||||
|
with InitialPositionMixin {
|
||||||
|
CustomHorizontalDragGestureRecognizer({
|
||||||
|
super.debugOwner,
|
||||||
|
super.supportedDevices,
|
||||||
|
super.allowedButtonsFilter,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
DeviceGestureSettings get gestureSettings => _gestureSettings;
|
||||||
|
final _gestureSettings = DeviceGestureSettings(touchSlop: touchSlopH);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool hasSufficientGlobalDistanceToAccept(
|
bool hasSufficientGlobalDistanceToAccept(
|
||||||
@@ -41,7 +44,7 @@ double touchSlopH = Pref.touchSlopH;
|
|||||||
|
|
||||||
bool _computeHitSlop(
|
bool _computeHitSlop(
|
||||||
double globalDistanceMoved,
|
double globalDistanceMoved,
|
||||||
DeviceGestureSettings? settings,
|
DeviceGestureSettings settings,
|
||||||
PointerDeviceKind kind,
|
PointerDeviceKind kind,
|
||||||
Offset? initialPosition,
|
Offset? initialPosition,
|
||||||
Offset lastPosition,
|
Offset lastPosition,
|
||||||
@@ -53,10 +56,10 @@ bool _computeHitSlop(
|
|||||||
case PointerDeviceKind.invertedStylus:
|
case PointerDeviceKind.invertedStylus:
|
||||||
case PointerDeviceKind.unknown:
|
case PointerDeviceKind.unknown:
|
||||||
case PointerDeviceKind.touch:
|
case PointerDeviceKind.touch:
|
||||||
return globalDistanceMoved > touchSlopH &&
|
return globalDistanceMoved > settings.touchSlop! &&
|
||||||
_calc(initialPosition!, lastPosition);
|
_calc(initialPosition!, lastPosition);
|
||||||
case PointerDeviceKind.trackpad:
|
case PointerDeviceKind.trackpad:
|
||||||
return globalDistanceMoved > (settings?.touchSlop ?? kTouchSlop);
|
return globalDistanceMoved > settings.touchSlop!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart';
|
import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart';
|
||||||
|
import 'package:PiliPlus/utils/platform_utils.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
|
|
||||||
mixin ImageGestureRecognizerMixin on GestureRecognizer {
|
mixin ImageGestureRecognizerMixin on GestureRecognizer {
|
||||||
int? _pointer;
|
int? _pointer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void addPointer(PointerDownEvent event) {
|
void addPointer(PointerDownEvent event, {bool isPointerAllowed = true}) {
|
||||||
if (_pointer == event.pointer) {
|
if (_pointer == event.pointer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_pointer = event.pointer;
|
_pointer = event.pointer;
|
||||||
|
if (isPointerAllowed) {
|
||||||
super.addPointer(event);
|
super.addPointer(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
typedef IsBoundaryAllowed =
|
|
||||||
bool Function(Offset? initialPosition, OffsetPair lastPosition);
|
|
||||||
|
|
||||||
class ImageHorizontalDragGestureRecognizer
|
class ImageHorizontalDragGestureRecognizer
|
||||||
extends CustomHorizontalDragGestureRecognizer
|
extends CustomHorizontalDragGestureRecognizer
|
||||||
@@ -26,23 +26,62 @@ class ImageHorizontalDragGestureRecognizer
|
|||||||
super.allowedButtonsFilter,
|
super.allowedButtonsFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
IsBoundaryAllowed? isBoundaryAllowed;
|
static final double _touchSlop = PlatformUtils.isDesktop
|
||||||
|
? kPrecisePointerHitSlop
|
||||||
|
: 3.0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool hasSufficientGlobalDistanceToAccept(
|
DeviceGestureSettings get gestureSettings => _gestureSettings;
|
||||||
PointerDeviceKind pointerDeviceKind,
|
final _gestureSettings = DeviceGestureSettings(touchSlop: _touchSlop);
|
||||||
double? deviceTouchSlop,
|
|
||||||
) {
|
bool isAtLeftEdge = false;
|
||||||
return super.hasSufficientGlobalDistanceToAccept(
|
bool isAtRightEdge = false;
|
||||||
pointerDeviceKind,
|
|
||||||
deviceTouchSlop,
|
void setAtBothEdges() {
|
||||||
) &&
|
isAtLeftEdge = isAtRightEdge = true;
|
||||||
(isBoundaryAllowed?.call(initialPosition, lastPosition) ?? true);
|
}
|
||||||
|
|
||||||
|
bool _isEdgeAllowed(double dx) {
|
||||||
|
if ((initialPosition!.dx - dx).abs() < _touchSlop) return true;
|
||||||
|
if (isAtLeftEdge) {
|
||||||
|
if (isAtRightEdge) {
|
||||||
|
return _hasAcceptedOrChecked = true;
|
||||||
|
}
|
||||||
|
_hasAcceptedOrChecked = true;
|
||||||
|
return initialPosition!.dx < dx;
|
||||||
|
} else if (isAtRightEdge) {
|
||||||
|
_hasAcceptedOrChecked = true;
|
||||||
|
return initialPosition!.dx > dx;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void handleEvent(PointerEvent event) {
|
||||||
isBoundaryAllowed = null;
|
if (!_hasAcceptedOrChecked &&
|
||||||
super.dispose();
|
event is PointerMoveEvent &&
|
||||||
|
_pointer == event.pointer) {
|
||||||
|
if (!_isEdgeAllowed(event.position.dx)) {
|
||||||
|
rejectGesture(event.pointer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.handleEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _hasAcceptedOrChecked = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void acceptGesture(int pointer) {
|
||||||
|
_hasAcceptedOrChecked = true;
|
||||||
|
super.acceptGesture(pointer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void stopTrackingPointer(int pointer) {
|
||||||
|
_hasAcceptedOrChecked = false;
|
||||||
|
isAtLeftEdge = false;
|
||||||
|
isAtRightEdge = false;
|
||||||
|
super.stopTrackingPointer(pointer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/physics.dart' show FrictionSimulation;
|
import 'package:flutter/physics.dart' show FrictionSimulation;
|
||||||
|
import 'package:flutter/scheduler.dart' show SchedulerBinding;
|
||||||
import 'package:flutter/services.dart' show HardwareKeyboard;
|
import 'package:flutter/services.dart' show HardwareKeyboard;
|
||||||
|
|
||||||
///
|
///
|
||||||
@@ -69,7 +70,6 @@ class Viewer extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
|
class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
|
||||||
double get _interactionEndFrictionCoefficient => 0.0001 * _scale; // 0.0000135
|
|
||||||
static const double _scaleFactor = kDefaultMouseScrollToScaleFactor;
|
static const double _scaleFactor = kDefaultMouseScrollToScaleFactor;
|
||||||
|
|
||||||
_GestureType? _gestureType;
|
_GestureType? _gestureType;
|
||||||
@@ -171,6 +171,7 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_stopFling();
|
||||||
_animationController
|
_animationController
|
||||||
..removeListener(_listener)
|
..removeListener(_listener)
|
||||||
..dispose();
|
..dispose();
|
||||||
@@ -265,6 +266,8 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onScaleStart(ScaleStartDetails details) {
|
void _onScaleStart(ScaleStartDetails details) {
|
||||||
|
_stopFling();
|
||||||
|
|
||||||
if (_animationController.isAnimating) {
|
if (_animationController.isAnimating) {
|
||||||
_animationController.stop();
|
_animationController.stop();
|
||||||
}
|
}
|
||||||
@@ -329,40 +332,106 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ref https://github.com/ahnaineh/custom_interactive_viewer
|
||||||
|
int? _flingFrameCallbackId;
|
||||||
|
Simulation? _flingSimulation;
|
||||||
|
Duration? _flingStartTime;
|
||||||
|
double _lastFlingElapsedSeconds = 0.0;
|
||||||
|
Offset _flingDirection = Offset.zero;
|
||||||
|
|
||||||
|
/// Calculate appropriate friction based on velocity magnitude
|
||||||
|
double _calculateDynamicFriction(double velocityMagnitude) {
|
||||||
|
// Use higher friction for faster flicks
|
||||||
|
// These values can be tuned for the feel you want
|
||||||
|
if (velocityMagnitude > 5000) {
|
||||||
|
return 0.03; // Higher friction for very fast flicks
|
||||||
|
} else if (velocityMagnitude > 3000) {
|
||||||
|
return 0.02; // Medium friction for moderate flicks
|
||||||
|
} else {
|
||||||
|
return 0.01; // Lower friction for gentle movements
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startFling(Velocity velocity) {
|
||||||
|
_stopFling();
|
||||||
|
|
||||||
|
final double velocityMagnitude = velocity.pixelsPerSecond.distance;
|
||||||
|
final double frictionCoefficient = _calculateDynamicFriction(
|
||||||
|
velocityMagnitude,
|
||||||
|
);
|
||||||
|
|
||||||
|
_flingSimulation = FrictionSimulation(
|
||||||
|
frictionCoefficient,
|
||||||
|
0.0,
|
||||||
|
velocityMagnitude,
|
||||||
|
);
|
||||||
|
|
||||||
|
_flingDirection = velocityMagnitude > 0
|
||||||
|
? velocity.pixelsPerSecond / velocityMagnitude
|
||||||
|
: Offset.zero;
|
||||||
|
|
||||||
|
_flingStartTime = null;
|
||||||
|
_lastFlingElapsedSeconds = 0.0;
|
||||||
|
_scheduleFlingFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scheduleFlingFrame() {
|
||||||
|
_flingFrameCallbackId = SchedulerBinding.instance.scheduleFrameCallback(
|
||||||
|
_handleFlingFrame,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleFlingFrame(Duration timeStamp) {
|
||||||
|
if (_flingSimulation == null) return;
|
||||||
|
|
||||||
|
_flingStartTime ??= timeStamp;
|
||||||
|
final double elapsedSeconds =
|
||||||
|
(timeStamp - _flingStartTime!).inMicroseconds / 1e6;
|
||||||
|
|
||||||
|
final double distance = _flingSimulation!.x(elapsedSeconds);
|
||||||
|
final double prevDistance = _flingSimulation!.x(_lastFlingElapsedSeconds);
|
||||||
|
final double delta = distance - prevDistance;
|
||||||
|
_lastFlingElapsedSeconds = elapsedSeconds;
|
||||||
|
|
||||||
|
if ((prevDistance != 0.0 && delta.abs() < 0.1) ||
|
||||||
|
_flingSimulation!.isDone(elapsedSeconds)) {
|
||||||
|
_stopFling();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Offset movement = _flingDirection * delta;
|
||||||
|
_position = _clampPosition(_position + movement, _scale);
|
||||||
|
setState(() {});
|
||||||
|
|
||||||
|
if (_flingSimulation!.isDone(elapsedSeconds)) {
|
||||||
|
_stopFling();
|
||||||
|
} else {
|
||||||
|
_scheduleFlingFrame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stopFling() {
|
||||||
|
if (_flingFrameCallbackId != null) {
|
||||||
|
SchedulerBinding.instance.cancelFrameCallbackWithId(
|
||||||
|
_flingFrameCallbackId!,
|
||||||
|
);
|
||||||
|
_flingFrameCallbackId = null;
|
||||||
|
}
|
||||||
|
_flingStartTime = null;
|
||||||
|
_lastFlingElapsedSeconds = 0.0;
|
||||||
|
_flingSimulation = null;
|
||||||
|
}
|
||||||
|
|
||||||
/// ref [InteractiveViewer]
|
/// ref [InteractiveViewer]
|
||||||
void _onScaleEnd(ScaleEndDetails details) {
|
void _onScaleEnd(ScaleEndDetails details) {
|
||||||
switch (_gestureType) {
|
switch (_gestureType) {
|
||||||
case _GestureType.pan:
|
case _GestureType.pan:
|
||||||
if (details.velocity.pixelsPerSecond.distance < kMinFlingVelocity) {
|
final double velocityMagnitude =
|
||||||
return;
|
details.velocity.pixelsPerSecond.distance;
|
||||||
|
if (velocityMagnitude >= 200.0) {
|
||||||
|
_startFling(details.velocity);
|
||||||
}
|
}
|
||||||
final drag = _interactionEndFrictionCoefficient;
|
|
||||||
final FrictionSimulation frictionSimulationX = FrictionSimulation(
|
|
||||||
drag,
|
|
||||||
_position.dx,
|
|
||||||
details.velocity.pixelsPerSecond.dx,
|
|
||||||
);
|
|
||||||
final FrictionSimulation frictionSimulationY = FrictionSimulation(
|
|
||||||
drag,
|
|
||||||
_position.dy,
|
|
||||||
details.velocity.pixelsPerSecond.dy,
|
|
||||||
);
|
|
||||||
final double tFinal = _getFinalTime(
|
|
||||||
details.velocity.pixelsPerSecond.distance,
|
|
||||||
drag,
|
|
||||||
);
|
|
||||||
final position = _clampPosition(
|
|
||||||
Offset(frictionSimulationX.finalX, frictionSimulationY.finalX),
|
|
||||||
_scale,
|
|
||||||
);
|
|
||||||
|
|
||||||
_scaleFrom = _scaleTo = _scale;
|
|
||||||
_positionFrom = _position;
|
|
||||||
_positionTo = position;
|
|
||||||
|
|
||||||
_animationController
|
|
||||||
..duration = Duration(milliseconds: (tFinal * 1000).round())
|
|
||||||
..forward(from: 0);
|
|
||||||
case _GestureType.scale:
|
case _GestureType.scale:
|
||||||
// if (details.scaleVelocity.abs() < 0.1) {
|
// if (details.scaleVelocity.abs() < 0.1) {
|
||||||
// return;
|
// return;
|
||||||
@@ -417,9 +486,10 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
|
|||||||
_doubleTapGestureRecognizer
|
_doubleTapGestureRecognizer
|
||||||
..onDoubleTapDown = _onDoubleTapDown
|
..onDoubleTapDown = _onDoubleTapDown
|
||||||
..onDoubleTap = _onDoubleTap;
|
..onDoubleTap = _onDoubleTap;
|
||||||
_horizontalDragGestureRecognizer
|
_horizontalDragGestureRecognizer.addPointer(
|
||||||
..isBoundaryAllowed = _isBoundaryAllowed
|
event,
|
||||||
..addPointer(event);
|
isPointerAllowed: _isAtEdge(event.localPosition),
|
||||||
|
);
|
||||||
_scaleGestureRecognizer.addPointer(event);
|
_scaleGestureRecognizer.addPointer(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,25 +497,28 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
|
|||||||
_scaleGestureRecognizer.addPointerPanZoom(event);
|
_scaleGestureRecognizer.addPointerPanZoom(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isBoundaryAllowed(Offset? initialPosition, OffsetPair lastPosition) {
|
bool _isAtEdge(Offset position) {
|
||||||
if (initialPosition == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (_scale <= widget.minScale) {
|
if (_scale <= widget.minScale) {
|
||||||
|
_horizontalDragGestureRecognizer.setAtBothEdges();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
final containerWidth = widget.containerSize.width;
|
final containerWidth = widget.containerSize.width;
|
||||||
final imageWidth = _imageSize.width * _scale;
|
final imageWidth = _imageSize.width * _scale;
|
||||||
if (imageWidth <= containerWidth) {
|
if (imageWidth <= containerWidth) {
|
||||||
|
_horizontalDragGestureRecognizer.setAtBothEdges();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
final dx = (1 - _scale) * containerWidth / 2;
|
final dx = (1 - _scale) * containerWidth / 2;
|
||||||
final dxOffset = (imageWidth - containerWidth) / 2;
|
final dxOffset = (imageWidth - containerWidth) / 2;
|
||||||
if (initialPosition.dx < lastPosition.global.dx) {
|
if (_position.dx.equals(dx + dxOffset, 1e-6)) {
|
||||||
return _position.dx.equals(dx + dxOffset, 1e-6);
|
_horizontalDragGestureRecognizer.isAtLeftEdge = true;
|
||||||
} else {
|
return true;
|
||||||
return _position.dx.equals(dx - dxOffset, 1e-6);
|
|
||||||
}
|
}
|
||||||
|
if (_position.dx.equals(dx - dxOffset, 1e-6)) {
|
||||||
|
_horizontalDragGestureRecognizer.isAtRightEdge = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onPointerSignal(PointerSignalEvent event) {
|
void _onPointerSignal(PointerSignalEvent event) {
|
||||||
@@ -471,11 +544,3 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum _GestureType { pan, scale, drag }
|
enum _GestureType { pan, scale, drag }
|
||||||
|
|
||||||
double _getFinalTime(
|
|
||||||
double velocity,
|
|
||||||
double drag, {
|
|
||||||
double effectivelyMotionless = 10,
|
|
||||||
}) {
|
|
||||||
return math.log(effectivelyMotionless / velocity) / math.log(drag / 100);
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user