Jonny Borges

add snackbars tests and fix bugs

@@ -21,6 +21,12 @@ class HomeView extends GetView<HomeController> { @@ -21,6 +21,12 @@ class HomeView extends GetView<HomeController> {
21 child: Scaffold( 21 child: Scaffold(
22 backgroundColor: Colors.transparent, 22 backgroundColor: Colors.transparent,
23 appBar: AppBar( 23 appBar: AppBar(
  24 + leading: IconButton(
  25 + icon: Icon(Icons.add),
  26 + onPressed: () {
  27 + Get.snackbar('title', 'message');
  28 + },
  29 + ),
24 title: Text('covid'.tr), 30 title: Text('covid'.tr),
25 backgroundColor: Colors.white10, 31 backgroundColor: Colors.white10,
26 elevation: 0, 32 elevation: 0,
1 import 'dart:ui' as ui; 1 import 'dart:ui' as ui;
2 2
  3 +import 'package:flutter/cupertino.dart';
3 import 'package:flutter/material.dart'; 4 import 'package:flutter/material.dart';
4 import 'package:flutter/scheduler.dart'; 5 import 'package:flutter/scheduler.dart';
5 6
@@ -276,7 +277,7 @@ extension ExtensionDialog on GetInterface { @@ -276,7 +277,7 @@ extension ExtensionDialog on GetInterface {
276 } 277 }
277 278
278 extension ExtensionSnackbar on GetInterface { 279 extension ExtensionSnackbar on GetInterface {
279 - void rawSnackbar({ 280 + SnackbarController rawSnackbar({
280 String? title, 281 String? title,
281 String? message, 282 String? message,
282 Widget? titleText, 283 Widget? titleText,
@@ -296,7 +297,7 @@ extension ExtensionSnackbar on GetInterface { @@ -296,7 +297,7 @@ extension ExtensionSnackbar on GetInterface {
296 Gradient? backgroundGradient, 297 Gradient? backgroundGradient,
297 Widget? mainButton, 298 Widget? mainButton,
298 OnTap? onTap, 299 OnTap? onTap,
299 - Duration duration = const Duration(seconds: 3), 300 + Duration? duration = const Duration(seconds: 3),
300 bool isDismissible = true, 301 bool isDismissible = true,
301 DismissDirection? dismissDirection, 302 DismissDirection? dismissDirection,
302 bool showProgressIndicator = false, 303 bool showProgressIndicator = false,
@@ -309,12 +310,12 @@ extension ExtensionSnackbar on GetInterface { @@ -309,12 +310,12 @@ extension ExtensionSnackbar on GetInterface {
309 Curve reverseAnimationCurve = Curves.easeOutCirc, 310 Curve reverseAnimationCurve = Curves.easeOutCirc,
310 Duration animationDuration = const Duration(seconds: 1), 311 Duration animationDuration = const Duration(seconds: 1),
311 SnackbarStatusCallback? snackbarStatus, 312 SnackbarStatusCallback? snackbarStatus,
312 - double? barBlur = 0.0, 313 + double barBlur = 0.0,
313 double overlayBlur = 0.0, 314 double overlayBlur = 0.0,
314 Color? overlayColor, 315 Color? overlayColor,
315 Form? userInputForm, 316 Form? userInputForm,
316 - }) async {  
317 - final getBar = GetSnackBar( 317 + }) {
  318 + final getSnackBar = GetSnackBar(
318 snackbarStatus: snackbarStatus, 319 snackbarStatus: snackbarStatus,
319 title: title, 320 title: title,
320 message: message, 321 message: message,
@@ -352,13 +353,16 @@ extension ExtensionSnackbar on GetInterface { @@ -352,13 +353,16 @@ extension ExtensionSnackbar on GetInterface {
352 userInputForm: userInputForm, 353 userInputForm: userInputForm,
353 ); 354 );
354 355
  356 + final controller = SnackbarController(getSnackBar);
  357 +
355 if (instantInit) { 358 if (instantInit) {
356 - getBar.show(); 359 + controller.show();
357 } else { 360 } else {
358 SchedulerBinding.instance!.addPostFrameCallback((_) { 361 SchedulerBinding.instance!.addPostFrameCallback((_) {
359 - getBar.show(); 362 + controller.show();
360 }); 363 });
361 } 364 }
  365 + return controller;
362 } 366 }
363 367
364 SnackbarController showSnackbar(GetSnackBar snackbar) { 368 SnackbarController showSnackbar(GetSnackBar snackbar) {
@@ -371,7 +375,7 @@ extension ExtensionSnackbar on GetInterface { @@ -371,7 +375,7 @@ extension ExtensionSnackbar on GetInterface {
371 String title, 375 String title,
372 String message, { 376 String message, {
373 Color? colorText, 377 Color? colorText,
374 - Duration? duration, 378 + Duration? duration = const Duration(seconds: 3),
375 379
376 /// with instantInit = false you can put snackbar on initState 380 /// with instantInit = false you can put snackbar on initState
377 bool instantInit = true, 381 bool instantInit = true,
@@ -431,7 +435,7 @@ extension ExtensionSnackbar on GetInterface { @@ -431,7 +435,7 @@ extension ExtensionSnackbar on GetInterface {
431 snackPosition: snackPosition ?? SnackPosition.TOP, 435 snackPosition: snackPosition ?? SnackPosition.TOP,
432 borderRadius: borderRadius ?? 15, 436 borderRadius: borderRadius ?? 15,
433 margin: margin ?? EdgeInsets.symmetric(horizontal: 10), 437 margin: margin ?? EdgeInsets.symmetric(horizontal: 10),
434 - duration: duration ?? Duration(seconds: 3), 438 + duration: duration,
435 barBlur: barBlur ?? 7.0, 439 barBlur: barBlur ?? 7.0,
436 backgroundColor: backgroundColor ?? Colors.grey.withOpacity(0.2), 440 backgroundColor: backgroundColor ?? Colors.grey.withOpacity(0.2),
437 icon: icon, 441 icon: icon,
@@ -517,6 +521,7 @@ extension GetNavigation on GetInterface { @@ -517,6 +521,7 @@ extension GetNavigation on GetInterface {
517 routeName ??= "/${page.runtimeType}"; 521 routeName ??= "/${page.runtimeType}";
518 routeName = _cleanRouteName(routeName); 522 routeName = _cleanRouteName(routeName);
519 if (preventDuplicates && routeName == currentRoute) { 523 if (preventDuplicates && routeName == currentRoute) {
  524 + CupertinoPageRoute ds;
520 return null; 525 return null;
521 } 526 }
522 return global(id).currentState?.push<T>( 527 return global(id).currentState?.push<T>(
@@ -5,111 +5,247 @@ import 'package:flutter/cupertino.dart'; @@ -5,111 +5,247 @@ import 'package:flutter/cupertino.dart';
5 import 'package:flutter/foundation.dart'; 5 import 'package:flutter/foundation.dart';
6 import 'package:flutter/gestures.dart'; 6 import 'package:flutter/gestures.dart';
7 import 'package:flutter/material.dart'; 7 import 'package:flutter/material.dart';
8 -import '../../../get.dart';  
9 8
  9 +import '../../../get.dart';
10 import 'default_transitions.dart'; 10 import 'default_transitions.dart';
11 import 'transitions_type.dart'; 11 import 'transitions_type.dart';
12 12
13 const double _kBackGestureWidth = 20.0; 13 const double _kBackGestureWidth = 20.0;
14 -const double _kMinFlingVelocity = 1.0; // Screen widths per second. 14 +const int _kMaxDroppedSwipePageForwardAnimationTime =
  15 + 800; // Screen widths per second.
15 16
16 // An eyeballed value for the maximum time it takes 17 // An eyeballed value for the maximum time it takes
17 //for a page to animate forward 18 //for a page to animate forward
18 // if the user releases a page mid swipe. 19 // if the user releases a page mid swipe.
19 -const int _kMaxDroppedSwipePageForwardAnimationTime = 800; // Milliseconds. 20 +const int _kMaxPageBackAnimationTime = 300; // Milliseconds.
20 21
21 // The maximum time for a page to get reset to it's original position if the 22 // The maximum time for a page to get reset to it's original position if the
22 // user releases a page mid swipe. 23 // user releases a page mid swipe.
23 -const int _kMaxPageBackAnimationTime = 300; // Milliseconds. 24 +const double _kMinFlingVelocity = 1.0; // Milliseconds.
24 25
25 -mixin GetPageRouteTransitionMixin<T> on PageRoute<T> {  
26 - /// Builds the primary contents of the route.  
27 - @protected  
28 - Widget buildContent(BuildContext context); 26 +class CupertinoBackGestureController<T> {
  27 + final AnimationController controller;
29 28
30 - /// {@template flutter.cupertino.CupertinoRouteTransitionMixin.title}  
31 - /// A title string for this route. 29 + final NavigatorState navigator;
  30 +
  31 + /// Creates a controller for an iOS-style back gesture.
32 /// 32 ///
33 - /// Used to auto-populate [CupertinoNavigationBar] and  
34 - /// [CupertinoSliverNavigationBar]'s `middle`/`largeTitle` widgets when  
35 - /// one is not manually supplied.  
36 - /// {@endtemplate}  
37 - String? get title; 33 + /// The [navigator] and [controller] arguments must not be null.
  34 + CupertinoBackGestureController({
  35 + required this.navigator,
  36 + required this.controller,
  37 + }) {
  38 + navigator.didStartUserGesture();
  39 + }
38 40
39 - double Function(BuildContext context)? get gestureWidth; 41 + /// The drag gesture has ended with a horizontal motion of
  42 + /// [fractionalVelocity] as a fraction of screen width per second.
  43 + void dragEnd(double velocity) {
  44 + // Fling in the appropriate direction.
  45 + // AnimationController.fling is guaranteed to
  46 + // take at least one frame.
  47 + //
  48 + // This curve has been determined through rigorously eyeballing native iOS
  49 + // animations.
  50 + const Curve animationCurve = Curves.fastLinearToSlowEaseIn;
  51 + final bool animateForward;
40 52
41 - ValueNotifier<String?>? _previousTitle; 53 + // If the user releases the page before mid screen with sufficient velocity,
  54 + // or after mid screen, we should animate the page out. Otherwise, the page
  55 + // should be animated back in.
  56 + if (velocity.abs() >= _kMinFlingVelocity) {
  57 + animateForward = velocity <= 0;
  58 + } else {
  59 + animateForward = controller.value > 0.5;
  60 + }
42 61
43 - /// The title string of the previous [CupertinoPageRoute].  
44 - ///  
45 - /// The [ValueListenable]'s value is readable after the route is installed  
46 - /// onto a [Navigator]. The [ValueListenable] will also notify its listeners  
47 - /// if the value changes (such as by replacing the previous route).  
48 - ///  
49 - /// The [ValueListenable] itself will be null before the route is installed.  
50 - /// Its content value will be null if the previous route has no title or  
51 - /// is not a [CupertinoPageRoute].  
52 - ///  
53 - /// See also:  
54 - ///  
55 - /// * [ValueListenableBuilder], which can be used to listen and rebuild  
56 - /// widgets based on a ValueListenable.  
57 - ValueListenable<String?> get previousTitle {  
58 - assert(  
59 - _previousTitle != null,  
60 - '''  
61 -Cannot read the previousTitle for a route that has not yet been installed''', 62 + if (animateForward) {
  63 + // The closer the panel is to dismissing, the shorter the animation is.
  64 + // We want to cap the animation time, but we want to use a linear curve
  65 + // to determine it.
  66 + final droppedPageForwardAnimationTime = min(
  67 + lerpDouble(
  68 + _kMaxDroppedSwipePageForwardAnimationTime, 0, controller.value)!
  69 + .floor(),
  70 + _kMaxPageBackAnimationTime,
62 ); 71 );
63 - return _previousTitle!; 72 + controller.animateTo(1.0,
  73 + duration: Duration(milliseconds: droppedPageForwardAnimationTime),
  74 + curve: animationCurve);
  75 + } else {
  76 + // This route is destined to pop at this point. Reuse navigator's pop.
  77 + navigator.pop();
  78 +
  79 + // The popping may have finished inline if already at the
  80 + // target destination.
  81 + if (controller.isAnimating) {
  82 + // Otherwise, use a custom popping animation duration and curve.
  83 + final droppedPageBackAnimationTime = lerpDouble(
  84 + 0, _kMaxDroppedSwipePageForwardAnimationTime, controller.value)!
  85 + .floor();
  86 + controller.animateBack(0.0,
  87 + duration: Duration(milliseconds: droppedPageBackAnimationTime),
  88 + curve: animationCurve);
  89 + }
64 } 90 }
65 91
66 - @override  
67 - void didChangePrevious(Route<dynamic>? previousRoute) {  
68 - final previousTitleString = previousRoute is CupertinoRouteTransitionMixin  
69 - ? previousRoute.title  
70 - : null;  
71 - if (_previousTitle == null) {  
72 - _previousTitle = ValueNotifier<String?>(previousTitleString); 92 + if (controller.isAnimating) {
  93 + // Keep the userGestureInProgress in true state so we don't change the
  94 + // curve of the page transition mid-flight since CupertinoPageTransition
  95 + // depends on userGestureInProgress.
  96 + late AnimationStatusListener animationStatusCallback;
  97 + animationStatusCallback = (status) {
  98 + navigator.didStopUserGesture();
  99 + controller.removeStatusListener(animationStatusCallback);
  100 + };
  101 + controller.addStatusListener(animationStatusCallback);
73 } else { 102 } else {
74 - _previousTitle!.value = previousTitleString; 103 + navigator.didStopUserGesture();
75 } 104 }
76 - super.didChangePrevious(previousRoute);  
77 } 105 }
78 106
79 - @override  
80 - // A relatively rigorous eyeball estimation.  
81 - Duration get transitionDuration => const Duration(milliseconds: 400); 107 + /// The drag gesture has changed by [fractionalDelta]. The total range of the
  108 + /// drag should be 0.0 to 1.0.
  109 + void dragUpdate(double delta) {
  110 + controller.value -= delta;
  111 + }
  112 +}
  113 +
  114 +class CupertinoBackGestureDetector<T> extends StatefulWidget {
  115 + final Widget child;
  116 +
  117 + final double gestureWidth;
  118 + final ValueGetter<bool> enabledCallback;
  119 +
  120 + final ValueGetter<CupertinoBackGestureController<T>> onStartPopGesture;
  121 +
  122 + const CupertinoBackGestureDetector({
  123 + Key? key,
  124 + required this.enabledCallback,
  125 + required this.onStartPopGesture,
  126 + required this.child,
  127 + required this.gestureWidth,
  128 + }) : super(key: key);
82 129
83 @override 130 @override
84 - Color? get barrierColor => null; 131 + CupertinoBackGestureDetectorState<T> createState() =>
  132 + CupertinoBackGestureDetectorState<T>();
  133 +}
  134 +
  135 +class CupertinoBackGestureDetectorState<T>
  136 + extends State<CupertinoBackGestureDetector<T>> {
  137 + CupertinoBackGestureController<T>? _backGestureController;
  138 +
  139 + late HorizontalDragGestureRecognizer _recognizer;
85 140
86 @override 141 @override
87 - String? get barrierLabel => null; 142 + Widget build(BuildContext context) {
  143 + assert(debugCheckHasDirectionality(context));
  144 + // For devices with notches, the drag area needs to be larger on the side
  145 + // that has the notch.
  146 + var dragAreaWidth = Directionality.of(context) == TextDirection.ltr
  147 + ? MediaQuery.of(context).padding.left
  148 + : MediaQuery.of(context).padding.right;
  149 + dragAreaWidth = max(dragAreaWidth, widget.gestureWidth);
  150 + return Stack(
  151 + fit: StackFit.passthrough,
  152 + children: <Widget>[
  153 + widget.child,
  154 + PositionedDirectional(
  155 + start: 0.0,
  156 + width: dragAreaWidth,
  157 + top: 0.0,
  158 + bottom: 0.0,
  159 + child: Listener(
  160 + onPointerDown: _handlePointerDown,
  161 + behavior: HitTestBehavior.translucent,
  162 + ),
  163 + ),
  164 + ],
  165 + );
  166 + }
88 167
89 - bool get showCupertinoParallax; 168 + @override
  169 + void dispose() {
  170 + _recognizer.dispose();
  171 + super.dispose();
  172 + }
90 173
91 @override 174 @override
92 - bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {  
93 - // Don't perform outgoing animation if the next route is a  
94 - // fullscreen dialog. 175 + void initState() {
  176 + super.initState();
  177 + _recognizer = HorizontalDragGestureRecognizer(debugOwner: this)
  178 + ..onStart = _handleDragStart
  179 + ..onUpdate = _handleDragUpdate
  180 + ..onEnd = _handleDragEnd
  181 + ..onCancel = _handleDragCancel;
  182 + }
95 183
96 - return nextRoute is GetPageRouteTransitionMixin &&  
97 - !nextRoute.fullscreenDialog &&  
98 - nextRoute.showCupertinoParallax; 184 + double _convertToLogical(double value) {
  185 + switch (Directionality.of(context)) {
  186 + case TextDirection.rtl:
  187 + return -value;
  188 + case TextDirection.ltr:
  189 + return value;
  190 + }
99 } 191 }
100 192
101 - /// True if an iOS-style back swipe pop gesture is currently  
102 - /// underway for [route]. 193 + void _handleDragCancel() {
  194 + assert(mounted);
  195 + // This can be called even if start is not called, paired with
  196 + // the "down" event
  197 + // that we don't consider here.
  198 + _backGestureController?.dragEnd(0.0);
  199 + _backGestureController = null;
  200 + }
  201 +
  202 + void _handleDragEnd(DragEndDetails details) {
  203 + assert(mounted);
  204 + assert(_backGestureController != null);
  205 + _backGestureController!.dragEnd(_convertToLogical(
  206 + details.velocity.pixelsPerSecond.dx / context.size!.width));
  207 + _backGestureController = null;
  208 + }
  209 +
  210 + void _handleDragStart(DragStartDetails details) {
  211 + assert(mounted);
  212 + assert(_backGestureController == null);
  213 + _backGestureController = widget.onStartPopGesture();
  214 + }
  215 +
  216 + void _handleDragUpdate(DragUpdateDetails details) {
  217 + assert(mounted);
  218 + assert(_backGestureController != null);
  219 + _backGestureController!.dragUpdate(
  220 + _convertToLogical(details.primaryDelta! / context.size!.width));
  221 + }
  222 +
  223 + void _handlePointerDown(PointerDownEvent event) {
  224 + if (widget.enabledCallback()) _recognizer.addPointer(event);
  225 + }
  226 +}
  227 +
  228 +mixin GetPageRouteTransitionMixin<T> on PageRoute<T> {
  229 + ValueNotifier<String?>? _previousTitle;
  230 +
  231 + @override
  232 + Color? get barrierColor => null;
  233 +
  234 + @override
  235 + String? get barrierLabel => null;
  236 +
  237 + double Function(BuildContext context)? get gestureWidth;
  238 +
  239 + /// Whether a pop gesture can be started by the user.
103 /// 240 ///
104 - /// This just check the route's [NavigatorState.userGestureInProgress]. 241 + /// Returns true if the user can edge-swipe to a previous route.
105 /// 242 ///
106 - /// See also: 243 + /// Returns false once [isPopGestureInProgress] is true, but
  244 + /// [isPopGestureInProgress] can only become true if [popGestureEnabled] was
  245 + /// true first.
107 /// 246 ///
108 - /// * [popGestureEnabled], which returns true if a user-triggered pop gesture  
109 - /// would be allowed.  
110 - static bool isPopGestureInProgress(PageRoute<dynamic> route) {  
111 - return route.navigator!.userGestureInProgress;  
112 - } 247 + /// This should only be used between frames, not during build.
  248 + bool get popGestureEnabled => _isPopGestureEnabled(this);
113 249
114 /// True if an iOS-style back swipe pop gesture is currently 250 /// True if an iOS-style back swipe pop gesture is currently
115 /// underway for this route. 251 /// underway for this route.
@@ -122,44 +258,47 @@ Cannot read the previousTitle for a route that has not yet been installed''', @@ -122,44 +258,47 @@ Cannot read the previousTitle for a route that has not yet been installed''',
122 /// would be allowed. 258 /// would be allowed.
123 bool get popGestureInProgress => isPopGestureInProgress(this); 259 bool get popGestureInProgress => isPopGestureInProgress(this);
124 260
125 - /// Whether a pop gesture can be started by the user. 261 + /// The title string of the previous [CupertinoPageRoute].
126 /// 262 ///
127 - /// Returns true if the user can edge-swipe to a previous route. 263 + /// The [ValueListenable]'s value is readable after the route is installed
  264 + /// onto a [Navigator]. The [ValueListenable] will also notify its listeners
  265 + /// if the value changes (such as by replacing the previous route).
128 /// 266 ///
129 - /// Returns false once [isPopGestureInProgress] is true, but  
130 - /// [isPopGestureInProgress] can only become true if [popGestureEnabled] was  
131 - /// true first. 267 + /// The [ValueListenable] itself will be null before the route is installed.
  268 + /// Its content value will be null if the previous route has no title or
  269 + /// is not a [CupertinoPageRoute].
132 /// 270 ///
133 - /// This should only be used between frames, not during build.  
134 - bool get popGestureEnabled => _isPopGestureEnabled(this);  
135 -  
136 - static bool _isPopGestureEnabled<T>(PageRoute<T> route) {  
137 - // If there's nothing to go back to, then obviously we don't support  
138 - // the back gesture.  
139 - if (route.isFirst) return false;  
140 - // If the route wouldn't actually pop if we popped it, then the gesture  
141 - // would be really confusing (or would skip internal routes),  
142 - //so disallow it.  
143 - if (route.willHandlePopInternally) return false;  
144 - // If attempts to dismiss this route might be vetoed such as in a page  
145 - // with forms, then do not allow the user to dismiss the route with a swipe.  
146 - if (route.hasScopedWillPopCallback) return false;  
147 - // Fullscreen dialogs aren't dismissible by back swipe.  
148 - if (route.fullscreenDialog) return false;  
149 - // If we're in an animation already, we cannot be manually swiped.  
150 - if (route.animation!.status != AnimationStatus.completed) return false;  
151 - // If we're being popped into, we also cannot be swiped until the pop above  
152 - // it completes. This translates to our secondary animation being  
153 - // dismissed.  
154 - if (route.secondaryAnimation!.status != AnimationStatus.dismissed) {  
155 - return false; 271 + /// See also:
  272 + ///
  273 + /// * [ValueListenableBuilder], which can be used to listen and rebuild
  274 + /// widgets based on a ValueListenable.
  275 + ValueListenable<String?> get previousTitle {
  276 + assert(
  277 + _previousTitle != null,
  278 + '''
  279 +Cannot read the previousTitle for a route that has not yet been installed''',
  280 + );
  281 + return _previousTitle!;
156 } 282 }
157 - // If we're in a gesture already, we cannot start another.  
158 - if (isPopGestureInProgress(route)) return false;  
159 283
160 - // Looks like a back gesture would be welcome!  
161 - return true;  
162 - } 284 + bool get showCupertinoParallax;
  285 +
  286 + /// {@template flutter.cupertino.CupertinoRouteTransitionMixin.title}
  287 + /// A title string for this route.
  288 + ///
  289 + /// Used to auto-populate [CupertinoNavigationBar] and
  290 + /// [CupertinoSliverNavigationBar]'s `middle`/`largeTitle` widgets when
  291 + /// one is not manually supplied.
  292 + /// {@endtemplate}
  293 + String? get title;
  294 +
  295 + @override
  296 + // A relatively rigorous eyeball estimation.
  297 + Duration get transitionDuration => const Duration(milliseconds: 400);
  298 +
  299 + /// Builds the primary contents of the route.
  300 + @protected
  301 + Widget buildContent(BuildContext context);
163 302
164 @override 303 @override
165 Widget buildPage(BuildContext context, Animation<double> animation, 304 Widget buildPage(BuildContext context, Animation<double> animation,
@@ -172,18 +311,37 @@ Cannot read the previousTitle for a route that has not yet been installed''', @@ -172,18 +311,37 @@ Cannot read the previousTitle for a route that has not yet been installed''',
172 ); 311 );
173 return result; 312 return result;
174 } 313 }
175 -  
176 - // Called by CupertinoBackGestureDetector when a pop ("back") drag start  
177 - // gesture is detected. The returned controller handles all of the subsequent  
178 - // drag events.  
179 - static CupertinoBackGestureController<T> _startPopGesture<T>(  
180 - PageRoute<T> route) {  
181 - assert(_isPopGestureEnabled(route));  
182 -  
183 - return CupertinoBackGestureController<T>(  
184 - navigator: route.navigator!,  
185 - controller: route.controller!, // protected access  
186 - ); 314 +
  315 + @override
  316 + Widget buildTransitions(BuildContext context, Animation<double> animation,
  317 + Animation<double> secondaryAnimation, Widget child) {
  318 + return buildPageTransitions<T>(
  319 + this, context, animation, secondaryAnimation, child);
  320 + }
  321 +
  322 + @override
  323 + bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {
  324 + // Don't perform outgoing animation if the next route is a
  325 + // fullscreen dialog.
  326 +
  327 + return (nextRoute is GetPageRouteTransitionMixin &&
  328 + !nextRoute.fullscreenDialog &&
  329 + nextRoute.showCupertinoParallax) ||
  330 + (nextRoute is CupertinoRouteTransitionMixin &&
  331 + !nextRoute.fullscreenDialog);
  332 + }
  333 +
  334 + @override
  335 + void didChangePrevious(Route<dynamic>? previousRoute) {
  336 + final previousTitleString = previousRoute is CupertinoRouteTransitionMixin
  337 + ? previousRoute.title
  338 + : null;
  339 + if (_previousTitle == null) {
  340 + _previousTitle = ValueNotifier<String?>(previousTitleString);
  341 + } else {
  342 + _previousTitle!.value = previousTitleString;
  343 + }
  344 + super.didChangePrevious(previousRoute);
187 } 345 }
188 346
189 /// Returns a [CupertinoFullscreenDialogTransition] if [route] is a full 347 /// Returns a [CupertinoFullscreenDialogTransition] if [route] is a full
@@ -485,211 +643,57 @@ Cannot read the previousTitle for a route that has not yet been installed''', @@ -485,211 +643,57 @@ Cannot read the previousTitle for a route that has not yet been installed''',
485 } 643 }
486 } 644 }
487 645
488 - @override  
489 - Widget buildTransitions(BuildContext context, Animation<double> animation,  
490 - Animation<double> secondaryAnimation, Widget child) {  
491 - return buildPageTransitions<T>(  
492 - this, context, animation, secondaryAnimation, child);  
493 - }  
494 -}  
495 -  
496 -class CupertinoBackGestureDetector<T> extends StatefulWidget {  
497 - const CupertinoBackGestureDetector({  
498 - Key? key,  
499 - required this.enabledCallback,  
500 - required this.onStartPopGesture,  
501 - required this.child,  
502 - required this.gestureWidth,  
503 - }) : super(key: key);  
504 -  
505 - final Widget child;  
506 - final double gestureWidth;  
507 -  
508 - final ValueGetter<bool> enabledCallback;  
509 -  
510 - final ValueGetter<CupertinoBackGestureController<T>> onStartPopGesture;  
511 -  
512 - @override  
513 - CupertinoBackGestureDetectorState<T> createState() =>  
514 - CupertinoBackGestureDetectorState<T>();  
515 -}  
516 -  
517 -class CupertinoBackGestureDetectorState<T>  
518 - extends State<CupertinoBackGestureDetector<T>> {  
519 - CupertinoBackGestureController<T>? _backGestureController;  
520 -  
521 - late HorizontalDragGestureRecognizer _recognizer;  
522 -  
523 - @override  
524 - void initState() {  
525 - super.initState();  
526 - _recognizer = HorizontalDragGestureRecognizer(debugOwner: this)  
527 - ..onStart = _handleDragStart  
528 - ..onUpdate = _handleDragUpdate  
529 - ..onEnd = _handleDragEnd  
530 - ..onCancel = _handleDragCancel;  
531 - }  
532 -  
533 - @override  
534 - void dispose() {  
535 - _recognizer.dispose();  
536 - super.dispose();  
537 - }  
538 -  
539 - void _handleDragStart(DragStartDetails details) {  
540 - assert(mounted);  
541 - assert(_backGestureController == null);  
542 - _backGestureController = widget.onStartPopGesture();  
543 - }  
544 -  
545 - void _handleDragUpdate(DragUpdateDetails details) {  
546 - assert(mounted);  
547 - assert(_backGestureController != null);  
548 - _backGestureController!.dragUpdate(  
549 - _convertToLogical(details.primaryDelta! / context.size!.width));  
550 - }  
551 -  
552 - void _handleDragEnd(DragEndDetails details) {  
553 - assert(mounted);  
554 - assert(_backGestureController != null);  
555 - _backGestureController!.dragEnd(_convertToLogical(  
556 - details.velocity.pixelsPerSecond.dx / context.size!.width));  
557 - _backGestureController = null;  
558 - }  
559 -  
560 - void _handleDragCancel() {  
561 - assert(mounted);  
562 - // This can be called even if start is not called, paired with  
563 - // the "down" event  
564 - // that we don't consider here.  
565 - _backGestureController?.dragEnd(0.0);  
566 - _backGestureController = null;  
567 - }  
568 -  
569 - void _handlePointerDown(PointerDownEvent event) {  
570 - if (widget.enabledCallback()) _recognizer.addPointer(event);  
571 - }  
572 -  
573 - double _convertToLogical(double value) {  
574 - switch (Directionality.of(context)) {  
575 - case TextDirection.rtl:  
576 - return -value;  
577 - case TextDirection.ltr:  
578 - return value;  
579 - }  
580 - }  
581 -  
582 - @override  
583 - Widget build(BuildContext context) {  
584 - assert(debugCheckHasDirectionality(context));  
585 - // For devices with notches, the drag area needs to be larger on the side  
586 - // that has the notch.  
587 - var dragAreaWidth = Directionality.of(context) == TextDirection.ltr  
588 - ? MediaQuery.of(context).padding.left  
589 - : MediaQuery.of(context).padding.right;  
590 - dragAreaWidth = max(dragAreaWidth, widget.gestureWidth);  
591 - return Stack(  
592 - fit: StackFit.passthrough,  
593 - children: <Widget>[  
594 - widget.child,  
595 - PositionedDirectional(  
596 - start: 0.0,  
597 - width: dragAreaWidth,  
598 - top: 0.0,  
599 - bottom: 0.0,  
600 - child: Listener(  
601 - onPointerDown: _handlePointerDown,  
602 - behavior: HitTestBehavior.translucent,  
603 - ),  
604 - ),  
605 - ],  
606 - );  
607 - }  
608 -}  
609 -  
610 -class CupertinoBackGestureController<T> {  
611 - /// Creates a controller for an iOS-style back gesture. 646 + // Called by CupertinoBackGestureDetector when a pop ("back") drag start
  647 + // gesture is detected. The returned controller handles all of the subsequent
  648 + // drag events.
  649 + /// True if an iOS-style back swipe pop gesture is currently
  650 + /// underway for [route].
612 /// 651 ///
613 - /// The [navigator] and [controller] arguments must not be null.  
614 - CupertinoBackGestureController({  
615 - required this.navigator,  
616 - required this.controller,  
617 - }) {  
618 - navigator.didStartUserGesture(); 652 + /// This just check the route's [NavigatorState.userGestureInProgress].
  653 + ///
  654 + /// See also:
  655 + ///
  656 + /// * [popGestureEnabled], which returns true if a user-triggered pop gesture
  657 + /// would be allowed.
  658 + static bool isPopGestureInProgress(PageRoute<dynamic> route) {
  659 + return route.navigator!.userGestureInProgress;
619 } 660 }
620 661
621 - final AnimationController controller;  
622 - final NavigatorState navigator;  
623 -  
624 - /// The drag gesture has changed by [fractionalDelta]. The total range of the  
625 - /// drag should be 0.0 to 1.0.  
626 - void dragUpdate(double delta) {  
627 - controller.value -= delta; 662 + static bool _isPopGestureEnabled<T>(PageRoute<T> route) {
  663 + // If there's nothing to go back to, then obviously we don't support
  664 + // the back gesture.
  665 + if (route.isFirst) return false;
  666 + // If the route wouldn't actually pop if we popped it, then the gesture
  667 + // would be really confusing (or would skip internal routes),
  668 + //so disallow it.
  669 + if (route.willHandlePopInternally) return false;
  670 + // If attempts to dismiss this route might be vetoed such as in a page
  671 + // with forms, then do not allow the user to dismiss the route with a swipe.
  672 + if (route.hasScopedWillPopCallback) return false;
  673 + // Fullscreen dialogs aren't dismissible by back swipe.
  674 + if (route.fullscreenDialog) return false;
  675 + // If we're in an animation already, we cannot be manually swiped.
  676 + if (route.animation!.status != AnimationStatus.completed) return false;
  677 + // If we're being popped into, we also cannot be swiped until the pop above
  678 + // it completes. This translates to our secondary animation being
  679 + // dismissed.
  680 + if (route.secondaryAnimation!.status != AnimationStatus.dismissed) {
  681 + return false;
628 } 682 }
  683 + // If we're in a gesture already, we cannot start another.
  684 + if (isPopGestureInProgress(route)) return false;
629 685
630 - /// The drag gesture has ended with a horizontal motion of  
631 - /// [fractionalVelocity] as a fraction of screen width per second.  
632 - void dragEnd(double velocity) {  
633 - // Fling in the appropriate direction.  
634 - // AnimationController.fling is guaranteed to  
635 - // take at least one frame.  
636 - //  
637 - // This curve has been determined through rigorously eyeballing native iOS  
638 - // animations.  
639 - const Curve animationCurve = Curves.fastLinearToSlowEaseIn;  
640 - final bool animateForward;  
641 -  
642 - // If the user releases the page before mid screen with sufficient velocity,  
643 - // or after mid screen, we should animate the page out. Otherwise, the page  
644 - // should be animated back in.  
645 - if (velocity.abs() >= _kMinFlingVelocity) {  
646 - animateForward = velocity <= 0;  
647 - } else {  
648 - animateForward = controller.value > 0.5; 686 + // Looks like a back gesture would be welcome!
  687 + return true;
649 } 688 }
650 689
651 - if (animateForward) {  
652 - // The closer the panel is to dismissing, the shorter the animation is.  
653 - // We want to cap the animation time, but we want to use a linear curve  
654 - // to determine it.  
655 - final droppedPageForwardAnimationTime = min(  
656 - lerpDouble(  
657 - _kMaxDroppedSwipePageForwardAnimationTime, 0, controller.value)!  
658 - .floor(),  
659 - _kMaxPageBackAnimationTime,  
660 - );  
661 - controller.animateTo(1.0,  
662 - duration: Duration(milliseconds: droppedPageForwardAnimationTime),  
663 - curve: animationCurve);  
664 - } else {  
665 - // This route is destined to pop at this point. Reuse navigator's pop.  
666 - navigator.pop();  
667 -  
668 - // The popping may have finished inline if already at the  
669 - // target destination.  
670 - if (controller.isAnimating) {  
671 - // Otherwise, use a custom popping animation duration and curve.  
672 - final droppedPageBackAnimationTime = lerpDouble(  
673 - 0, _kMaxDroppedSwipePageForwardAnimationTime, controller.value)!  
674 - .floor();  
675 - controller.animateBack(0.0,  
676 - duration: Duration(milliseconds: droppedPageBackAnimationTime),  
677 - curve: animationCurve);  
678 - }  
679 - } 690 + static CupertinoBackGestureController<T> _startPopGesture<T>(
  691 + PageRoute<T> route) {
  692 + assert(_isPopGestureEnabled(route));
680 693
681 - if (controller.isAnimating) {  
682 - // Keep the userGestureInProgress in true state so we don't change the  
683 - // curve of the page transition mid-flight since CupertinoPageTransition  
684 - // depends on userGestureInProgress.  
685 - late AnimationStatusListener animationStatusCallback;  
686 - animationStatusCallback = (status) {  
687 - navigator.didStopUserGesture();  
688 - controller.removeStatusListener(animationStatusCallback);  
689 - };  
690 - controller.addStatusListener(animationStatusCallback);  
691 - } else {  
692 - navigator.didStopUserGesture();  
693 - } 694 + return CupertinoBackGestureController<T>(
  695 + navigator: route.navigator!,
  696 + controller: route.controller!, // protected access
  697 + );
694 } 698 }
695 } 699 }
@@ -145,7 +145,7 @@ class GetSnackBar extends StatefulWidget { @@ -145,7 +145,7 @@ class GetSnackBar extends StatefulWidget {
145 /// Default is 0.0. If different than 0.0, blurs only Snack's background. 145 /// Default is 0.0. If different than 0.0, blurs only Snack's background.
146 /// To take effect, make sure your [backgroundColor] has some opacity. 146 /// To take effect, make sure your [backgroundColor] has some opacity.
147 /// The greater the value, the greater the blur. 147 /// The greater the value, the greater the blur.
148 - final double? barBlur; 148 + final double barBlur;
149 149
150 /// Default is 0.0. If different than 0.0, creates a blurred 150 /// Default is 0.0. If different than 0.0, creates a blurred
151 /// overlay that prevents the user from interacting with the screen. 151 /// overlay that prevents the user from interacting with the screen.
@@ -201,7 +201,7 @@ class GetSnackBar extends StatefulWidget { @@ -201,7 +201,7 @@ class GetSnackBar extends StatefulWidget {
201 }) : super(key: key); 201 }) : super(key: key);
202 202
203 @override 203 @override
204 - State createState() => _GetSnackBarState(); 204 + State createState() => GetSnackBarState();
205 205
206 /// Show the snack. It's call [SnackbarStatus.OPENING] state 206 /// Show the snack. It's call [SnackbarStatus.OPENING] state
207 /// followed by [SnackbarStatus.OPEN] 207 /// followed by [SnackbarStatus.OPEN]
@@ -210,30 +210,7 @@ class GetSnackBar extends StatefulWidget { @@ -210,30 +210,7 @@ class GetSnackBar extends StatefulWidget {
210 } 210 }
211 } 211 }
212 212
213 -enum RowStyle {  
214 - icon,  
215 - action,  
216 - all,  
217 - none,  
218 -}  
219 -  
220 -/// Indicates Status of snackbar  
221 -/// [SnackbarStatus.OPEN] Snack is fully open, [SnackbarStatus.CLOSED] Snackbar  
222 -/// has closed,  
223 -/// [SnackbarStatus.OPENING] Starts with the opening animation and ends  
224 -/// with the full  
225 -/// snackbar display, [SnackbarStatus.CLOSING] Starts with the closing animation  
226 -/// and ends  
227 -/// with the full snackbar dispose  
228 -enum SnackbarStatus { OPEN, CLOSED, OPENING, CLOSING }  
229 -  
230 -/// Indicates if snack is going to start at the [TOP] or at the [BOTTOM]  
231 -enum SnackPosition { TOP, BOTTOM }  
232 -  
233 -/// Indicates if snack will be attached to the edge of the screen or not  
234 -enum SnackStyle { FLOATING, GROUNDED }  
235 -  
236 -class _GetSnackBarState extends State<GetSnackBar> 213 +class GetSnackBarState extends State<GetSnackBar>
237 with TickerProviderStateMixin { 214 with TickerProviderStateMixin {
238 AnimationController? _fadeController; 215 AnimationController? _fadeController;
239 late Animation<double> _fadeAnimation; 216 late Animation<double> _fadeAnimation;
@@ -306,7 +283,7 @@ class _GetSnackBarState extends State<GetSnackBar> @@ -306,7 +283,7 @@ class _GetSnackBarState extends State<GetSnackBar>
306 borderRadius: BorderRadius.circular(widget.borderRadius), 283 borderRadius: BorderRadius.circular(widget.borderRadius),
307 child: BackdropFilter( 284 child: BackdropFilter(
308 filter: ImageFilter.blur( 285 filter: ImageFilter.blur(
309 - sigmaX: widget.barBlur!, sigmaY: widget.barBlur!), 286 + sigmaX: widget.barBlur, sigmaY: widget.barBlur),
310 child: Container( 287 child: Container(
311 height: snapshot.data!.height, 288 height: snapshot.data!.height,
312 width: snapshot.data!.width, 289 width: snapshot.data!.width,
@@ -579,3 +556,26 @@ You need to either use message[String], or messageText[Widget] or define a userI @@ -579,3 +556,26 @@ You need to either use message[String], or messageText[Widget] or define a userI
579 556
580 void _updateProgress() => setState(() {}); 557 void _updateProgress() => setState(() {});
581 } 558 }
  559 +
  560 +enum RowStyle {
  561 + icon,
  562 + action,
  563 + all,
  564 + none,
  565 +}
  566 +
  567 +/// Indicates Status of snackbar
  568 +/// [SnackbarStatus.OPEN] Snack is fully open, [SnackbarStatus.CLOSED] Snackbar
  569 +/// has closed,
  570 +/// [SnackbarStatus.OPENING] Starts with the opening animation and ends
  571 +/// with the full
  572 +/// snackbar display, [SnackbarStatus.CLOSING] Starts with the closing animation
  573 +/// and ends
  574 +/// with the full snackbar dispose
  575 +enum SnackbarStatus { OPEN, CLOSED, OPENING, CLOSING }
  576 +
  577 +/// Indicates if snack is going to start at the [TOP] or at the [BOTTOM]
  578 +enum SnackPosition { TOP, BOTTOM }
  579 +
  580 +/// Indicates if snack will be attached to the edge of the screen or not
  581 +enum SnackStyle { FLOATING, GROUNDED }
@@ -8,12 +8,13 @@ import '../../../get.dart'; @@ -8,12 +8,13 @@ import '../../../get.dart';
8 class SnackbarController { 8 class SnackbarController {
9 static final _snackBarQueue = _SnackBarQueue(); 9 static final _snackBarQueue = _SnackBarQueue();
10 static bool get isSnackbarBeingShown => _snackBarQueue._isJobInProgress; 10 static bool get isSnackbarBeingShown => _snackBarQueue._isJobInProgress;
  11 + final key = GlobalKey<GetSnackBarState>();
11 12
12 late Animation<double> _filterBlurAnimation; 13 late Animation<double> _filterBlurAnimation;
13 late Animation<Color?> _filterColorAnimation; 14 late Animation<Color?> _filterColorAnimation;
14 15
15 final GetSnackBar snackbar; 16 final GetSnackBar snackbar;
16 - final _transitionCompleter = Completer<SnackbarController>(); 17 + final _transitionCompleter = Completer();
17 18
18 late SnackbarStatusCallback? _snackbarStatus; 19 late SnackbarStatusCallback? _snackbarStatus;
19 late final Alignment? _initialAlignment; 20 late final Alignment? _initialAlignment;
@@ -42,10 +43,14 @@ class SnackbarController { @@ -42,10 +43,14 @@ class SnackbarController {
42 43
43 SnackbarController(this.snackbar); 44 SnackbarController(this.snackbar);
44 45
45 - Future<SnackbarController> get future => _transitionCompleter.future; 46 + Future<void> get future => _transitionCompleter.future;
46 47
47 /// Close the snackbar with animation 48 /// Close the snackbar with animation
48 - Future<void> close() async { 49 + Future<void> close({bool withAnimations = true}) async {
  50 + if (!withAnimations) {
  51 + _removeOverlay();
  52 + return;
  53 + }
49 _removeEntry(); 54 _removeEntry();
50 await future; 55 await future;
51 } 56 }
@@ -141,7 +146,7 @@ class SnackbarController { @@ -141,7 +146,7 @@ class SnackbarController {
141 return AnimationController( 146 return AnimationController(
142 duration: snackbar.animationDuration, 147 duration: snackbar.animationDuration,
143 debugLabel: '$runtimeType', 148 debugLabel: '$runtimeType',
144 - vsync: navigator!, 149 + vsync: _overlayState!,
145 ); 150 );
146 } 151 }
147 152
@@ -181,7 +186,7 @@ class SnackbarController { @@ -181,7 +186,7 @@ class SnackbarController {
181 onTap: () { 186 onTap: () {
182 if (snackbar.isDismissible && !_onTappedDismiss) { 187 if (snackbar.isDismissible && !_onTappedDismiss) {
183 _onTappedDismiss = true; 188 _onTappedDismiss = true;
184 - Get.back(); 189 + close();
185 } 190 }
186 }, 191 },
187 child: AnimatedBuilder( 192 child: AnimatedBuilder(
@@ -252,7 +257,8 @@ class SnackbarController { @@ -252,7 +257,8 @@ class SnackbarController {
252 }, 257 },
253 key: const Key('dismissible'), 258 key: const Key('dismissible'),
254 onDismissed: (_) { 259 onDismissed: (_) {
255 - _onDismiss(); 260 + _wasDismissedBySwipe = true;
  261 + _removeEntry();
256 }, 262 },
257 child: _getSnackbarContainer(child), 263 child: _getSnackbarContainer(child),
258 ); 264 );
@@ -291,12 +297,6 @@ class SnackbarController { @@ -291,12 +297,6 @@ class SnackbarController {
291 } 297 }
292 } 298 }
293 299
294 - void _onDismiss() {  
295 - _cancelTimer();  
296 - _wasDismissedBySwipe = true;  
297 - _removeEntry();  
298 - }  
299 -  
300 void _removeEntry() { 300 void _removeEntry() {
301 assert( 301 assert(
302 !_transitionCompleter.isCompleted, 302 !_transitionCompleter.isCompleted,
@@ -307,7 +307,6 @@ class SnackbarController { @@ -307,7 +307,6 @@ class SnackbarController {
307 307
308 if (_wasDismissedBySwipe) { 308 if (_wasDismissedBySwipe) {
309 Timer(const Duration(milliseconds: 200), _controller.reset); 309 Timer(const Duration(milliseconds: 200), _controller.reset);
310 -  
311 _wasDismissedBySwipe = false; 310 _wasDismissedBySwipe = false;
312 } else { 311 } else {
313 _controller.reverse(); 312 _controller.reverse();
@@ -323,7 +322,7 @@ class SnackbarController { @@ -323,7 +322,7 @@ class SnackbarController {
323 'Cannot remove overlay from a disposed snackbar'); 322 'Cannot remove overlay from a disposed snackbar');
324 _controller.dispose(); 323 _controller.dispose();
325 _overlayEntries.clear(); 324 _overlayEntries.clear();
326 - _transitionCompleter.complete(this); 325 + _transitionCompleter.complete();
327 } 326 }
328 327
329 Future<void> _show() { 328 Future<void> _show() {
@@ -365,6 +364,7 @@ class _SnackBarQueue { @@ -365,6 +364,7 @@ class _SnackBarQueue {
365 } 364 }
366 365
367 Future<void> _closeCurrentJob() async { 366 Future<void> _closeCurrentJob() async {
368 - await _currentSnackbar?.close(); 367 + if (_currentSnackbar == null) return;
  368 + await _currentSnackbar!.close();
369 } 369 }
370 } 370 }
@@ -5,6 +5,8 @@ part of rx_types; @@ -5,6 +5,8 @@ part of rx_types;
5 /// This interface is the contract that _RxImpl]<T> uses in all it's 5 /// This interface is the contract that _RxImpl]<T> uses in all it's
6 /// subclass. 6 /// subclass.
7 abstract class RxInterface<T> { 7 abstract class RxInterface<T> {
  8 + static RxInterface? proxy;
  9 +
8 bool get canUpdate; 10 bool get canUpdate;
9 11
10 /// Adds a listener to stream 12 /// Adds a listener to stream
@@ -13,8 +15,6 @@ abstract class RxInterface<T> { @@ -13,8 +15,6 @@ abstract class RxInterface<T> {
13 /// Close the Rx Variable 15 /// Close the Rx Variable
14 void close(); 16 void close();
15 17
16 - static RxInterface? proxy;  
17 -  
18 /// Calls `callback` with current value, when the value changes. 18 /// Calls `callback` with current value, when the value changes.
19 StreamSubscription<T> listen(void Function(T event) onData, 19 StreamSubscription<T> listen(void Function(T event) onData,
20 {Function? onError, void Function()? onDone, bool? cancelOnError}); 20 {Function? onError, void Function()? onDone, bool? cancelOnError});
  1 +// ignore_for_file: lines_longer_than_80_chars
  2 +
  3 +import 'package:flutter/foundation.dart';
  4 +import 'package:flutter/material.dart';
1 import 'package:flutter/scheduler.dart'; 5 import 'package:flutter/scheduler.dart';
  6 +
2 import '../../get_state_manager.dart'; 7 import '../../get_state_manager.dart';
3 8
4 /// Used like `SingleTickerProviderMixin` but only with Get Controllers. 9 /// Used like `SingleTickerProviderMixin` but only with Get Controllers.
@@ -7,6 +12,84 @@ import '../../get_state_manager.dart'; @@ -7,6 +12,84 @@ import '../../get_state_manager.dart';
7 /// Example: 12 /// Example:
8 ///``` 13 ///```
9 ///class SplashController extends GetxController with 14 ///class SplashController extends GetxController with
  15 +/// GetSingleTickerProviderStateMixin {
  16 +/// AnimationController controller;
  17 +///
  18 +/// @override
  19 +/// void onInit() {
  20 +/// final duration = const Duration(seconds: 2);
  21 +/// controller =
  22 +/// AnimationController.unbounded(duration: duration, vsync: this);
  23 +/// controller.repeat();
  24 +/// controller.addListener(() =>
  25 +/// print("Animation Controller value: ${controller.value}"));
  26 +/// }
  27 +/// ...
  28 +/// ```
  29 +mixin GetSingleTickerProviderStateMixin on GetxController
  30 + implements TickerProvider {
  31 + Ticker? _ticker;
  32 +
  33 + @override
  34 + Ticker createTicker(TickerCallback onTick) {
  35 + assert(() {
  36 + if (_ticker == null) return true;
  37 + throw FlutterError.fromParts(<DiagnosticsNode>[
  38 + ErrorSummary(
  39 + '$runtimeType is a SingleTickerProviderStateMixin but multiple tickers were created.'),
  40 + ErrorDescription(
  41 + 'A SingleTickerProviderStateMixin can only be used as a TickerProvider once.'),
  42 + ErrorHint(
  43 + 'If a State is used for multiple AnimationController objects, or if it is passed to other '
  44 + 'objects and those objects might use it more than one time in total, then instead of '
  45 + 'mixing in a SingleTickerProviderStateMixin, use a regular TickerProviderStateMixin.',
  46 + ),
  47 + ]);
  48 + }());
  49 + _ticker =
  50 + Ticker(onTick, debugLabel: kDebugMode ? 'created by $this' : null);
  51 + // We assume that this is called from initState, build, or some sort of
  52 + // event handler, and that thus TickerMode.of(context) would return true. We
  53 + // can't actually check that here because if we're in initState then we're
  54 + // not allowed to do inheritance checks yet.
  55 + return _ticker!;
  56 + }
  57 +
  58 + void didChangeDependencies(BuildContext context) {
  59 + if (_ticker != null) _ticker!.muted = !TickerMode.of(context);
  60 + }
  61 +
  62 + @override
  63 + void onClose() {
  64 + assert(() {
  65 + if (_ticker == null || !_ticker!.isActive) return true;
  66 + throw FlutterError.fromParts(<DiagnosticsNode>[
  67 + ErrorSummary('$this was disposed with an active Ticker.'),
  68 + ErrorDescription(
  69 + '$runtimeType created a Ticker via its SingleTickerProviderStateMixin, but at the time '
  70 + 'dispose() was called on the mixin, that Ticker was still active. The Ticker must '
  71 + 'be disposed before calling super.dispose().',
  72 + ),
  73 + ErrorHint(
  74 + 'Tickers used by AnimationControllers '
  75 + 'should be disposed by calling dispose() on the AnimationController itself. '
  76 + 'Otherwise, the ticker will leak.',
  77 + ),
  78 + _ticker!.describeForError('The offending ticker was'),
  79 + ]);
  80 + }());
  81 + super.onClose();
  82 + }
  83 +}
  84 +
  85 +@Deprecated('use GetSingleTickerProviderStateMixin')
  86 +
  87 +/// Used like `SingleTickerProviderMixin` but only with Get Controllers.
  88 +/// Simplifies AnimationController creation inside GetxController.
  89 +///
  90 +/// Example:
  91 +///```
  92 +///class SplashController extends GetxController with
10 /// SingleGetTickerProviderMixin { 93 /// SingleGetTickerProviderMixin {
11 /// AnimationController _ac; 94 /// AnimationController _ac;
12 /// 95 ///
@@ -481,58 +481,6 @@ void main() { @@ -481,58 +481,6 @@ void main() {
481 expect(find.byType(FirstScreen), findsOneWidget); 481 expect(find.byType(FirstScreen), findsOneWidget);
482 }); 482 });
483 483
484 - testWidgets("Get.snackbar test", (tester) async {  
485 - await tester.pumpWidget(  
486 - GetMaterialApp(  
487 - popGesture: true,  
488 - home: ElevatedButton(  
489 - child: Text('Open Snackbar'),  
490 - onPressed: () {  
491 - Get.snackbar('title', "message", duration: Duration(seconds: 1));  
492 - },  
493 - ),  
494 - ),  
495 - );  
496 -  
497 - expect(Get.isSnackbarOpen, false);  
498 - await tester.tap(find.text('Open Snackbar'));  
499 -  
500 - expect(Get.isSnackbarOpen, true);  
501 - await tester.pump(const Duration(seconds: 1));  
502 - });  
503 -  
504 - testWidgets("Get.rawSnackbar test", (tester) async {  
505 - await tester.pumpWidget(  
506 - Wrapper(  
507 - child: ElevatedButton(  
508 - child: Text('Open Snackbar'),  
509 - onPressed: () {  
510 - Get.rawSnackbar(  
511 - title: 'title',  
512 - message: "message",  
513 - onTap: (_) {  
514 - print('snackbar tapped');  
515 - },  
516 - duration: Duration(seconds: 1),  
517 - shouldIconPulse: true,  
518 - icon: Icon(Icons.alarm),  
519 - showProgressIndicator: true,  
520 - barBlur: null,  
521 - isDismissible: true,  
522 - leftBarIndicatorColor: Colors.amber,  
523 - overlayBlur: 1.0,  
524 - );  
525 - },  
526 - ),  
527 - ),  
528 - );  
529 -  
530 - expect(Get.isSnackbarOpen, false);  
531 - await tester.tap(find.text('Open Snackbar'));  
532 -  
533 - expect(Get.isSnackbarOpen, true);  
534 - await tester.pump(const Duration(seconds: 1));  
535 - });  
536 } 484 }
537 485
538 class FirstScreen extends StatelessWidget { 486 class FirstScreen extends StatelessWidget {
@@ -10,28 +10,49 @@ void main() { @@ -10,28 +10,49 @@ void main() {
10 name: '/city', 10 name: '/city',
11 page: () => Container(), 11 page: () => Container(),
12 children: [ 12 children: [
13 - GetPage(name: '/home', page: () => Container(), children: [  
14 - GetPage(name: '/bed-room', page: () => Container()),  
15 - GetPage(name: '/living-room', page: () => Container()),  
16 - ]), 13 + GetPage(
  14 + name: '/home',
  15 + page: () => Container(),
  16 + transition: Transition.rightToLeftWithFade,
  17 + children: [
  18 + GetPage(
  19 + name: '/bed-room',
  20 + transition: Transition.size,
  21 + page: () => Container(),
  22 + ),
  23 + GetPage(
  24 + name: '/living-room',
  25 + transition: Transition.topLevel,
  26 + page: () => Container(),
  27 + ),
  28 + ],
  29 + ),
17 GetPage( 30 GetPage(
18 name: '/work', 31 name: '/work',
  32 + transition: Transition.upToDown,
19 page: () => Container(), 33 page: () => Container(),
20 children: [ 34 children: [
21 GetPage( 35 GetPage(
22 name: '/office', 36 name: '/office',
  37 + transition: Transition.zoom,
23 page: () => Container(), 38 page: () => Container(),
24 children: [ 39 children: [
25 GetPage( 40 GetPage(
26 name: '/pen', 41 name: '/pen',
  42 + transition: Transition.cupertino,
27 page: () => Container(), 43 page: () => Container(),
28 parameters: testParams, 44 parameters: testParams,
29 ), 45 ),
30 - GetPage(name: '/paper', page: () => Container()), 46 + GetPage(
  47 + name: '/paper',
  48 + page: () => Container(),
  49 + transition: Transition.downToUp,
  50 + ),
31 ], 51 ],
32 ), 52 ),
33 GetPage( 53 GetPage(
34 name: '/meeting-room', 54 name: '/meeting-room',
  55 + transition: Transition.fade,
35 page: () => Container(), 56 page: () => Container(),
36 ), 57 ),
37 ], 58 ],
@@ -56,15 +77,42 @@ void main() { @@ -56,15 +77,42 @@ void main() {
56 77
57 test('Parse Page without children', () { 78 test('Parse Page without children', () {
58 final pageTree = [ 79 final pageTree = [
59 - GetPage(name: '/city', page: () => Container()),  
60 - GetPage(name: '/city/home', page: () => Container()),  
61 - GetPage(name: '/city/home/bed-room', page: () => Container()),  
62 - GetPage(name: '/city/home/living-room', page: () => Container()),  
63 - GetPage(name: '/city/work', page: () => Container()),  
64 - GetPage(name: '/city/work/office', page: () => Container()),  
65 - GetPage(name: '/city/work/office/pen', page: () => Container()),  
66 - GetPage(name: '/city/work/office/paper', page: () => Container()),  
67 - GetPage(name: '/city/work/meeting-room', page: () => Container()), 80 + GetPage(
  81 + name: '/city',
  82 + page: () => Container(),
  83 + transition: Transition.cupertino),
  84 + GetPage(
  85 + name: '/city/home',
  86 + page: () => Container(),
  87 + transition: Transition.downToUp),
  88 + GetPage(
  89 + name: '/city/home/bed-room',
  90 + page: () => Container(),
  91 + transition: Transition.fade),
  92 + GetPage(
  93 + name: '/city/home/living-room',
  94 + page: () => Container(),
  95 + transition: Transition.fadeIn),
  96 + GetPage(
  97 + name: '/city/work',
  98 + page: () => Container(),
  99 + transition: Transition.leftToRight),
  100 + GetPage(
  101 + name: '/city/work/office',
  102 + page: () => Container(),
  103 + transition: Transition.leftToRightWithFade),
  104 + GetPage(
  105 + name: '/city/work/office/pen',
  106 + page: () => Container(),
  107 + transition: Transition.native),
  108 + GetPage(
  109 + name: '/city/work/office/paper',
  110 + page: () => Container(),
  111 + transition: Transition.noTransition),
  112 + GetPage(
  113 + name: '/city/work/meeting-room',
  114 + page: () => Container(),
  115 + transition: Transition.rightToLeft),
68 ]; 116 ];
69 117
70 final tree = ParseRouteTree(routes: pageTree); 118 final tree = ParseRouteTree(routes: pageTree);
1 -// import 'package:flutter/material.dart';  
2 -// import 'package:flutter_test/flutter_test.dart';  
3 -// import 'package:get/get.dart'; 1 +import 'package:flutter/cupertino.dart';
  2 +import 'package:flutter/material.dart';
  3 +import 'package:flutter_test/flutter_test.dart';
  4 +import 'package:get/get.dart';
4 5
5 void main() { 6 void main() {
6 - // testWidgets(  
7 - // 'GetPage page null',  
8 - // (tester) async {  
9 - // expect(() => GetPage(page: null, name: null), throwsAssertionError);  
10 - // },  
11 - // );  
12 -  
13 - // testWidgets(  
14 - // "GetPage maintainState null",  
15 - // (tester) async {  
16 - // expect(  
17 - // () => GetPage(page: () => Scaffold(), maintainState: null, name: '/'),  
18 - // throwsAssertionError,  
19 - // );  
20 - // },  
21 - // );  
22 -  
23 - // testWidgets(  
24 - // "GetPage name null",  
25 - // (tester) async {  
26 - // expect(  
27 - // () => GetPage(page: () => Scaffold(),  
28 - // maintainState: null, name: null),  
29 - // throwsAssertionError,  
30 - // );  
31 - // },  
32 - // );  
33 -  
34 - // testWidgets(  
35 - // "GetPage fullscreenDialog null",  
36 - // (tester) async {  
37 - // expect(  
38 - // () =>  
39 - // GetPage(page: () => Scaffold(), fullscreenDialog: null, name: '/'),  
40 - // throwsAssertionError,  
41 - // );  
42 - // },  
43 - // ); 7 + testWidgets('Back swipe dismiss interrupted by route push', (tester) async {
  8 + // final scaffoldKey = GlobalKey();
  9 +
  10 + await tester.pumpWidget(
  11 + GetCupertinoApp(
  12 + popGesture: true,
  13 + home: CupertinoPageScaffold(
  14 + // key: scaffoldKey,
  15 + child: Center(
  16 + child: CupertinoButton(
  17 + onPressed: () {
  18 + Get.to(() => CupertinoPageScaffold(
  19 + child: Center(child: Text('route')),
  20 + ));
  21 + },
  22 + child: const Text('push'),
  23 + ),
  24 + ),
  25 + ),
  26 + ),
  27 + );
  28 +
  29 + // Check the basic iOS back-swipe dismiss transition. Dragging the pushed
  30 + // route halfway across the screen will trigger the iOS dismiss animation
  31 +
  32 + await tester.tap(find.text('push'));
  33 + await tester.pumpAndSettle();
  34 + expect(find.text('route'), findsOneWidget);
  35 + expect(find.text('push'), findsNothing);
  36 +
  37 + var gesture = await tester.startGesture(const Offset(5, 300));
  38 + await gesture.moveBy(const Offset(400, 0));
  39 + await gesture.up();
  40 + await tester.pump();
  41 + expect(
  42 + // The 'route' route has been dragged to the right, halfway across the screen
  43 + tester.getTopLeft(find.ancestor(
  44 + of: find.text('route'),
  45 + matching: find.byType(CupertinoPageScaffold))),
  46 + const Offset(400, 0),
  47 + );
  48 + expect(
  49 + // The 'push' route is sliding in from the left.
  50 + tester
  51 + .getTopLeft(find.ancestor(
  52 + of: find.text('push'),
  53 + matching: find.byType(CupertinoPageScaffold)))
  54 + .dx,
  55 + 0,
  56 + );
  57 + await tester.pumpAndSettle();
  58 + expect(find.text('push'), findsOneWidget);
  59 + expect(
  60 + tester.getTopLeft(find.ancestor(
  61 + of: find.text('push'), matching: find.byType(CupertinoPageScaffold))),
  62 + Offset.zero,
  63 + );
  64 + expect(find.text('route'), findsNothing);
  65 +
  66 + // Run the dismiss animation 60%, which exposes the route "push" button,
  67 + // and then press the button.
  68 +
  69 + await tester.tap(find.text('push'));
  70 + await tester.pumpAndSettle();
  71 + expect(find.text('route'), findsOneWidget);
  72 + expect(find.text('push'), findsNothing);
  73 +
  74 + gesture = await tester.startGesture(const Offset(5, 300));
  75 + await gesture.moveBy(const Offset(400, 0)); // Drag halfway.
  76 + await gesture.up();
  77 + // Trigger the snapping animation.
  78 + // Since the back swipe drag was brought to >=50% of the screen, it will
  79 + // self snap to finish the pop transition as the gesture is lifted.
  80 + //
  81 + // This drag drop animation is 400ms when dropped exactly halfway
  82 + // (800 / [pixel distance remaining], see
  83 + // _CupertinoBackGestureController.dragEnd). It follows a curve that is very
  84 + // steep initially.
  85 + await tester.pump();
  86 + expect(
  87 + tester.getTopLeft(find.ancestor(
  88 + of: find.text('route'),
  89 + matching: find.byType(CupertinoPageScaffold))),
  90 + const Offset(400, 0),
  91 + );
  92 + // Let the dismissing snapping animation go 60%.
  93 + await tester.pump(const Duration(milliseconds: 240));
  94 + expect(
  95 + tester
  96 + .getTopLeft(find.ancestor(
  97 + of: find.text('route'),
  98 + matching: find.byType(CupertinoPageScaffold)))
  99 + .dx,
  100 + moreOrLessEquals(798, epsilon: 1),
  101 + );
  102 +
  103 + // Use the navigator to push a route instead of tapping the 'push' button.
  104 + // The topmost route (the one that's animating away), ignores input while
  105 + // the pop is underway because route.navigator.userGestureInProgress.
  106 + Get.to(() => const CupertinoPageScaffold(
  107 + child: Center(child: Text('route')),
  108 + ));
  109 +
  110 + await tester.pumpAndSettle();
  111 + expect(find.text('route'), findsOneWidget);
  112 + expect(find.text('push'), findsNothing);
  113 + expect(
  114 + tester
  115 + .state<NavigatorState>(find.byType(Navigator))
  116 + .userGestureInProgress,
  117 + false,
  118 + );
  119 + });
44 } 120 }
  1 +import 'package:flutter/material.dart';
  2 +import 'package:flutter_test/flutter_test.dart';
  3 +import 'package:get/get.dart';
  4 +
  5 +void main() {
  6 + testWidgets("test if Get.isSnackbarOpen works with Get.snackbar",
  7 + (tester) async {
  8 + await tester.pumpWidget(
  9 + GetMaterialApp(
  10 + popGesture: true,
  11 + home: ElevatedButton(
  12 + child: Text('Open Snackbar'),
  13 + onPressed: () {
  14 + Get.snackbar(
  15 + 'title',
  16 + "message",
  17 + duration: Duration(seconds: 1),
  18 + isDismissible: false,
  19 + );
  20 + },
  21 + ),
  22 + ),
  23 + );
  24 +
  25 + expect(Get.isSnackbarOpen, false);
  26 + await tester.tap(find.text('Open Snackbar'));
  27 +
  28 + expect(Get.isSnackbarOpen, true);
  29 + await tester.pump(const Duration(seconds: 1));
  30 + expect(Get.isSnackbarOpen, false);
  31 + });
  32 +
  33 + testWidgets("Get.rawSnackbar test", (tester) async {
  34 + await tester.pumpWidget(
  35 + GetMaterialApp(
  36 + popGesture: true,
  37 + home: ElevatedButton(
  38 + child: Text('Open Snackbar'),
  39 + onPressed: () {
  40 + Get.rawSnackbar(
  41 + title: 'title',
  42 + message: "message",
  43 + onTap: (_) {
  44 + print('snackbar tapped');
  45 + },
  46 + shouldIconPulse: true,
  47 + icon: Icon(Icons.alarm),
  48 + showProgressIndicator: true,
  49 + duration: Duration(seconds: 1),
  50 + isDismissible: true,
  51 + leftBarIndicatorColor: Colors.amber,
  52 + overlayBlur: 1.0,
  53 + );
  54 + },
  55 + ),
  56 + ),
  57 + );
  58 +
  59 + expect(Get.isSnackbarOpen, false);
  60 + await tester.tap(find.text('Open Snackbar'));
  61 +
  62 + expect(Get.isSnackbarOpen, true);
  63 + await tester.pump(const Duration(seconds: 1));
  64 + expect(Get.isSnackbarOpen, false);
  65 + });
  66 +
  67 + testWidgets("test snackbar queue", (tester) async {
  68 + final messageOne = Text('title');
  69 +
  70 + final messageTwo = Text('titleTwo');
  71 +
  72 + await tester.pumpWidget(
  73 + GetMaterialApp(
  74 + popGesture: true,
  75 + home: ElevatedButton(
  76 + child: Text('Open Snackbar'),
  77 + onPressed: () {
  78 + Get.rawSnackbar(
  79 + messageText: messageOne, duration: Duration(seconds: 1));
  80 + Get.rawSnackbar(
  81 + messageText: messageTwo, duration: Duration(seconds: 1));
  82 + },
  83 + ),
  84 + ),
  85 + );
  86 +
  87 + expect(Get.isSnackbarOpen, false);
  88 + await tester.tap(find.text('Open Snackbar'));
  89 + expect(Get.isSnackbarOpen, true);
  90 +
  91 + await tester.pump(const Duration(milliseconds: 500));
  92 + expect(find.text('title'), findsOneWidget);
  93 + expect(find.text('titleTwo'), findsNothing);
  94 + await tester.pump(const Duration(milliseconds: 500));
  95 + expect(find.text('title'), findsNothing);
  96 + expect(find.text('titleTwo'), findsOneWidget);
  97 + Get.closeAllSnackbars();
  98 + });
  99 +
  100 + testWidgets("test snackbar dismissible", (tester) async {
  101 + const dismissDirection = DismissDirection.vertical;
  102 + const snackBarTapTarget = Key('snackbar-tap-target');
  103 +
  104 + late final GetSnackBar getBar;
  105 +
  106 + await tester.pumpWidget(GetMaterialApp(
  107 + home: Scaffold(
  108 + body: Builder(
  109 + builder: (context) {
  110 + return Column(
  111 + children: <Widget>[
  112 + GestureDetector(
  113 + key: snackBarTapTarget,
  114 + onTap: () {
  115 + getBar = GetSnackBar(
  116 + message: 'bar1',
  117 + duration: const Duration(seconds: 2),
  118 + isDismissible: true,
  119 + dismissDirection: dismissDirection,
  120 + );
  121 + Get.showSnackbar(getBar);
  122 + },
  123 + behavior: HitTestBehavior.opaque,
  124 + child: const SizedBox(
  125 + height: 100.0,
  126 + width: 100.0,
  127 + ),
  128 + ),
  129 + ],
  130 + );
  131 + },
  132 + ),
  133 + ),
  134 + ));
  135 +
  136 + expect(Get.isSnackbarOpen, false);
  137 + expect(find.text('bar1'), findsNothing);
  138 +
  139 + await tester.tap(find.byKey(snackBarTapTarget));
  140 + await tester.pumpAndSettle();
  141 +
  142 + expect(Get.isSnackbarOpen, true);
  143 + await tester.pump(const Duration(milliseconds: 500));
  144 + expect(find.byWidget(getBar), findsOneWidget);
  145 + await tester.ensureVisible(find.byWidget(getBar));
  146 + await tester.drag(find.byWidget(getBar), Offset(0.0, 50.0));
  147 + await tester.pump(const Duration(milliseconds: 500));
  148 +
  149 + expect(Get.isSnackbarOpen, false);
  150 + });
  151 +
  152 + testWidgets("test snackbar onTap", (tester) async {
  153 + const dismissDirection = DismissDirection.vertical;
  154 + const snackBarTapTarget = Key('snackbar-tap-target');
  155 + var counter = 0;
  156 +
  157 + late final GetSnackBar getBar;
  158 +
  159 + late final SnackbarController getBarController;
  160 +
  161 + await tester.pumpWidget(GetMaterialApp(
  162 + home: Scaffold(
  163 + body: Builder(
  164 + builder: (context) {
  165 + return Column(
  166 + children: <Widget>[
  167 + GestureDetector(
  168 + key: snackBarTapTarget,
  169 + onTap: () {
  170 + getBar = GetSnackBar(
  171 + message: 'bar1',
  172 + onTap: (_) {
  173 + counter++;
  174 + },
  175 + duration: const Duration(seconds: 2),
  176 + isDismissible: true,
  177 + dismissDirection: dismissDirection,
  178 + );
  179 + getBarController = Get.showSnackbar(getBar);
  180 + },
  181 + behavior: HitTestBehavior.opaque,
  182 + child: const SizedBox(
  183 + height: 100.0,
  184 + width: 100.0,
  185 + ),
  186 + ),
  187 + ],
  188 + );
  189 + },
  190 + ),
  191 + ),
  192 + ));
  193 +
  194 + await tester.pumpAndSettle();
  195 +
  196 + expect(Get.isSnackbarOpen, false);
  197 + expect(find.text('bar1'), findsNothing);
  198 +
  199 + await tester.tap(find.byKey(snackBarTapTarget));
  200 + await tester.pumpAndSettle();
  201 +
  202 + expect(Get.isSnackbarOpen, true);
  203 + await tester.pump(const Duration(milliseconds: 500));
  204 + expect(find.byWidget(getBar), findsOneWidget);
  205 + await tester.ensureVisible(find.byWidget(getBar));
  206 + await tester.tap(find.byWidget(getBar));
  207 + expect(counter, 1);
  208 + await tester.pump(const Duration(milliseconds: 3000));
  209 + await getBarController.close(withAnimations: false);
  210 + });
  211 +}