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