жесты починил

This commit is contained in:
Jganenok
2026-05-28 12:52:15 +07:00
parent be928f8c1a
commit b97d474629
3 changed files with 337 additions and 49 deletions
@@ -0,0 +1,81 @@
import 'package:flutter/gestures.dart';
class RightwardDragRecognizer extends HorizontalDragGestureRecognizer {
RightwardDragRecognizer({super.debugOwner}) {
onlyAcceptDragOnThreshold = true;
}
static const double _kMinAcceptVelocity = 700.0;
static const double _kMinAcceptDistance = 20.0;
final Map<int, Offset> _initialPositions = {};
final Map<int, VelocityTracker> _velocityTrackers = {};
final Map<int, double> _currentDeltaX = {};
@override
void addAllowedPointer(PointerDownEvent event) {
_initialPositions[event.pointer] = event.position;
final tracker = VelocityTracker.withKind(event.kind);
tracker.addPosition(event.timeStamp, event.localPosition);
_velocityTrackers[event.pointer] = tracker;
super.addAllowedPointer(event);
}
@override
void handleEvent(PointerEvent event) {
if (event is PointerMoveEvent) {
_velocityTrackers[event.pointer]
?.addPosition(event.timeStamp, event.localPosition);
final initial = _initialPositions[event.pointer];
if (initial != null) {
final dx = event.position.dx - initial.dx;
_currentDeltaX[event.pointer] = dx;
if (dx < -kTouchSlop) {
stopTrackingPointer(event.pointer);
_cleanup(event.pointer);
return;
}
}
}
super.handleEvent(event);
}
@override
bool hasSufficientGlobalDistanceToAccept(
PointerDeviceKind pointerDeviceKind,
double? deviceTouchSlop,
) {
if (!super.hasSufficientGlobalDistanceToAccept(
pointerDeviceKind, deviceTouchSlop)) {
return false;
}
double maxDx = 0;
for (final dx in _currentDeltaX.values) {
if (dx > maxDx) maxDx = dx;
}
if (maxDx < _kMinAcceptDistance) return false;
for (final tracker in _velocityTrackers.values) {
final vx = tracker.getVelocity().pixelsPerSecond.dx;
if (vx >= _kMinAcceptVelocity) return true;
}
return false;
}
void _cleanup(int pointer) {
_initialPositions.remove(pointer);
_velocityTrackers.remove(pointer);
_currentDeltaX.remove(pointer);
}
@override
void didStopTrackingLastPointer(int pointer) {
_cleanup(pointer);
super.didStopTrackingLastPointer(pointer);
}
@override
void rejectGesture(int pointer) {
_cleanup(pointer);
super.rejectGesture(pointer);
}
}
+219 -3
View File
@@ -1,12 +1,101 @@
import 'dart:math' as math;
import 'dart:ui';
import 'package:flutter/cupertino.dart';
class SwipeRoute<T> extends CupertinoPageRoute<T> {
import 'rightward_drag_recognizer.dart';
class SwipeRoute<T> extends PageRoute<T> {
SwipeRoute({
required super.builder,
required this.builder,
super.settings,
super.maintainState,
super.fullscreenDialog,
this.maintainState = true,
});
final WidgetBuilder builder;
@override
final bool maintainState;
@override
Color? get barrierColor => null;
@override
String? get barrierLabel => null;
@override
Duration get transitionDuration => const Duration(milliseconds: 400);
@override
Duration get reverseTransitionDuration => const Duration(milliseconds: 400);
@override
bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {
return nextRoute is SwipeRoute || nextRoute is CupertinoRouteTransitionMixin;
}
@override
bool get popGestureInProgress => _gestureController != null;
_SwipeBackController<T>? _gestureController;
@override
bool get popGestureEnabled {
if (isFirst) return false;
if (willHandlePopInternally) return false;
if (popDisposition == RoutePopDisposition.doNotPop) return false;
if (animation?.status != AnimationStatus.completed) return false;
if (secondaryAnimation?.status != AnimationStatus.dismissed) return false;
if (popGestureInProgress) return false;
return true;
}
_SwipeBackController<T> _startPopGesture() {
final gesture = _SwipeBackController<T>(
navigator: navigator!,
controller: controller!,
);
_gestureController = gesture;
gesture._onEnd = () {
if (_gestureController == gesture) {
_gestureController = null;
}
};
return gesture;
}
@override
Widget buildPage(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return Semantics(
scopesRoute: true,
explicitChildNodes: true,
child: builder(context),
);
}
@override
Widget buildTransitions(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return _SwipeBackGestureDetector<T>(
enabledCallback: () => popGestureEnabled,
onStartPopGesture: _startPopGesture,
child: CupertinoPageTransition(
primaryRouteAnimation: animation,
secondaryRouteAnimation: secondaryAnimation,
linearTransition: popGestureInProgress,
child: child,
),
);
}
}
Future<T?> pushSwipeable<T>(
@@ -18,3 +107,130 @@ Future<T?> pushSwipeable<T>(
SwipeRoute<T>(builder: builder, settings: settings),
);
}
class _SwipeBackGestureDetector<T> extends StatefulWidget {
const _SwipeBackGestureDetector({
required this.enabledCallback,
required this.onStartPopGesture,
required this.child,
});
final ValueGetter<bool> enabledCallback;
final ValueGetter<_SwipeBackController<T>> onStartPopGesture;
final Widget child;
@override
State<_SwipeBackGestureDetector<T>> createState() =>
_SwipeBackGestureDetectorState<T>();
}
class _SwipeBackGestureDetectorState<T>
extends State<_SwipeBackGestureDetector<T>> {
_SwipeBackController<T>? _backController;
double _width = 0;
void _handleStart(DragStartDetails details) {
if (!widget.enabledCallback()) return;
_width = context.size?.width ?? MediaQuery.of(context).size.width;
if (_width <= 0) _width = 1.0;
_backController = widget.onStartPopGesture();
}
void _handleUpdate(DragUpdateDetails details) {
final delta = details.primaryDelta ?? 0.0;
_backController?.dragUpdate(delta / _width);
}
void _handleEnd(DragEndDetails details) {
final velocity = details.velocity.pixelsPerSecond.dx / _width;
_backController?.dragEnd(velocity);
_backController = null;
}
void _handleCancel() {
_backController?.dragEnd(0.0);
_backController = null;
}
@override
Widget build(BuildContext context) {
return RawGestureDetector(
behavior: HitTestBehavior.translucent,
gestures: <Type, GestureRecognizerFactory>{
RightwardDragRecognizer:
GestureRecognizerFactoryWithHandlers<RightwardDragRecognizer>(
() => RightwardDragRecognizer(debugOwner: this),
(instance) {
instance
..onStart = _handleStart
..onUpdate = _handleUpdate
..onEnd = _handleEnd
..onCancel = _handleCancel;
},
),
},
child: widget.child,
);
}
}
class _SwipeBackController<T> {
_SwipeBackController({
required this.navigator,
required this.controller,
});
final NavigatorState navigator;
final AnimationController controller;
VoidCallback? _onEnd;
static const double _kMinFlingVelocity = 1.0;
void dragUpdate(double delta) {
controller.value -= delta;
}
void dragEnd(double velocity) {
const animationCurve = Curves.fastLinearToSlowEaseIn;
final bool animateForward;
if (velocity.abs() >= _kMinFlingVelocity) {
animateForward = velocity <= 0;
} else {
animateForward = controller.value > 0.5;
}
if (animateForward) {
final forwardMs = math.min(
lerpDouble(800, 0, controller.value)!.floor(),
300,
);
controller.animateTo(
1.0,
duration: Duration(milliseconds: forwardMs),
curve: animationCurve,
);
} else {
navigator.pop();
if (controller.isAnimating) {
final backMs = lerpDouble(0, 800, controller.value)!.floor();
controller.animateBack(
0.0,
duration: Duration(milliseconds: backMs),
curve: animationCurve,
);
}
}
if (controller.isAnimating) {
late AnimationStatusListener statusCb;
statusCb = (status) {
_onEnd?.call();
controller.removeStatusListener(statusCb);
};
controller.addStatusListener(statusCb);
} else {
_onEnd?.call();
}
}
}
+37 -46
View File
@@ -1,22 +1,19 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'rightward_drag_recognizer.dart';
class SwipeToPop extends StatefulWidget {
final Widget child;
final double edgeWidth;
final double popThreshold;
final double velocityThreshold;
final bool fullWidth;
final bool enabled;
final VoidCallback? onPop;
const SwipeToPop({
super.key,
required this.child,
this.edgeWidth = 28,
this.popThreshold = 0.35,
this.velocityThreshold = 700,
this.fullWidth = false,
this.enabled = true,
this.onPop,
});
@@ -28,6 +25,7 @@ class SwipeToPop extends StatefulWidget {
class _SwipeToPopState extends State<SwipeToPop>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
double _width = 0;
@override
void initState() {
@@ -53,17 +51,19 @@ class _SwipeToPopState extends State<SwipeToPop>
}
void _onDragStart(DragStartDetails _) {
_width = context.size?.width ?? MediaQuery.of(context).size.width;
if (_width <= 0) _width = 1.0;
_controller.stop();
}
void _onDragUpdate(DragUpdateDetails d, double width) {
if (width <= 0) return;
final next = (_controller.value + d.delta.dx / width).clamp(0.0, 1.0);
void _onDragUpdate(DragUpdateDetails d) {
final next =
(_controller.value + (d.primaryDelta ?? 0.0) / _width).clamp(0.0, 1.0);
_controller.value = next;
}
Future<void> _onDragEnd(DragEndDetails d, double width) async {
final velocity = d.primaryVelocity ?? 0;
Future<void> _onDragEnd(DragEndDetails d) async {
final velocity = d.velocity.pixelsPerSecond.dx;
final pastThreshold = _controller.value > widget.popThreshold ||
velocity > widget.velocityThreshold;
if (pastThreshold) {
@@ -96,44 +96,35 @@ class _SwipeToPopState extends State<SwipeToPop>
return LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth;
final gestureChild = GestureDetector(
return RawGestureDetector(
behavior: HitTestBehavior.translucent,
dragStartBehavior: DragStartBehavior.down,
onHorizontalDragStart: _onDragStart,
onHorizontalDragUpdate: (d) => _onDragUpdate(d, width),
onHorizontalDragEnd: (d) => _onDragEnd(d, width),
onHorizontalDragCancel: _onDragCancel,
);
return Stack(
children: [
Positioned.fill(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
final t = _controller.value;
return Transform.translate(
offset: Offset(t * width, 0),
child: Opacity(
opacity: (1.0 - t * 0.35).clamp(0.0, 1.0),
child: child,
),
);
},
child: widget.child,
),
gestures: <Type, GestureRecognizerFactory>{
RightwardDragRecognizer:
GestureRecognizerFactoryWithHandlers<RightwardDragRecognizer>(
() => RightwardDragRecognizer(debugOwner: this),
(instance) {
instance
..onStart = _onDragStart
..onUpdate = _onDragUpdate
..onEnd = _onDragEnd
..onCancel = _onDragCancel;
},
),
if (widget.fullWidth)
Positioned.fill(child: gestureChild)
else
Positioned(
left: 0,
top: 0,
bottom: 0,
width: widget.edgeWidth,
child: gestureChild,
),
],
},
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
final t = _controller.value;
return Transform.translate(
offset: Offset(t * width, 0),
child: Opacity(
opacity: (1.0 - t * 0.35).clamp(0.0, 1.0),
child: child,
),
);
},
child: widget.child,
),
);
},
);