Jonny Borges
Committed by GitHub

Merge pull request #2012 from jonataslaw/snackbar-queue

Add snackbar queue, tests, and improve code
@@ -7,7 +7,7 @@ import 'routes/app_pages.dart'; @@ -7,7 +7,7 @@ import 'routes/app_pages.dart';
7 import 'shared/logger/logger_utils.dart'; 7 import 'shared/logger/logger_utils.dart';
8 8
9 void main() { 9 void main() {
10 - runApp(const MyApp()); 10 + runApp(MyApp());
11 } 11 }
12 12
13 class MyApp extends StatelessWidget { 13 class MyApp extends StatelessWidget {
@@ -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,
@@ -16,5 +16,5 @@ export 'src/routes/get_route.dart'; @@ -16,5 +16,5 @@ export 'src/routes/get_route.dart';
16 export 'src/routes/observers/route_observer.dart'; 16 export 'src/routes/observers/route_observer.dart';
17 export 'src/routes/route_middleware.dart'; 17 export 'src/routes/route_middleware.dart';
18 export 'src/routes/transitions_type.dart'; 18 export 'src/routes/transitions_type.dart';
19 -export 'src/snackbar/snack.dart'; 19 +export 'src/snackbar/snackbar.dart';
20 export 'src/snackbar/snackbar_controller.dart'; 20 export 'src/snackbar/snackbar_controller.dart';
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 - );  
63 - return _previousTitle!;  
64 - } 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,
  71 + );
  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();
65 78
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); 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 + }
  90 + }
  91 +
  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); 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!;
  282 + }
135 283
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;  
156 - }  
157 - // If we're in a gesture already, we cannot start another.  
158 - if (isPopGestureInProgress(route)) return false; 284 + bool get showCupertinoParallax;
159 285
160 - // Looks like a back gesture would be welcome!  
161 - return true;  
162 - } 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,
@@ -173,17 +312,36 @@ Cannot read the previousTitle for a route that has not yet been installed''', @@ -173,17 +312,36 @@ Cannot read the previousTitle for a route that has not yet been installed''',
173 return result; 312 return result;
174 } 313 }
175 314
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)); 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 + }
182 321
183 - return CupertinoBackGestureController<T>(  
184 - navigator: route.navigator!,  
185 - controller: route.controller!, // protected access  
186 - ); 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();  
619 - }  
620 -  
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; 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;
628 } 660 }
629 661
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; 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;
649 } 682 }
  683 + // If we're in a gesture already, we cannot start another.
  684 + if (isPopGestureInProgress(route)) return false;
650 685
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(); 686 + // Looks like a back gesture would be welcome!
  687 + return true;
  688 + }
667 689
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 }
@@ -8,6 +8,7 @@ import '../../../get_core/get_core.dart'; @@ -8,6 +8,7 @@ import '../../../get_core/get_core.dart';
8 import '../../get_navigation.dart'; 8 import '../../get_navigation.dart';
9 9
10 typedef OnTap = void Function(GetSnackBar snack); 10 typedef OnTap = void Function(GetSnackBar snack);
  11 +
11 typedef SnackbarStatusCallback = void Function(SnackbarStatus? status); 12 typedef SnackbarStatusCallback = void Function(SnackbarStatus? status);
12 13
13 class GetSnackBar extends StatefulWidget { 14 class GetSnackBar extends StatefulWidget {
@@ -17,6 +18,11 @@ class GetSnackBar extends StatefulWidget { @@ -17,6 +18,11 @@ class GetSnackBar extends StatefulWidget {
17 /// The title displayed to the user 18 /// The title displayed to the user
18 final String? title; 19 final String? title;
19 20
  21 + /// The direction in which the SnackBar can be dismissed.
  22 + ///
  23 + /// Cannot be null, defaults to [DismissDirection.down] when
  24 + /// [snackPosition] == [SnackPosition.BOTTOM] and [DismissDirection.up]
  25 + /// when [snackPosition] == [SnackPosition.TOP]
20 final DismissDirection? dismissDirection; 26 final DismissDirection? dismissDirection;
21 27
22 /// The message displayed to the user. 28 /// The message displayed to the user.
@@ -44,7 +50,8 @@ class GetSnackBar extends StatefulWidget { @@ -44,7 +50,8 @@ class GetSnackBar extends StatefulWidget {
44 /// Check (this example)[https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/material/shadows.dart] 50 /// Check (this example)[https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/material/shadows.dart]
45 final List<BoxShadow>? boxShadows; 51 final List<BoxShadow>? boxShadows;
46 52
47 - /// Makes [backgroundColor] be ignored. 53 + /// Give to GetSnackbar a gradient background.
  54 + /// It Makes [backgroundColor] be ignored.
48 final Gradient? backgroundGradient; 55 final Gradient? backgroundGradient;
49 56
50 /// You can use any widget here, but I recommend [Icon] or [Image] as 57 /// You can use any widget here, but I recommend [Icon] or [Image] as
@@ -55,7 +62,10 @@ class GetSnackBar extends StatefulWidget { @@ -55,7 +62,10 @@ class GetSnackBar extends StatefulWidget {
55 /// An option to animate the icon (if present). Defaults to true. 62 /// An option to animate the icon (if present). Defaults to true.
56 final bool shouldIconPulse; 63 final bool shouldIconPulse;
57 64
58 - /// A [TextButton] widget if you need an action from the user. 65 + /// (optional) An action that the user can take based on the snack bar.
  66 + ///
  67 + /// For example, the snack bar might let the user undo the operation that
  68 + /// prompted the snackbar.
59 final Widget? mainButton; 69 final Widget? mainButton;
60 70
61 /// A callback that registers the user's click anywhere. 71 /// A callback that registers the user's click anywhere.
@@ -135,7 +145,7 @@ class GetSnackBar extends StatefulWidget { @@ -135,7 +145,7 @@ class GetSnackBar extends StatefulWidget {
135 /// 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.
136 /// To take effect, make sure your [backgroundColor] has some opacity. 146 /// To take effect, make sure your [backgroundColor] has some opacity.
137 /// The greater the value, the greater the blur. 147 /// The greater the value, the greater the blur.
138 - final double? barBlur; 148 + final double barBlur;
139 149
140 /// 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
141 /// overlay that prevents the user from interacting with the screen. 151 /// overlay that prevents the user from interacting with the screen.
@@ -191,7 +201,7 @@ class GetSnackBar extends StatefulWidget { @@ -191,7 +201,7 @@ class GetSnackBar extends StatefulWidget {
191 }) : super(key: key); 201 }) : super(key: key);
192 202
193 @override 203 @override
194 - State createState() => _GetSnackBarState(); 204 + State createState() => GetSnackBarState();
195 205
196 /// Show the snack. It's call [SnackbarStatus.OPENING] state 206 /// Show the snack. It's call [SnackbarStatus.OPENING] state
197 /// followed by [SnackbarStatus.OPEN] 207 /// followed by [SnackbarStatus.OPEN]
@@ -200,23 +210,7 @@ class GetSnackBar extends StatefulWidget { @@ -200,23 +210,7 @@ class GetSnackBar extends StatefulWidget {
200 } 210 }
201 } 211 }
202 212
203 -/// Indicates Status of snackbar  
204 -/// [SnackbarStatus.OPEN] Snack is fully open, [SnackbarStatus.CLOSED] Snackbar  
205 -/// has closed,  
206 -/// [SnackbarStatus.OPENING] Starts with the opening animation and ends  
207 -/// with the full  
208 -/// snackbar display, [SnackbarStatus.CLOSING] Starts with the closing animation  
209 -/// and ends  
210 -/// with the full snackbar dispose  
211 -enum SnackbarStatus { OPEN, CLOSED, OPENING, CLOSING }  
212 -  
213 -/// Indicates if snack is going to start at the [TOP] or at the [BOTTOM]  
214 -enum SnackPosition { TOP, BOTTOM }  
215 -  
216 -/// Indicates if snack will be attached to the edge of the screen or not  
217 -enum SnackStyle { FLOATING, GROUNDED }  
218 -  
219 -class _GetSnackBarState extends State<GetSnackBar> 213 +class GetSnackBarState extends State<GetSnackBar>
220 with TickerProviderStateMixin { 214 with TickerProviderStateMixin {
221 AnimationController? _fadeController; 215 AnimationController? _fadeController;
222 late Animation<double> _fadeAnimation; 216 late Animation<double> _fadeAnimation;
@@ -235,12 +229,30 @@ class _GetSnackBarState extends State<GetSnackBar> @@ -235,12 +229,30 @@ class _GetSnackBarState extends State<GetSnackBar>
235 229
236 final Completer<Size> _boxHeightCompleter = Completer<Size>(); 230 final Completer<Size> _boxHeightCompleter = Completer<Size>();
237 231
238 - late VoidCallback _progressListener;  
239 -  
240 late CurvedAnimation _progressAnimation; 232 late CurvedAnimation _progressAnimation;
241 233
242 final _backgroundBoxKey = GlobalKey(); 234 final _backgroundBoxKey = GlobalKey();
243 235
  236 + double get buttonPadding {
  237 + if (widget.padding.right - 12 < 0) {
  238 + return 4;
  239 + } else {
  240 + return widget.padding.right - 12;
  241 + }
  242 + }
  243 +
  244 + RowStyle get _rowStyle {
  245 + if (widget.mainButton != null && widget.icon == null) {
  246 + return RowStyle.action;
  247 + } else if (widget.mainButton == null && widget.icon != null) {
  248 + return RowStyle.icon;
  249 + } else if (widget.mainButton != null && widget.icon != null) {
  250 + return RowStyle.all;
  251 + } else {
  252 + return RowStyle.none;
  253 + }
  254 + }
  255 +
244 @override 256 @override
245 Widget build(BuildContext context) { 257 Widget build(BuildContext context) {
246 return Align( 258 return Align(
@@ -258,7 +270,42 @@ class _GetSnackBarState extends State<GetSnackBar> @@ -258,7 +270,42 @@ class _GetSnackBarState extends State<GetSnackBar>
258 top: widget.snackPosition == SnackPosition.TOP, 270 top: widget.snackPosition == SnackPosition.TOP,
259 left: false, 271 left: false,
260 right: false, 272 right: false,
261 - child: _getSnack(), 273 + child: Stack(
  274 + children: [
  275 + FutureBuilder<Size>(
  276 + future: _boxHeightCompleter.future,
  277 + builder: (context, snapshot) {
  278 + if (snapshot.hasData) {
  279 + if (widget.barBlur == 0) {
  280 + return _emptyWidget;
  281 + }
  282 + return ClipRRect(
  283 + borderRadius: BorderRadius.circular(widget.borderRadius),
  284 + child: BackdropFilter(
  285 + filter: ImageFilter.blur(
  286 + sigmaX: widget.barBlur, sigmaY: widget.barBlur),
  287 + child: Container(
  288 + height: snapshot.data!.height,
  289 + width: snapshot.data!.width,
  290 + decoration: BoxDecoration(
  291 + color: Colors.transparent,
  292 + borderRadius:
  293 + BorderRadius.circular(widget.borderRadius),
  294 + ),
  295 + ),
  296 + ),
  297 + );
  298 + } else {
  299 + return _emptyWidget;
  300 + }
  301 + },
  302 + ),
  303 + if (widget.userInputForm != null)
  304 + _containerWithForm()
  305 + else
  306 + _containerWithoutForm()
  307 + ],
  308 + ),
262 ), 309 ),
263 ), 310 ),
264 ); 311 );
@@ -267,8 +314,7 @@ class _GetSnackBarState extends State<GetSnackBar> @@ -267,8 +314,7 @@ class _GetSnackBarState extends State<GetSnackBar>
267 @override 314 @override
268 void dispose() { 315 void dispose() {
269 _fadeController?.dispose(); 316 _fadeController?.dispose();
270 -  
271 - widget.progressIndicatorController?.removeListener(_progressListener); 317 + widget.progressIndicatorController?.removeListener(_updateProgress);
272 widget.progressIndicatorController?.dispose(); 318 widget.progressIndicatorController?.dispose();
273 319
274 _focusAttachment.detach(); 320 _focusAttachment.detach();
@@ -284,9 +330,8 @@ class _GetSnackBarState extends State<GetSnackBar> @@ -284,9 +330,8 @@ class _GetSnackBarState extends State<GetSnackBar>
284 widget.userInputForm != null || 330 widget.userInputForm != null ||
285 ((widget.message != null && widget.message!.isNotEmpty) || 331 ((widget.message != null && widget.message!.isNotEmpty) ||
286 widget.messageText != null), 332 widget.messageText != null),
287 - """  
288 -A message is mandatory if you are not using userInputForm.  
289 -Set either a message or messageText"""); 333 + '''
  334 +You need to either use message[String], or messageText[Widget] or define a userInputForm[Form] in GetSnackbar''');
290 335
291 _isTitlePresent = (widget.title != null || widget.titleText != null); 336 _isTitlePresent = (widget.title != null || widget.titleText != null);
292 _messageTopMargin = _isTitlePresent ? 6.0 : widget.padding.top; 337 _messageTopMargin = _isTitlePresent ? 6.0 : widget.padding.top;
@@ -339,10 +384,7 @@ Set either a message or messageText"""); @@ -339,10 +384,7 @@ Set either a message or messageText""");
339 void _configureProgressIndicatorAnimation() { 384 void _configureProgressIndicatorAnimation() {
340 if (widget.showProgressIndicator && 385 if (widget.showProgressIndicator &&
341 widget.progressIndicatorController != null) { 386 widget.progressIndicatorController != null) {
342 - _progressListener = () {  
343 - setState(() {});  
344 - };  
345 - widget.progressIndicatorController!.addListener(_progressListener); 387 + widget.progressIndicatorController!.addListener(_updateProgress);
346 388
347 _progressAnimation = CurvedAnimation( 389 _progressAnimation = CurvedAnimation(
348 curve: Curves.linear, parent: widget.progressIndicatorController!); 390 curve: Curves.linear, parent: widget.progressIndicatorController!);
@@ -371,7 +413,7 @@ Set either a message or messageText"""); @@ -371,7 +413,7 @@ Set either a message or messageText""");
371 _fadeController!.forward(); 413 _fadeController!.forward();
372 } 414 }
373 415
374 - Widget _generateInputSnack() { 416 + Widget _containerWithForm() {
375 return Container( 417 return Container(
376 key: _backgroundBoxKey, 418 key: _backgroundBoxKey,
377 constraints: widget.maxWidth != null 419 constraints: widget.maxWidth != null
@@ -383,7 +425,10 @@ Set either a message or messageText"""); @@ -383,7 +425,10 @@ Set either a message or messageText""");
383 boxShadow: widget.boxShadows, 425 boxShadow: widget.boxShadows,
384 borderRadius: BorderRadius.circular(widget.borderRadius), 426 borderRadius: BorderRadius.circular(widget.borderRadius),
385 border: widget.borderColor != null 427 border: widget.borderColor != null
386 - ? Border.all(color: widget.borderColor!, width: widget.borderWidth!) 428 + ? Border.all(
  429 + color: widget.borderColor!,
  430 + width: widget.borderWidth!,
  431 + )
387 : null, 432 : null,
388 ), 433 ),
389 child: Padding( 434 child: Padding(
@@ -398,7 +443,14 @@ Set either a message or messageText"""); @@ -398,7 +443,14 @@ Set either a message or messageText""");
398 ); 443 );
399 } 444 }
400 445
401 - Widget _generateSnack() { 446 + Widget _containerWithoutForm() {
  447 + final iconPadding = widget.padding.left > 16.0 ? widget.padding.left : 0.0;
  448 + final left = _rowStyle == RowStyle.icon || _rowStyle == RowStyle.all
  449 + ? 4.0
  450 + : widget.padding.left;
  451 + final right = _rowStyle == RowStyle.action || _rowStyle == RowStyle.all
  452 + ? 8.0
  453 + : widget.padding.right;
402 return Container( 454 return Container(
403 key: _backgroundBoxKey, 455 key: _backgroundBoxKey,
404 constraints: widget.maxWidth != null 456 constraints: widget.maxWidth != null
@@ -427,177 +479,65 @@ Set either a message or messageText"""); @@ -427,177 +479,65 @@ Set either a message or messageText""");
427 : _emptyWidget, 479 : _emptyWidget,
428 Row( 480 Row(
429 mainAxisSize: MainAxisSize.max, 481 mainAxisSize: MainAxisSize.max,
430 - children: _getAppropriateRowLayout(),  
431 - ),  
432 - ],  
433 - ),  
434 - );  
435 - }  
436 -  
437 - List<Widget> _getAppropriateRowLayout() {  
438 - double buttonRightPadding;  
439 - var iconPadding = 0.0;  
440 - if (widget.padding.right - 12 < 0) {  
441 - buttonRightPadding = 4;  
442 - } else {  
443 - buttonRightPadding = widget.padding.right - 12;  
444 - }  
445 -  
446 - if (widget.padding.left > 16.0) {  
447 - iconPadding = widget.padding.left;  
448 - }  
449 -  
450 - if (widget.icon == null && widget.mainButton == null) {  
451 - return [  
452 - _buildLeftBarIndicator(),  
453 - Expanded(  
454 - flex: 1,  
455 - child: Column(  
456 - crossAxisAlignment: CrossAxisAlignment.stretch,  
457 - mainAxisSize: MainAxisSize.min,  
458 - children: <Widget>[  
459 - (_isTitlePresent)  
460 - ? Padding(  
461 - padding: EdgeInsets.only(  
462 - top: widget.padding.top,  
463 - left: widget.padding.left,  
464 - right: widget.padding.right,  
465 - ),  
466 - child: _getTitleText(),  
467 - )  
468 - : _emptyWidget,  
469 - Padding(  
470 - padding: EdgeInsets.only(  
471 - top: _messageTopMargin,  
472 - left: widget.padding.left,  
473 - right: widget.padding.right,  
474 - bottom: widget.padding.bottom, 482 + children: [
  483 + _buildLeftBarIndicator(),
  484 + if (_rowStyle == RowStyle.icon)
  485 + ConstrainedBox(
  486 + constraints:
  487 + BoxConstraints.tightFor(width: 42.0 + iconPadding),
  488 + child: _getIcon(),
475 ), 489 ),
476 - child: widget.messageText ?? _getDefaultNotificationText(),  
477 - ),  
478 - ],  
479 - ),  
480 - ),  
481 - ];  
482 - } else if (widget.icon != null && widget.mainButton == null) {  
483 - return <Widget>[  
484 - _buildLeftBarIndicator(),  
485 - ConstrainedBox(  
486 - constraints: BoxConstraints.tightFor(width: 42.0 + iconPadding),  
487 - child: _getIcon(),  
488 - ),  
489 - Expanded(  
490 - flex: 1,  
491 - child: Column(  
492 - crossAxisAlignment: CrossAxisAlignment.stretch,  
493 - mainAxisSize: MainAxisSize.min,  
494 - children: <Widget>[  
495 - (_isTitlePresent)  
496 - ? Padding( 490 + Expanded(
  491 + flex: 1,
  492 + child: Column(
  493 + crossAxisAlignment: CrossAxisAlignment.stretch,
  494 + mainAxisSize: MainAxisSize.min,
  495 + children: <Widget>[
  496 + if (_isTitlePresent)
  497 + Padding(
  498 + padding: EdgeInsets.only(
  499 + top: widget.padding.top,
  500 + left: left,
  501 + right: right,
  502 + ),
  503 + child: widget.titleText ??
  504 + Text(
  505 + widget.title ?? "",
  506 + style: TextStyle(
  507 + fontSize: 16.0,
  508 + color: Colors.white,
  509 + fontWeight: FontWeight.bold,
  510 + ),
  511 + ),
  512 + )
  513 + else
  514 + _emptyWidget,
  515 + Padding(
497 padding: EdgeInsets.only( 516 padding: EdgeInsets.only(
498 - top: widget.padding.top,  
499 - left: 4.0,  
500 - right: widget.padding.left, 517 + top: _messageTopMargin,
  518 + left: left,
  519 + right: right,
  520 + bottom: widget.padding.bottom,
501 ), 521 ),
502 - child: _getTitleText(),  
503 - )  
504 - : _emptyWidget,  
505 - Padding(  
506 - padding: EdgeInsets.only(  
507 - top: _messageTopMargin,  
508 - left: 4.0,  
509 - right: widget.padding.right,  
510 - bottom: widget.padding.bottom,  
511 - ),  
512 - child: widget.messageText ?? _getDefaultNotificationText(),  
513 - ),  
514 - ],  
515 - ),  
516 - ),  
517 - ];  
518 - } else if (widget.icon == null && widget.mainButton != null) {  
519 - return <Widget>[  
520 - _buildLeftBarIndicator(),  
521 - Expanded(  
522 - flex: 1,  
523 - child: Column(  
524 - crossAxisAlignment: CrossAxisAlignment.stretch,  
525 - mainAxisSize: MainAxisSize.min,  
526 - children: <Widget>[  
527 - (_isTitlePresent)  
528 - ? Padding(  
529 - padding: EdgeInsets.only(  
530 - top: widget.padding.top,  
531 - left: widget.padding.left,  
532 - right: widget.padding.right,  
533 - ),  
534 - child: _getTitleText(),  
535 - )  
536 - : _emptyWidget,  
537 - Padding(  
538 - padding: EdgeInsets.only(  
539 - top: _messageTopMargin,  
540 - left: widget.padding.left,  
541 - right: 8.0,  
542 - bottom: widget.padding.bottom, 522 + child: widget.messageText ??
  523 + Text(
  524 + widget.message ?? "",
  525 + style:
  526 + TextStyle(fontSize: 14.0, color: Colors.white),
  527 + ),
  528 + ),
  529 + ],
543 ), 530 ),
544 - child: widget.messageText ?? _getDefaultNotificationText(),  
545 ), 531 ),
546 - ],  
547 - ),  
548 - ),  
549 - Padding(  
550 - padding: EdgeInsets.only(right: buttonRightPadding),  
551 - child: _getMainActionButton(),  
552 - ),  
553 - ];  
554 - } else {  
555 - return <Widget>[  
556 - _buildLeftBarIndicator(),  
557 - ConstrainedBox(  
558 - constraints: BoxConstraints.tightFor(width: 42.0 + iconPadding),  
559 - child: _getIcon(),  
560 - ),  
561 - Expanded(  
562 - flex: 1,  
563 - child: Column(  
564 - crossAxisAlignment: CrossAxisAlignment.stretch,  
565 - mainAxisSize: MainAxisSize.min,  
566 - children: <Widget>[  
567 - (_isTitlePresent)  
568 - ? Padding(  
569 - padding: EdgeInsets.only(  
570 - top: widget.padding.top,  
571 - left: 4.0,  
572 - right: 8.0,  
573 - ),  
574 - child: _getTitleText(),  
575 - )  
576 - : _emptyWidget,  
577 - Padding(  
578 - padding: EdgeInsets.only(  
579 - top: _messageTopMargin,  
580 - left: 4.0,  
581 - right: 8.0,  
582 - bottom: widget.padding.bottom, 532 + if (_rowStyle == RowStyle.action)
  533 + Padding(
  534 + padding: EdgeInsets.only(right: buttonPadding),
  535 + child: widget.mainButton,
583 ), 536 ),
584 - child: widget.messageText ?? _getDefaultNotificationText(),  
585 - ),  
586 ], 537 ],
587 ), 538 ),
588 - ),  
589 - Padding(  
590 - padding: EdgeInsets.only(right: buttonRightPadding),  
591 - child: _getMainActionButton(),  
592 - ),  
593 - ];  
594 - }  
595 - }  
596 -  
597 - Text _getDefaultNotificationText() {  
598 - return Text(  
599 - widget.message ?? "",  
600 - style: TextStyle(fontSize: 14.0, color: Colors.white), 539 + ],
  540 + ),
601 ); 541 );
602 } 542 }
603 543
@@ -614,59 +554,28 @@ Set either a message or messageText"""); @@ -614,59 +554,28 @@ Set either a message or messageText""");
614 } 554 }
615 } 555 }
616 556
617 - Widget? _getMainActionButton() {  
618 - return widget.mainButton;  
619 - } 557 + void _updateProgress() => setState(() {});
  558 +}
620 559
621 - Widget _getSnack() {  
622 - Widget snack; 560 +enum RowStyle {
  561 + icon,
  562 + action,
  563 + all,
  564 + none,
  565 +}
623 566
624 - if (widget.userInputForm != null) {  
625 - snack = _generateInputSnack();  
626 - } else {  
627 - snack = _generateSnack();  
628 - } 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 }
629 576
630 - return Stack(  
631 - children: [  
632 - FutureBuilder<Size>(  
633 - future: _boxHeightCompleter.future,  
634 - builder: (context, snapshot) {  
635 - if (snapshot.hasData) {  
636 - if (widget.barBlur == 0) {  
637 - return _emptyWidget;  
638 - }  
639 - return ClipRRect(  
640 - borderRadius: BorderRadius.circular(widget.borderRadius),  
641 - child: BackdropFilter(  
642 - filter: ImageFilter.blur(  
643 - sigmaX: widget.barBlur!, sigmaY: widget.barBlur!),  
644 - child: Container(  
645 - height: snapshot.data!.height,  
646 - width: snapshot.data!.width,  
647 - decoration: BoxDecoration(  
648 - color: Colors.transparent,  
649 - borderRadius: BorderRadius.circular(widget.borderRadius),  
650 - ),  
651 - ),  
652 - ),  
653 - );  
654 - } else {  
655 - return _emptyWidget;  
656 - }  
657 - },  
658 - ),  
659 - snack,  
660 - ],  
661 - );  
662 - } 577 +/// Indicates if snack is going to start at the [TOP] or at the [BOTTOM]
  578 +enum SnackPosition { TOP, BOTTOM }
663 579
664 - Widget _getTitleText() {  
665 - return widget.titleText ??  
666 - Text(  
667 - widget.title ?? "",  
668 - style: TextStyle(  
669 - fontSize: 16.0, color: Colors.white, fontWeight: FontWeight.bold),  
670 - );  
671 - }  
672 -} 580 +/// Indicates if snack will be attached to the edge of the screen or not
  581 +enum SnackStyle { FLOATING, GROUNDED }
@@ -7,13 +7,14 @@ import '../../../get.dart'; @@ -7,13 +7,14 @@ import '../../../get.dart';
7 7
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 }
@@ -54,7 +59,7 @@ class SnackbarController { @@ -54,7 +59,7 @@ class SnackbarController {
54 /// Only one GetSnackbar will be displayed at a time, and this method returns 59 /// Only one GetSnackbar will be displayed at a time, and this method returns
55 /// a future to when the snackbar disappears. 60 /// a future to when the snackbar disappears.
56 Future<void> show() { 61 Future<void> show() {
57 - return _snackBarQueue.addJob(this); 62 + return _snackBarQueue._addJob(this);
58 } 63 }
59 64
60 void _cancelTimer() { 65 void _cancelTimer() {
@@ -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();
@@ -319,10 +318,11 @@ class SnackbarController { @@ -319,10 +318,11 @@ class SnackbarController {
319 element.remove(); 318 element.remove();
320 } 319 }
321 320
322 - assert(!_transitionCompleter.isCompleted, 'Cannot remove overlay twice.'); 321 + assert(!_transitionCompleter.isCompleted,
  322 + 'Cannot remove overlay from a disposed snackbar');
323 _controller.dispose(); 323 _controller.dispose();
324 _overlayEntries.clear(); 324 _overlayEntries.clear();
325 - _transitionCompleter.complete(this); 325 + _transitionCompleter.complete();
326 } 326 }
327 327
328 Future<void> _show() { 328 Future<void> _show() {
@@ -331,38 +331,40 @@ class SnackbarController { @@ -331,38 +331,40 @@ class SnackbarController {
331 } 331 }
332 332
333 static void cancelAllSnackbars() { 333 static void cancelAllSnackbars() {
334 - _snackBarQueue.cancelAllJobs(); 334 + _snackBarQueue._cancelAllJobs();
335 } 335 }
336 336
337 static Future<void> closeCurrentSnackbar() async { 337 static Future<void> closeCurrentSnackbar() async {
338 - await _snackBarQueue.closeCurrentJob(); 338 + await _snackBarQueue._closeCurrentJob();
339 } 339 }
340 } 340 }
341 341
342 class _SnackBarQueue { 342 class _SnackBarQueue {
343 final _queue = GetQueue(); 343 final _queue = GetQueue();
  344 + final _snackbarList = <SnackbarController>[];
344 345
345 - bool _isJobInProgress = false;  
346 -  
347 - SnackbarController? _currentSnackbar; 346 + SnackbarController? get _currentSnackbar {
  347 + if (_snackbarList.isEmpty) return null;
  348 + return _snackbarList.first;
  349 + }
348 350
349 - bool get isJobInProgress => _isJobInProgress; 351 + bool get _isJobInProgress => _snackbarList.isNotEmpty;
350 352
351 - Future<void> addJob(SnackbarController job) async {  
352 - _isJobInProgress = true;  
353 - _currentSnackbar = job; 353 + Future<void> _addJob(SnackbarController job) async {
  354 + _snackbarList.add(job);
354 final data = await _queue.add(job._show); 355 final data = await _queue.add(job._show);
355 - _isJobInProgress = false;  
356 - _currentSnackbar = null; 356 + _snackbarList.remove(job);
357 return data; 357 return data;
358 } 358 }
359 359
360 - void cancelAllJobs() {  
361 - _currentSnackbar?.close(); 360 + Future<void> _cancelAllJobs() async {
  361 + await _currentSnackbar?.close();
362 _queue.cancelAllJobs(); 362 _queue.cancelAllJobs();
  363 + _snackbarList.clear();
363 } 364 }
364 365
365 - Future<void> closeCurrentJob() async {  
366 - await _currentSnackbar?.close(); 366 + Future<void> _closeCurrentJob() async {
  367 + if (_currentSnackbar == null) return;
  368 + await _currentSnackbar!.close();
367 } 369 }
368 } 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 +}