жесты починил
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user