235 lines
5.9 KiB
Dart
235 lines
5.9 KiB
Dart
import 'dart:math' as math;
|
|
import 'dart:ui';
|
|
|
|
import 'package:flutter/cupertino.dart';
|
|
|
|
import 'directional_drag_recognizer.dart';
|
|
|
|
class SwipeRoute<T> extends PageRoute<T> {
|
|
SwipeRoute({
|
|
required this.builder,
|
|
super.settings,
|
|
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>(
|
|
BuildContext context,
|
|
WidgetBuilder builder, {
|
|
RouteSettings? settings,
|
|
}) {
|
|
return Navigator.of(
|
|
context,
|
|
).push<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();
|
|
}
|
|
}
|
|
}
|