Committed by
GitHub
Merge pull request #2012 from jonataslaw/snackbar-queue
Add snackbar queue, tests, and improve code
Showing
13 changed files
with
1006 additions
and
714 deletions
| @@ -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 | } | 
test/navigation/snackbar_test.dart
0 → 100644
| 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 | +} | 
- 
Please register or login to post a comment