import 'dart:math'; import 'dart:ui'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:venera/components/components.dart'; const double _kBackGestureWidth = 20.0; const int _kMaxDroppedSwipePageForwardAnimationTime = 800; const int _kMaxPageBackAnimationTime = 300; const double _kMinFlingVelocity = 1.0; class AppPageRoute extends PageRoute with _AppRouteTransitionMixin{ /// Construct a MaterialPageRoute whose contents are defined by [builder]. AppPageRoute({ required this.builder, super.settings, this.maintainState = true, super.fullscreenDialog, super.allowSnapshotting = true, super.barrierDismissible = false, this.enableIOSGesture = true, this.preventRebuild = true, this.isRootRoute = false, }) { assert(opaque); } /// Builds the primary contents of the route. final WidgetBuilder builder; String? label; @override toString() => "/$label"; @override Widget buildContent(BuildContext context) { var widget = builder(context); if(widget is NaviPaddingWidget) { label = widget.child.runtimeType.toString(); } else { label = widget.runtimeType.toString(); } return widget; } @override final bool maintainState; @override String get debugLabel => '${super.debugLabel}(${settings.name})'; @override final bool enableIOSGesture; @override final bool preventRebuild; @override final bool isRootRoute; } mixin _AppRouteTransitionMixin on PageRoute { /// Builds the primary contents of the route. @protected Widget buildContent(BuildContext context); @override Duration get transitionDuration => const Duration(milliseconds: 300); @override Color? get barrierColor => null; @override String? get barrierLabel => null; @override bool canTransitionTo(TransitionRoute nextRoute) { // Don't perform outgoing animation if the next route is a fullscreen dialog. return nextRoute is PageRoute && !nextRoute.fullscreenDialog; } bool get enableIOSGesture; bool get preventRebuild; bool get isRootRoute; Widget? _child; @override Widget buildPage( BuildContext context, Animation animation, Animation secondaryAnimation, ) { Widget result; if(preventRebuild){ result = _child ?? (_child = buildContent(context)); } else { result = buildContent(context); } return Semantics( scopesRoute: true, explicitChildNodes: true, child: result, ); } static bool _isPopGestureEnabled(PageRoute route) { if (route.isFirst || route.willHandlePopInternally || route.popDisposition == RoutePopDisposition.doNotPop || route.fullscreenDialog || route.animation!.status != AnimationStatus.completed || route.secondaryAnimation!.status != AnimationStatus.dismissed || route.navigator!.userGestureInProgress) { return false; } return true; } @override Widget buildTransitions(BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { if(isRootRoute) { return FadeTransition( opacity: Tween(begin: 0, end: 1.0).animate(CurvedAnimation( parent: animation, curve: Curves.ease )), child: FadeTransition( opacity: Tween(begin: 1.0, end: 0).animate(CurvedAnimation( parent: secondaryAnimation, curve: Curves.ease )), child: child, ), ); } return SlidePageTransitionBuilder().buildTransitions( this, context, animation, secondaryAnimation, enableIOSGesture ? IOSBackGestureDetector( gestureWidth: _kBackGestureWidth, enabledCallback: () => _isPopGestureEnabled(this), onStartPopGesture: () => _startPopGesture(this), child: child) : child); } IOSBackGestureController _startPopGesture(PageRoute route) { return IOSBackGestureController(route.controller!, route.navigator!); } } class IOSBackGestureController { final AnimationController controller; final NavigatorState navigator; IOSBackGestureController(this.controller, this.navigator) { navigator.didStartUserGesture(); } void dragEnd(double velocity) { const Curve animationCurve = Curves.fastLinearToSlowEaseIn; final bool animateForward; if (velocity.abs() >= _kMinFlingVelocity) { animateForward = velocity <= 0; } else { animateForward = controller.value > 0.5; } if (animateForward) { final droppedPageForwardAnimationTime = min( lerpDouble( _kMaxDroppedSwipePageForwardAnimationTime, 0, controller.value)! .floor(), _kMaxPageBackAnimationTime, ); controller.animateTo(1.0, duration: Duration(milliseconds: droppedPageForwardAnimationTime), curve: animationCurve); } else { navigator.pop(); if (controller.isAnimating) { final droppedPageBackAnimationTime = lerpDouble( 0, _kMaxDroppedSwipePageForwardAnimationTime, controller.value)! .floor(); controller.animateBack(0.0, duration: Duration(milliseconds: droppedPageBackAnimationTime), curve: animationCurve); } } if (controller.isAnimating) { late AnimationStatusListener animationStatusCallback; animationStatusCallback = (status) { navigator.didStopUserGesture(); controller.removeStatusListener(animationStatusCallback); }; controller.addStatusListener(animationStatusCallback); } else { navigator.didStopUserGesture(); } } void dragUpdate(double delta) { controller.value -= delta; } } class IOSBackGestureDetector extends StatefulWidget { const IOSBackGestureDetector( {required this.enabledCallback, required this.child, required this.gestureWidth, required this.onStartPopGesture, super.key}); final double gestureWidth; final bool Function() enabledCallback; final IOSBackGestureController Function() onStartPopGesture; final Widget child; @override State createState() => _IOSBackGestureDetectorState(); } class _IOSBackGestureDetectorState extends State { IOSBackGestureController? _backGestureController; late HorizontalDragGestureRecognizer _recognizer; @override void dispose() { _recognizer.dispose(); super.dispose(); } @override void initState() { super.initState(); _recognizer = HorizontalDragGestureRecognizer(debugOwner: this) ..onStart = _handleDragStart ..onUpdate = _handleDragUpdate ..onEnd = _handleDragEnd ..onCancel = _handleDragCancel; } @override Widget build(BuildContext context) { var dragAreaWidth = Directionality.of(context) == TextDirection.ltr ? MediaQuery.of(context).padding.left : MediaQuery.of(context).padding.right; dragAreaWidth = max(dragAreaWidth, widget.gestureWidth); return Stack( fit: StackFit.passthrough, children: [ widget.child, Positioned( width: dragAreaWidth, top: 0.0, bottom: 0.0, left: 0, child: Listener( onPointerDown: _handlePointerDown, behavior: HitTestBehavior.translucent, ), ), ], ); } void _handlePointerDown(PointerDownEvent event) { if (widget.enabledCallback()) _recognizer.addPointer(event); } void _handleDragCancel() { assert(mounted); _backGestureController?.dragEnd(0.0); _backGestureController = null; } double _convertToLogical(double value) { switch (Directionality.of(context)) { case TextDirection.rtl: return -value; case TextDirection.ltr: return value; } } void _handleDragEnd(DragEndDetails details) { assert(mounted); assert(_backGestureController != null); _backGestureController!.dragEnd(_convertToLogical( details.velocity.pixelsPerSecond.dx / context.size!.width)); _backGestureController = null; } void _handleDragStart(DragStartDetails details) { assert(mounted); assert(_backGestureController == null); _backGestureController = widget.onStartPopGesture(); } void _handleDragUpdate(DragUpdateDetails details) { assert(mounted); assert(_backGestureController != null); _backGestureController!.dragUpdate( _convertToLogical(details.primaryDelta! / context.size!.width)); } } class SlidePageTransitionBuilder extends PageTransitionsBuilder { @override Widget buildTransitions( PageRoute route, BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { return SlideTransition( position: Tween( begin: const Offset(1, 0), end: Offset.zero, ).animate(CurvedAnimation( parent: animation, curve: Curves.ease, )), child: SlideTransition( position: Tween( begin: Offset.zero, end: const Offset(-0.4, 0), ).animate(CurvedAnimation( parent: secondaryAnimation, curve: Curves.ease, )), child: PhysicalModel( color: Colors.transparent, borderRadius: BorderRadius.zero, clipBehavior: Clip.hardEdge, elevation: 6, child: Material(child: child,), ), ) ); } }