mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-04-20 03:06:59 +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:flutter/gestures.dart';
|
||||
|
||||
class CustomHorizontalDragGestureRecognizer
|
||||
extends HorizontalDragGestureRecognizer {
|
||||
CustomHorizontalDragGestureRecognizer({
|
||||
super.debugOwner,
|
||||
super.supportedDevices,
|
||||
super.allowedButtonsFilter,
|
||||
});
|
||||
|
||||
mixin InitialPositionMixin on GestureRecognizer {
|
||||
Offset? _initialPosition;
|
||||
Offset? get initialPosition => _initialPosition;
|
||||
|
||||
@override
|
||||
DeviceGestureSettings get gestureSettings => _gestureSettings;
|
||||
final _gestureSettings = DeviceGestureSettings(touchSlop: touchSlopH);
|
||||
|
||||
@override
|
||||
void addAllowedPointer(PointerDownEvent event) {
|
||||
super.addAllowedPointer(event);
|
||||
_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
|
||||
bool hasSufficientGlobalDistanceToAccept(
|
||||
@@ -41,7 +44,7 @@ double touchSlopH = Pref.touchSlopH;
|
||||
|
||||
bool _computeHitSlop(
|
||||
double globalDistanceMoved,
|
||||
DeviceGestureSettings? settings,
|
||||
DeviceGestureSettings settings,
|
||||
PointerDeviceKind kind,
|
||||
Offset? initialPosition,
|
||||
Offset lastPosition,
|
||||
@@ -53,10 +56,10 @@ bool _computeHitSlop(
|
||||
case PointerDeviceKind.invertedStylus:
|
||||
case PointerDeviceKind.unknown:
|
||||
case PointerDeviceKind.touch:
|
||||
return globalDistanceMoved > touchSlopH &&
|
||||
return globalDistanceMoved > settings.touchSlop! &&
|
||||
_calc(initialPosition!, lastPosition);
|
||||
case PointerDeviceKind.trackpad:
|
||||
return globalDistanceMoved > (settings?.touchSlop ?? kTouchSlop);
|
||||
return globalDistanceMoved > settings.touchSlop!;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart';
|
||||
import 'package:PiliPlus/utils/platform_utils.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
|
||||
mixin ImageGestureRecognizerMixin on GestureRecognizer {
|
||||
int? _pointer;
|
||||
|
||||
@override
|
||||
void addPointer(PointerDownEvent event) {
|
||||
void addPointer(PointerDownEvent event, {bool isPointerAllowed = true}) {
|
||||
if (_pointer == event.pointer) {
|
||||
return;
|
||||
}
|
||||
_pointer = event.pointer;
|
||||
super.addPointer(event);
|
||||
if (isPointerAllowed) {
|
||||
super.addPointer(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typedef IsBoundaryAllowed =
|
||||
bool Function(Offset? initialPosition, OffsetPair lastPosition);
|
||||
|
||||
class ImageHorizontalDragGestureRecognizer
|
||||
extends CustomHorizontalDragGestureRecognizer
|
||||
with ImageGestureRecognizerMixin {
|
||||
@@ -26,23 +26,62 @@ class ImageHorizontalDragGestureRecognizer
|
||||
super.allowedButtonsFilter,
|
||||
});
|
||||
|
||||
IsBoundaryAllowed? isBoundaryAllowed;
|
||||
static final double _touchSlop = PlatformUtils.isDesktop
|
||||
? kPrecisePointerHitSlop
|
||||
: 3.0;
|
||||
|
||||
@override
|
||||
bool hasSufficientGlobalDistanceToAccept(
|
||||
PointerDeviceKind pointerDeviceKind,
|
||||
double? deviceTouchSlop,
|
||||
) {
|
||||
return super.hasSufficientGlobalDistanceToAccept(
|
||||
pointerDeviceKind,
|
||||
deviceTouchSlop,
|
||||
) &&
|
||||
(isBoundaryAllowed?.call(initialPosition, lastPosition) ?? true);
|
||||
DeviceGestureSettings get gestureSettings => _gestureSettings;
|
||||
final _gestureSettings = DeviceGestureSettings(touchSlop: _touchSlop);
|
||||
|
||||
bool isAtLeftEdge = false;
|
||||
bool isAtRightEdge = false;
|
||||
|
||||
void setAtBothEdges() {
|
||||
isAtLeftEdge = isAtRightEdge = 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
|
||||
void dispose() {
|
||||
isBoundaryAllowed = null;
|
||||
super.dispose();
|
||||
void handleEvent(PointerEvent event) {
|
||||
if (!_hasAcceptedOrChecked &&
|
||||
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/material.dart';
|
||||
import 'package:flutter/physics.dart' show FrictionSimulation;
|
||||
import 'package:flutter/scheduler.dart' show SchedulerBinding;
|
||||
import 'package:flutter/services.dart' show HardwareKeyboard;
|
||||
|
||||
///
|
||||
@@ -69,7 +70,6 @@ class Viewer extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
|
||||
double get _interactionEndFrictionCoefficient => 0.0001 * _scale; // 0.0000135
|
||||
static const double _scaleFactor = kDefaultMouseScrollToScaleFactor;
|
||||
|
||||
_GestureType? _gestureType;
|
||||
@@ -171,6 +171,7 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_stopFling();
|
||||
_animationController
|
||||
..removeListener(_listener)
|
||||
..dispose();
|
||||
@@ -265,6 +266,8 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
|
||||
}
|
||||
|
||||
void _onScaleStart(ScaleStartDetails details) {
|
||||
_stopFling();
|
||||
|
||||
if (_animationController.isAnimating) {
|
||||
_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]
|
||||
void _onScaleEnd(ScaleEndDetails details) {
|
||||
switch (_gestureType) {
|
||||
case _GestureType.pan:
|
||||
if (details.velocity.pixelsPerSecond.distance < kMinFlingVelocity) {
|
||||
return;
|
||||
final double velocityMagnitude =
|
||||
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:
|
||||
// if (details.scaleVelocity.abs() < 0.1) {
|
||||
// return;
|
||||
@@ -417,9 +486,10 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
|
||||
_doubleTapGestureRecognizer
|
||||
..onDoubleTapDown = _onDoubleTapDown
|
||||
..onDoubleTap = _onDoubleTap;
|
||||
_horizontalDragGestureRecognizer
|
||||
..isBoundaryAllowed = _isBoundaryAllowed
|
||||
..addPointer(event);
|
||||
_horizontalDragGestureRecognizer.addPointer(
|
||||
event,
|
||||
isPointerAllowed: _isAtEdge(event.localPosition),
|
||||
);
|
||||
_scaleGestureRecognizer.addPointer(event);
|
||||
}
|
||||
|
||||
@@ -427,25 +497,28 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
|
||||
_scaleGestureRecognizer.addPointerPanZoom(event);
|
||||
}
|
||||
|
||||
bool _isBoundaryAllowed(Offset? initialPosition, OffsetPair lastPosition) {
|
||||
if (initialPosition == null) {
|
||||
return true;
|
||||
}
|
||||
bool _isAtEdge(Offset position) {
|
||||
if (_scale <= widget.minScale) {
|
||||
_horizontalDragGestureRecognizer.setAtBothEdges();
|
||||
return true;
|
||||
}
|
||||
final containerWidth = widget.containerSize.width;
|
||||
final imageWidth = _imageSize.width * _scale;
|
||||
if (imageWidth <= containerWidth) {
|
||||
_horizontalDragGestureRecognizer.setAtBothEdges();
|
||||
return true;
|
||||
}
|
||||
final dx = (1 - _scale) * containerWidth / 2;
|
||||
final dxOffset = (imageWidth - containerWidth) / 2;
|
||||
if (initialPosition.dx < lastPosition.global.dx) {
|
||||
return _position.dx.equals(dx + dxOffset, 1e-6);
|
||||
} else {
|
||||
return _position.dx.equals(dx - dxOffset, 1e-6);
|
||||
if (_position.dx.equals(dx + dxOffset, 1e-6)) {
|
||||
_horizontalDragGestureRecognizer.isAtLeftEdge = true;
|
||||
return true;
|
||||
}
|
||||
if (_position.dx.equals(dx - dxOffset, 1e-6)) {
|
||||
_horizontalDragGestureRecognizer.isAtRightEdge = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _onPointerSignal(PointerSignalEvent event) {
|
||||
@@ -471,11 +544,3 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
|
||||
}
|
||||
|
||||
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