Jonny Borges
Committed by GitHub

Merge pull request #2012 from jonataslaw/snackbar-queue

Add snackbar queue, tests, and improve code
... ... @@ -7,7 +7,7 @@ import 'routes/app_pages.dart';
import 'shared/logger/logger_utils.dart';
void main() {
runApp(const MyApp());
runApp(MyApp());
}
class MyApp extends StatelessWidget {
... ...
... ... @@ -21,6 +21,12 @@ class HomeView extends GetView<HomeController> {
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.add),
onPressed: () {
Get.snackbar('title', 'message');
},
),
title: Text('covid'.tr),
backgroundColor: Colors.white10,
elevation: 0,
... ...
... ... @@ -16,5 +16,5 @@ export 'src/routes/get_route.dart';
export 'src/routes/observers/route_observer.dart';
export 'src/routes/route_middleware.dart';
export 'src/routes/transitions_type.dart';
export 'src/snackbar/snack.dart';
export 'src/snackbar/snackbar.dart';
export 'src/snackbar/snackbar_controller.dart';
... ...
import 'dart:ui' as ui;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
... ... @@ -276,7 +277,7 @@ extension ExtensionDialog on GetInterface {
}
extension ExtensionSnackbar on GetInterface {
void rawSnackbar({
SnackbarController rawSnackbar({
String? title,
String? message,
Widget? titleText,
... ... @@ -296,7 +297,7 @@ extension ExtensionSnackbar on GetInterface {
Gradient? backgroundGradient,
Widget? mainButton,
OnTap? onTap,
Duration duration = const Duration(seconds: 3),
Duration? duration = const Duration(seconds: 3),
bool isDismissible = true,
DismissDirection? dismissDirection,
bool showProgressIndicator = false,
... ... @@ -309,12 +310,12 @@ extension ExtensionSnackbar on GetInterface {
Curve reverseAnimationCurve = Curves.easeOutCirc,
Duration animationDuration = const Duration(seconds: 1),
SnackbarStatusCallback? snackbarStatus,
double? barBlur = 0.0,
double barBlur = 0.0,
double overlayBlur = 0.0,
Color? overlayColor,
Form? userInputForm,
}) async {
final getBar = GetSnackBar(
}) {
final getSnackBar = GetSnackBar(
snackbarStatus: snackbarStatus,
title: title,
message: message,
... ... @@ -352,13 +353,16 @@ extension ExtensionSnackbar on GetInterface {
userInputForm: userInputForm,
);
final controller = SnackbarController(getSnackBar);
if (instantInit) {
getBar.show();
controller.show();
} else {
SchedulerBinding.instance!.addPostFrameCallback((_) {
getBar.show();
controller.show();
});
}
return controller;
}
SnackbarController showSnackbar(GetSnackBar snackbar) {
... ... @@ -371,7 +375,7 @@ extension ExtensionSnackbar on GetInterface {
String title,
String message, {
Color? colorText,
Duration? duration,
Duration? duration = const Duration(seconds: 3),
/// with instantInit = false you can put snackbar on initState
bool instantInit = true,
... ... @@ -431,7 +435,7 @@ extension ExtensionSnackbar on GetInterface {
snackPosition: snackPosition ?? SnackPosition.TOP,
borderRadius: borderRadius ?? 15,
margin: margin ?? EdgeInsets.symmetric(horizontal: 10),
duration: duration ?? Duration(seconds: 3),
duration: duration,
barBlur: barBlur ?? 7.0,
backgroundColor: backgroundColor ?? Colors.grey.withOpacity(0.2),
icon: icon,
... ... @@ -517,6 +521,7 @@ extension GetNavigation on GetInterface {
routeName ??= "/${page.runtimeType}";
routeName = _cleanRouteName(routeName);
if (preventDuplicates && routeName == currentRoute) {
CupertinoPageRoute ds;
return null;
}
return global(id).currentState?.push<T>(
... ...
... ... @@ -5,111 +5,247 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import '../../../get.dart';
import '../../../get.dart';
import 'default_transitions.dart';
import 'transitions_type.dart';
const double _kBackGestureWidth = 20.0;
const double _kMinFlingVelocity = 1.0; // Screen widths per second.
const int _kMaxDroppedSwipePageForwardAnimationTime =
800; // Screen widths per second.
// An eyeballed value for the maximum time it takes
//for a page to animate forward
// if the user releases a page mid swipe.
const int _kMaxDroppedSwipePageForwardAnimationTime = 800; // Milliseconds.
const int _kMaxPageBackAnimationTime = 300; // Milliseconds.
// The maximum time for a page to get reset to it's original position if the
// user releases a page mid swipe.
const int _kMaxPageBackAnimationTime = 300; // Milliseconds.
const double _kMinFlingVelocity = 1.0; // Milliseconds.
mixin GetPageRouteTransitionMixin<T> on PageRoute<T> {
/// Builds the primary contents of the route.
@protected
Widget buildContent(BuildContext context);
class CupertinoBackGestureController<T> {
final AnimationController controller;
/// {@template flutter.cupertino.CupertinoRouteTransitionMixin.title}
/// A title string for this route.
final NavigatorState navigator;
/// Creates a controller for an iOS-style back gesture.
///
/// Used to auto-populate [CupertinoNavigationBar] and
/// [CupertinoSliverNavigationBar]'s `middle`/`largeTitle` widgets when
/// one is not manually supplied.
/// {@endtemplate}
String? get title;
/// The [navigator] and [controller] arguments must not be null.
CupertinoBackGestureController({
required this.navigator,
required this.controller,
}) {
navigator.didStartUserGesture();
}
double Function(BuildContext context)? get gestureWidth;
/// The drag gesture has ended with a horizontal motion of
/// [fractionalVelocity] as a fraction of screen width per second.
void dragEnd(double velocity) {
// Fling in the appropriate direction.
// AnimationController.fling is guaranteed to
// take at least one frame.
//
// This curve has been determined through rigorously eyeballing native iOS
// animations.
const Curve animationCurve = Curves.fastLinearToSlowEaseIn;
final bool animateForward;
ValueNotifier<String?>? _previousTitle;
// If the user releases the page before mid screen with sufficient velocity,
// or after mid screen, we should animate the page out. Otherwise, the page
// should be animated back in.
if (velocity.abs() >= _kMinFlingVelocity) {
animateForward = velocity <= 0;
} else {
animateForward = controller.value > 0.5;
}
/// The title string of the previous [CupertinoPageRoute].
///
/// The [ValueListenable]'s value is readable after the route is installed
/// onto a [Navigator]. The [ValueListenable] will also notify its listeners
/// if the value changes (such as by replacing the previous route).
///
/// The [ValueListenable] itself will be null before the route is installed.
/// Its content value will be null if the previous route has no title or
/// is not a [CupertinoPageRoute].
///
/// See also:
///
/// * [ValueListenableBuilder], which can be used to listen and rebuild
/// widgets based on a ValueListenable.
ValueListenable<String?> get previousTitle {
assert(
_previousTitle != null,
'''
Cannot read the previousTitle for a route that has not yet been installed''',
if (animateForward) {
// The closer the panel is to dismissing, the shorter the animation is.
// We want to cap the animation time, but we want to use a linear curve
// to determine it.
final droppedPageForwardAnimationTime = min(
lerpDouble(
_kMaxDroppedSwipePageForwardAnimationTime, 0, controller.value)!
.floor(),
_kMaxPageBackAnimationTime,
);
return _previousTitle!;
controller.animateTo(1.0,
duration: Duration(milliseconds: droppedPageForwardAnimationTime),
curve: animationCurve);
} else {
// This route is destined to pop at this point. Reuse navigator's pop.
navigator.pop();
// The popping may have finished inline if already at the
// target destination.
if (controller.isAnimating) {
// Otherwise, use a custom popping animation duration and curve.
final droppedPageBackAnimationTime = lerpDouble(
0, _kMaxDroppedSwipePageForwardAnimationTime, controller.value)!
.floor();
controller.animateBack(0.0,
duration: Duration(milliseconds: droppedPageBackAnimationTime),
curve: animationCurve);
}
}
@override
void didChangePrevious(Route<dynamic>? previousRoute) {
final previousTitleString = previousRoute is CupertinoRouteTransitionMixin
? previousRoute.title
: null;
if (_previousTitle == null) {
_previousTitle = ValueNotifier<String?>(previousTitleString);
if (controller.isAnimating) {
// Keep the userGestureInProgress in true state so we don't change the
// curve of the page transition mid-flight since CupertinoPageTransition
// depends on userGestureInProgress.
late AnimationStatusListener animationStatusCallback;
animationStatusCallback = (status) {
navigator.didStopUserGesture();
controller.removeStatusListener(animationStatusCallback);
};
controller.addStatusListener(animationStatusCallback);
} else {
_previousTitle!.value = previousTitleString;
navigator.didStopUserGesture();
}
super.didChangePrevious(previousRoute);
}
@override
// A relatively rigorous eyeball estimation.
Duration get transitionDuration => const Duration(milliseconds: 400);
/// The drag gesture has changed by [fractionalDelta]. The total range of the
/// drag should be 0.0 to 1.0.
void dragUpdate(double delta) {
controller.value -= delta;
}
}
class CupertinoBackGestureDetector<T> extends StatefulWidget {
final Widget child;
final double gestureWidth;
final ValueGetter<bool> enabledCallback;
final ValueGetter<CupertinoBackGestureController<T>> onStartPopGesture;
const CupertinoBackGestureDetector({
Key? key,
required this.enabledCallback,
required this.onStartPopGesture,
required this.child,
required this.gestureWidth,
}) : super(key: key);
@override
Color? get barrierColor => null;
CupertinoBackGestureDetectorState<T> createState() =>
CupertinoBackGestureDetectorState<T>();
}
class CupertinoBackGestureDetectorState<T>
extends State<CupertinoBackGestureDetector<T>> {
CupertinoBackGestureController<T>? _backGestureController;
late HorizontalDragGestureRecognizer _recognizer;
@override
String? get barrierLabel => null;
Widget build(BuildContext context) {
assert(debugCheckHasDirectionality(context));
// For devices with notches, the drag area needs to be larger on the side
// that has the notch.
var dragAreaWidth = Directionality.of(context) == TextDirection.ltr
? MediaQuery.of(context).padding.left
: MediaQuery.of(context).padding.right;
dragAreaWidth = max(dragAreaWidth, widget.gestureWidth);
return Stack(
fit: StackFit.passthrough,
children: <Widget>[
widget.child,
PositionedDirectional(
start: 0.0,
width: dragAreaWidth,
top: 0.0,
bottom: 0.0,
child: Listener(
onPointerDown: _handlePointerDown,
behavior: HitTestBehavior.translucent,
),
),
],
);
}
bool get showCupertinoParallax;
@override
void dispose() {
_recognizer.dispose();
super.dispose();
}
@override
bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {
// Don't perform outgoing animation if the next route is a
// fullscreen dialog.
void initState() {
super.initState();
_recognizer = HorizontalDragGestureRecognizer(debugOwner: this)
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd
..onCancel = _handleDragCancel;
}
return nextRoute is GetPageRouteTransitionMixin &&
!nextRoute.fullscreenDialog &&
nextRoute.showCupertinoParallax;
double _convertToLogical(double value) {
switch (Directionality.of(context)) {
case TextDirection.rtl:
return -value;
case TextDirection.ltr:
return value;
}
}
/// True if an iOS-style back swipe pop gesture is currently
/// underway for [route].
void _handleDragCancel() {
assert(mounted);
// This can be called even if start is not called, paired with
// the "down" event
// that we don't consider here.
_backGestureController?.dragEnd(0.0);
_backGestureController = null;
}
void _handleDragEnd(DragEndDetails details) {
assert(mounted);
assert(_backGestureController != null);
_backGestureController!.dragEnd(_convertToLogical(
details.velocity.pixelsPerSecond.dx / context.size!.width));
_backGestureController = null;
}
void _handleDragStart(DragStartDetails details) {
assert(mounted);
assert(_backGestureController == null);
_backGestureController = widget.onStartPopGesture();
}
void _handleDragUpdate(DragUpdateDetails details) {
assert(mounted);
assert(_backGestureController != null);
_backGestureController!.dragUpdate(
_convertToLogical(details.primaryDelta! / context.size!.width));
}
void _handlePointerDown(PointerDownEvent event) {
if (widget.enabledCallback()) _recognizer.addPointer(event);
}
}
mixin GetPageRouteTransitionMixin<T> on PageRoute<T> {
ValueNotifier<String?>? _previousTitle;
@override
Color? get barrierColor => null;
@override
String? get barrierLabel => null;
double Function(BuildContext context)? get gestureWidth;
/// Whether a pop gesture can be started by the user.
///
/// This just check the route's [NavigatorState.userGestureInProgress].
/// Returns true if the user can edge-swipe to a previous route.
///
/// See also:
/// Returns false once [isPopGestureInProgress] is true, but
/// [isPopGestureInProgress] can only become true if [popGestureEnabled] was
/// true first.
///
/// * [popGestureEnabled], which returns true if a user-triggered pop gesture
/// would be allowed.
static bool isPopGestureInProgress(PageRoute<dynamic> route) {
return route.navigator!.userGestureInProgress;
}
/// This should only be used between frames, not during build.
bool get popGestureEnabled => _isPopGestureEnabled(this);
/// True if an iOS-style back swipe pop gesture is currently
/// underway for this route.
... ... @@ -122,44 +258,47 @@ Cannot read the previousTitle for a route that has not yet been installed''',
/// would be allowed.
bool get popGestureInProgress => isPopGestureInProgress(this);
/// Whether a pop gesture can be started by the user.
/// The title string of the previous [CupertinoPageRoute].
///
/// Returns true if the user can edge-swipe to a previous route.
/// The [ValueListenable]'s value is readable after the route is installed
/// onto a [Navigator]. The [ValueListenable] will also notify its listeners
/// if the value changes (such as by replacing the previous route).
///
/// Returns false once [isPopGestureInProgress] is true, but
/// [isPopGestureInProgress] can only become true if [popGestureEnabled] was
/// true first.
/// The [ValueListenable] itself will be null before the route is installed.
/// Its content value will be null if the previous route has no title or
/// is not a [CupertinoPageRoute].
///
/// This should only be used between frames, not during build.
bool get popGestureEnabled => _isPopGestureEnabled(this);
static bool _isPopGestureEnabled<T>(PageRoute<T> route) {
// If there's nothing to go back to, then obviously we don't support
// the back gesture.
if (route.isFirst) return false;
// If the route wouldn't actually pop if we popped it, then the gesture
// would be really confusing (or would skip internal routes),
//so disallow it.
if (route.willHandlePopInternally) return false;
// If attempts to dismiss this route might be vetoed such as in a page
// with forms, then do not allow the user to dismiss the route with a swipe.
if (route.hasScopedWillPopCallback) return false;
// Fullscreen dialogs aren't dismissible by back swipe.
if (route.fullscreenDialog) return false;
// If we're in an animation already, we cannot be manually swiped.
if (route.animation!.status != AnimationStatus.completed) return false;
// If we're being popped into, we also cannot be swiped until the pop above
// it completes. This translates to our secondary animation being
// dismissed.
if (route.secondaryAnimation!.status != AnimationStatus.dismissed) {
return false;
/// See also:
///
/// * [ValueListenableBuilder], which can be used to listen and rebuild
/// widgets based on a ValueListenable.
ValueListenable<String?> get previousTitle {
assert(
_previousTitle != null,
'''
Cannot read the previousTitle for a route that has not yet been installed''',
);
return _previousTitle!;
}
// If we're in a gesture already, we cannot start another.
if (isPopGestureInProgress(route)) return false;
// Looks like a back gesture would be welcome!
return true;
}
bool get showCupertinoParallax;
/// {@template flutter.cupertino.CupertinoRouteTransitionMixin.title}
/// A title string for this route.
///
/// Used to auto-populate [CupertinoNavigationBar] and
/// [CupertinoSliverNavigationBar]'s `middle`/`largeTitle` widgets when
/// one is not manually supplied.
/// {@endtemplate}
String? get title;
@override
// A relatively rigorous eyeball estimation.
Duration get transitionDuration => const Duration(milliseconds: 400);
/// Builds the primary contents of the route.
@protected
Widget buildContent(BuildContext context);
@override
Widget buildPage(BuildContext context, Animation<double> animation,
... ... @@ -172,18 +311,37 @@ Cannot read the previousTitle for a route that has not yet been installed''',
);
return result;
}
// Called by CupertinoBackGestureDetector when a pop ("back") drag start
// gesture is detected. The returned controller handles all of the subsequent
// drag events.
static CupertinoBackGestureController<T> _startPopGesture<T>(
PageRoute<T> route) {
assert(_isPopGestureEnabled(route));
return CupertinoBackGestureController<T>(
navigator: route.navigator!,
controller: route.controller!, // protected access
);
@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return buildPageTransitions<T>(
this, context, animation, secondaryAnimation, child);
}
@override
bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {
// Don't perform outgoing animation if the next route is a
// fullscreen dialog.
return (nextRoute is GetPageRouteTransitionMixin &&
!nextRoute.fullscreenDialog &&
nextRoute.showCupertinoParallax) ||
(nextRoute is CupertinoRouteTransitionMixin &&
!nextRoute.fullscreenDialog);
}
@override
void didChangePrevious(Route<dynamic>? previousRoute) {
final previousTitleString = previousRoute is CupertinoRouteTransitionMixin
? previousRoute.title
: null;
if (_previousTitle == null) {
_previousTitle = ValueNotifier<String?>(previousTitleString);
} else {
_previousTitle!.value = previousTitleString;
}
super.didChangePrevious(previousRoute);
}
/// 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''',
}
}
@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return buildPageTransitions<T>(
this, context, animation, secondaryAnimation, child);
}
}
class CupertinoBackGestureDetector<T> extends StatefulWidget {
const CupertinoBackGestureDetector({
Key? key,
required this.enabledCallback,
required this.onStartPopGesture,
required this.child,
required this.gestureWidth,
}) : super(key: key);
final Widget child;
final double gestureWidth;
final ValueGetter<bool> enabledCallback;
final ValueGetter<CupertinoBackGestureController<T>> onStartPopGesture;
@override
CupertinoBackGestureDetectorState<T> createState() =>
CupertinoBackGestureDetectorState<T>();
}
class CupertinoBackGestureDetectorState<T>
extends State<CupertinoBackGestureDetector<T>> {
CupertinoBackGestureController<T>? _backGestureController;
late HorizontalDragGestureRecognizer _recognizer;
@override
void initState() {
super.initState();
_recognizer = HorizontalDragGestureRecognizer(debugOwner: this)
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd
..onCancel = _handleDragCancel;
}
@override
void dispose() {
_recognizer.dispose();
super.dispose();
}
void _handleDragStart(DragStartDetails details) {
assert(mounted);
assert(_backGestureController == null);
_backGestureController = widget.onStartPopGesture();
}
void _handleDragUpdate(DragUpdateDetails details) {
assert(mounted);
assert(_backGestureController != null);
_backGestureController!.dragUpdate(
_convertToLogical(details.primaryDelta! / context.size!.width));
}
void _handleDragEnd(DragEndDetails details) {
assert(mounted);
assert(_backGestureController != null);
_backGestureController!.dragEnd(_convertToLogical(
details.velocity.pixelsPerSecond.dx / context.size!.width));
_backGestureController = null;
}
void _handleDragCancel() {
assert(mounted);
// This can be called even if start is not called, paired with
// the "down" event
// that we don't consider here.
_backGestureController?.dragEnd(0.0);
_backGestureController = null;
}
void _handlePointerDown(PointerDownEvent event) {
if (widget.enabledCallback()) _recognizer.addPointer(event);
}
double _convertToLogical(double value) {
switch (Directionality.of(context)) {
case TextDirection.rtl:
return -value;
case TextDirection.ltr:
return value;
}
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasDirectionality(context));
// For devices with notches, the drag area needs to be larger on the side
// that has the notch.
var dragAreaWidth = Directionality.of(context) == TextDirection.ltr
? MediaQuery.of(context).padding.left
: MediaQuery.of(context).padding.right;
dragAreaWidth = max(dragAreaWidth, widget.gestureWidth);
return Stack(
fit: StackFit.passthrough,
children: <Widget>[
widget.child,
PositionedDirectional(
start: 0.0,
width: dragAreaWidth,
top: 0.0,
bottom: 0.0,
child: Listener(
onPointerDown: _handlePointerDown,
behavior: HitTestBehavior.translucent,
),
),
],
);
}
}
class CupertinoBackGestureController<T> {
/// Creates a controller for an iOS-style back gesture.
// Called by CupertinoBackGestureDetector when a pop ("back") drag start
// gesture is detected. The returned controller handles all of the subsequent
// drag events.
/// True if an iOS-style back swipe pop gesture is currently
/// underway for [route].
///
/// The [navigator] and [controller] arguments must not be null.
CupertinoBackGestureController({
required this.navigator,
required this.controller,
}) {
navigator.didStartUserGesture();
/// This just check the route's [NavigatorState.userGestureInProgress].
///
/// See also:
///
/// * [popGestureEnabled], which returns true if a user-triggered pop gesture
/// would be allowed.
static bool isPopGestureInProgress(PageRoute<dynamic> route) {
return route.navigator!.userGestureInProgress;
}
final AnimationController controller;
final NavigatorState navigator;
/// The drag gesture has changed by [fractionalDelta]. The total range of the
/// drag should be 0.0 to 1.0.
void dragUpdate(double delta) {
controller.value -= delta;
static bool _isPopGestureEnabled<T>(PageRoute<T> route) {
// If there's nothing to go back to, then obviously we don't support
// the back gesture.
if (route.isFirst) return false;
// If the route wouldn't actually pop if we popped it, then the gesture
// would be really confusing (or would skip internal routes),
//so disallow it.
if (route.willHandlePopInternally) return false;
// If attempts to dismiss this route might be vetoed such as in a page
// with forms, then do not allow the user to dismiss the route with a swipe.
if (route.hasScopedWillPopCallback) return false;
// Fullscreen dialogs aren't dismissible by back swipe.
if (route.fullscreenDialog) return false;
// If we're in an animation already, we cannot be manually swiped.
if (route.animation!.status != AnimationStatus.completed) return false;
// If we're being popped into, we also cannot be swiped until the pop above
// it completes. This translates to our secondary animation being
// dismissed.
if (route.secondaryAnimation!.status != AnimationStatus.dismissed) {
return false;
}
// If we're in a gesture already, we cannot start another.
if (isPopGestureInProgress(route)) return false;
/// The drag gesture has ended with a horizontal motion of
/// [fractionalVelocity] as a fraction of screen width per second.
void dragEnd(double velocity) {
// Fling in the appropriate direction.
// AnimationController.fling is guaranteed to
// take at least one frame.
//
// This curve has been determined through rigorously eyeballing native iOS
// animations.
const Curve animationCurve = Curves.fastLinearToSlowEaseIn;
final bool animateForward;
// If the user releases the page before mid screen with sufficient velocity,
// or after mid screen, we should animate the page out. Otherwise, the page
// should be animated back in.
if (velocity.abs() >= _kMinFlingVelocity) {
animateForward = velocity <= 0;
} else {
animateForward = controller.value > 0.5;
// Looks like a back gesture would be welcome!
return true;
}
if (animateForward) {
// The closer the panel is to dismissing, the shorter the animation is.
// We want to cap the animation time, but we want to use a linear curve
// to determine it.
final droppedPageForwardAnimationTime = min(
lerpDouble(
_kMaxDroppedSwipePageForwardAnimationTime, 0, controller.value)!
.floor(),
_kMaxPageBackAnimationTime,
);
controller.animateTo(1.0,
duration: Duration(milliseconds: droppedPageForwardAnimationTime),
curve: animationCurve);
} else {
// This route is destined to pop at this point. Reuse navigator's pop.
navigator.pop();
// The popping may have finished inline if already at the
// target destination.
if (controller.isAnimating) {
// Otherwise, use a custom popping animation duration and curve.
final droppedPageBackAnimationTime = lerpDouble(
0, _kMaxDroppedSwipePageForwardAnimationTime, controller.value)!
.floor();
controller.animateBack(0.0,
duration: Duration(milliseconds: droppedPageBackAnimationTime),
curve: animationCurve);
}
}
static CupertinoBackGestureController<T> _startPopGesture<T>(
PageRoute<T> route) {
assert(_isPopGestureEnabled(route));
if (controller.isAnimating) {
// Keep the userGestureInProgress in true state so we don't change the
// curve of the page transition mid-flight since CupertinoPageTransition
// depends on userGestureInProgress.
late AnimationStatusListener animationStatusCallback;
animationStatusCallback = (status) {
navigator.didStopUserGesture();
controller.removeStatusListener(animationStatusCallback);
};
controller.addStatusListener(animationStatusCallback);
} else {
navigator.didStopUserGesture();
}
return CupertinoBackGestureController<T>(
navigator: route.navigator!,
controller: route.controller!, // protected access
);
}
}
... ...
... ... @@ -8,6 +8,7 @@ import '../../../get_core/get_core.dart';
import '../../get_navigation.dart';
typedef OnTap = void Function(GetSnackBar snack);
typedef SnackbarStatusCallback = void Function(SnackbarStatus? status);
class GetSnackBar extends StatefulWidget {
... ... @@ -17,6 +18,11 @@ class GetSnackBar extends StatefulWidget {
/// The title displayed to the user
final String? title;
/// The direction in which the SnackBar can be dismissed.
///
/// Cannot be null, defaults to [DismissDirection.down] when
/// [snackPosition] == [SnackPosition.BOTTOM] and [DismissDirection.up]
/// when [snackPosition] == [SnackPosition.TOP]
final DismissDirection? dismissDirection;
/// The message displayed to the user.
... ... @@ -44,7 +50,8 @@ class GetSnackBar extends StatefulWidget {
/// Check (this example)[https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/material/shadows.dart]
final List<BoxShadow>? boxShadows;
/// Makes [backgroundColor] be ignored.
/// Give to GetSnackbar a gradient background.
/// It Makes [backgroundColor] be ignored.
final Gradient? backgroundGradient;
/// You can use any widget here, but I recommend [Icon] or [Image] as
... ... @@ -55,7 +62,10 @@ class GetSnackBar extends StatefulWidget {
/// An option to animate the icon (if present). Defaults to true.
final bool shouldIconPulse;
/// A [TextButton] widget if you need an action from the user.
/// (optional) An action that the user can take based on the snack bar.
///
/// For example, the snack bar might let the user undo the operation that
/// prompted the snackbar.
final Widget? mainButton;
/// A callback that registers the user's click anywhere.
... ... @@ -135,7 +145,7 @@ class GetSnackBar extends StatefulWidget {
/// Default is 0.0. If different than 0.0, blurs only Snack's background.
/// To take effect, make sure your [backgroundColor] has some opacity.
/// The greater the value, the greater the blur.
final double? barBlur;
final double barBlur;
/// Default is 0.0. If different than 0.0, creates a blurred
/// overlay that prevents the user from interacting with the screen.
... ... @@ -191,7 +201,7 @@ class GetSnackBar extends StatefulWidget {
}) : super(key: key);
@override
State createState() => _GetSnackBarState();
State createState() => GetSnackBarState();
/// Show the snack. It's call [SnackbarStatus.OPENING] state
/// followed by [SnackbarStatus.OPEN]
... ... @@ -200,23 +210,7 @@ class GetSnackBar extends StatefulWidget {
}
}
/// Indicates Status of snackbar
/// [SnackbarStatus.OPEN] Snack is fully open, [SnackbarStatus.CLOSED] Snackbar
/// has closed,
/// [SnackbarStatus.OPENING] Starts with the opening animation and ends
/// with the full
/// snackbar display, [SnackbarStatus.CLOSING] Starts with the closing animation
/// and ends
/// with the full snackbar dispose
enum SnackbarStatus { OPEN, CLOSED, OPENING, CLOSING }
/// Indicates if snack is going to start at the [TOP] or at the [BOTTOM]
enum SnackPosition { TOP, BOTTOM }
/// Indicates if snack will be attached to the edge of the screen or not
enum SnackStyle { FLOATING, GROUNDED }
class _GetSnackBarState extends State<GetSnackBar>
class GetSnackBarState extends State<GetSnackBar>
with TickerProviderStateMixin {
AnimationController? _fadeController;
late Animation<double> _fadeAnimation;
... ... @@ -235,12 +229,30 @@ class _GetSnackBarState extends State<GetSnackBar>
final Completer<Size> _boxHeightCompleter = Completer<Size>();
late VoidCallback _progressListener;
late CurvedAnimation _progressAnimation;
final _backgroundBoxKey = GlobalKey();
double get buttonPadding {
if (widget.padding.right - 12 < 0) {
return 4;
} else {
return widget.padding.right - 12;
}
}
RowStyle get _rowStyle {
if (widget.mainButton != null && widget.icon == null) {
return RowStyle.action;
} else if (widget.mainButton == null && widget.icon != null) {
return RowStyle.icon;
} else if (widget.mainButton != null && widget.icon != null) {
return RowStyle.all;
} else {
return RowStyle.none;
}
}
@override
Widget build(BuildContext context) {
return Align(
... ... @@ -258,7 +270,42 @@ class _GetSnackBarState extends State<GetSnackBar>
top: widget.snackPosition == SnackPosition.TOP,
left: false,
right: false,
child: _getSnack(),
child: Stack(
children: [
FutureBuilder<Size>(
future: _boxHeightCompleter.future,
builder: (context, snapshot) {
if (snapshot.hasData) {
if (widget.barBlur == 0) {
return _emptyWidget;
}
return ClipRRect(
borderRadius: BorderRadius.circular(widget.borderRadius),
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: widget.barBlur, sigmaY: widget.barBlur),
child: Container(
height: snapshot.data!.height,
width: snapshot.data!.width,
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius:
BorderRadius.circular(widget.borderRadius),
),
),
),
);
} else {
return _emptyWidget;
}
},
),
if (widget.userInputForm != null)
_containerWithForm()
else
_containerWithoutForm()
],
),
),
),
);
... ... @@ -267,8 +314,7 @@ class _GetSnackBarState extends State<GetSnackBar>
@override
void dispose() {
_fadeController?.dispose();
widget.progressIndicatorController?.removeListener(_progressListener);
widget.progressIndicatorController?.removeListener(_updateProgress);
widget.progressIndicatorController?.dispose();
_focusAttachment.detach();
... ... @@ -284,9 +330,8 @@ class _GetSnackBarState extends State<GetSnackBar>
widget.userInputForm != null ||
((widget.message != null && widget.message!.isNotEmpty) ||
widget.messageText != null),
"""
A message is mandatory if you are not using userInputForm.
Set either a message or messageText""");
'''
You need to either use message[String], or messageText[Widget] or define a userInputForm[Form] in GetSnackbar''');
_isTitlePresent = (widget.title != null || widget.titleText != null);
_messageTopMargin = _isTitlePresent ? 6.0 : widget.padding.top;
... ... @@ -339,10 +384,7 @@ Set either a message or messageText""");
void _configureProgressIndicatorAnimation() {
if (widget.showProgressIndicator &&
widget.progressIndicatorController != null) {
_progressListener = () {
setState(() {});
};
widget.progressIndicatorController!.addListener(_progressListener);
widget.progressIndicatorController!.addListener(_updateProgress);
_progressAnimation = CurvedAnimation(
curve: Curves.linear, parent: widget.progressIndicatorController!);
... ... @@ -371,7 +413,7 @@ Set either a message or messageText""");
_fadeController!.forward();
}
Widget _generateInputSnack() {
Widget _containerWithForm() {
return Container(
key: _backgroundBoxKey,
constraints: widget.maxWidth != null
... ... @@ -383,7 +425,10 @@ Set either a message or messageText""");
boxShadow: widget.boxShadows,
borderRadius: BorderRadius.circular(widget.borderRadius),
border: widget.borderColor != null
? Border.all(color: widget.borderColor!, width: widget.borderWidth!)
? Border.all(
color: widget.borderColor!,
width: widget.borderWidth!,
)
: null,
),
child: Padding(
... ... @@ -398,7 +443,14 @@ Set either a message or messageText""");
);
}
Widget _generateSnack() {
Widget _containerWithoutForm() {
final iconPadding = widget.padding.left > 16.0 ? widget.padding.left : 0.0;
final left = _rowStyle == RowStyle.icon || _rowStyle == RowStyle.all
? 4.0
: widget.padding.left;
final right = _rowStyle == RowStyle.action || _rowStyle == RowStyle.all
? 8.0
: widget.padding.right;
return Container(
key: _backgroundBoxKey,
constraints: widget.maxWidth != null
... ... @@ -427,63 +479,12 @@ Set either a message or messageText""");
: _emptyWidget,
Row(
mainAxisSize: MainAxisSize.max,
children: _getAppropriateRowLayout(),
),
],
),
);
}
List<Widget> _getAppropriateRowLayout() {
double buttonRightPadding;
var iconPadding = 0.0;
if (widget.padding.right - 12 < 0) {
buttonRightPadding = 4;
} else {
buttonRightPadding = widget.padding.right - 12;
}
if (widget.padding.left > 16.0) {
iconPadding = widget.padding.left;
}
if (widget.icon == null && widget.mainButton == null) {
return [
_buildLeftBarIndicator(),
Expanded(
flex: 1,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
(_isTitlePresent)
? Padding(
padding: EdgeInsets.only(
top: widget.padding.top,
left: widget.padding.left,
right: widget.padding.right,
),
child: _getTitleText(),
)
: _emptyWidget,
Padding(
padding: EdgeInsets.only(
top: _messageTopMargin,
left: widget.padding.left,
right: widget.padding.right,
bottom: widget.padding.bottom,
),
child: widget.messageText ?? _getDefaultNotificationText(),
),
],
),
),
];
} else if (widget.icon != null && widget.mainButton == null) {
return <Widget>[
children: [
_buildLeftBarIndicator(),
if (_rowStyle == RowStyle.icon)
ConstrainedBox(
constraints: BoxConstraints.tightFor(width: 42.0 + iconPadding),
constraints:
BoxConstraints.tightFor(width: 42.0 + iconPadding),
child: _getIcon(),
),
Expanded(
... ... @@ -492,112 +493,51 @@ Set either a message or messageText""");
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
(_isTitlePresent)
? Padding(
padding: EdgeInsets.only(
top: widget.padding.top,
left: 4.0,
right: widget.padding.left,
),
child: _getTitleText(),
)
: _emptyWidget,
if (_isTitlePresent)
Padding(
padding: EdgeInsets.only(
top: _messageTopMargin,
left: 4.0,
right: widget.padding.right,
bottom: widget.padding.bottom,
),
child: widget.messageText ?? _getDefaultNotificationText(),
),
],
top: widget.padding.top,
left: left,
right: right,
),
child: widget.titleText ??
Text(
widget.title ?? "",
style: TextStyle(
fontSize: 16.0,
color: Colors.white,
fontWeight: FontWeight.bold,
),
];
} else if (widget.icon == null && widget.mainButton != null) {
return <Widget>[
_buildLeftBarIndicator(),
Expanded(
flex: 1,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
(_isTitlePresent)
? Padding(
padding: EdgeInsets.only(
top: widget.padding.top,
left: widget.padding.left,
right: widget.padding.right,
),
child: _getTitleText(),
)
: _emptyWidget,
else
_emptyWidget,
Padding(
padding: EdgeInsets.only(
top: _messageTopMargin,
left: widget.padding.left,
right: 8.0,
left: left,
right: right,
bottom: widget.padding.bottom,
),
child: widget.messageText ?? _getDefaultNotificationText(),
child: widget.messageText ??
Text(
widget.message ?? "",
style:
TextStyle(fontSize: 14.0, color: Colors.white),
),
),
],
),
),
if (_rowStyle == RowStyle.action)
Padding(
padding: EdgeInsets.only(right: buttonRightPadding),
child: _getMainActionButton(),
),
];
} else {
return <Widget>[
_buildLeftBarIndicator(),
ConstrainedBox(
constraints: BoxConstraints.tightFor(width: 42.0 + iconPadding),
child: _getIcon(),
),
Expanded(
flex: 1,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
(_isTitlePresent)
? Padding(
padding: EdgeInsets.only(
top: widget.padding.top,
left: 4.0,
right: 8.0,
),
child: _getTitleText(),
)
: _emptyWidget,
Padding(
padding: EdgeInsets.only(
top: _messageTopMargin,
left: 4.0,
right: 8.0,
bottom: widget.padding.bottom,
),
child: widget.messageText ?? _getDefaultNotificationText(),
padding: EdgeInsets.only(right: buttonPadding),
child: widget.mainButton,
),
],
),
],
),
Padding(
padding: EdgeInsets.only(right: buttonRightPadding),
child: _getMainActionButton(),
),
];
}
}
Text _getDefaultNotificationText() {
return Text(
widget.message ?? "",
style: TextStyle(fontSize: 14.0, color: Colors.white),
);
}
... ... @@ -614,59 +554,28 @@ Set either a message or messageText""");
}
}
Widget? _getMainActionButton() {
return widget.mainButton;
}
void _updateProgress() => setState(() {});
}
Widget _getSnack() {
Widget snack;
enum RowStyle {
icon,
action,
all,
none,
}
if (widget.userInputForm != null) {
snack = _generateInputSnack();
} else {
snack = _generateSnack();
}
/// Indicates Status of snackbar
/// [SnackbarStatus.OPEN] Snack is fully open, [SnackbarStatus.CLOSED] Snackbar
/// has closed,
/// [SnackbarStatus.OPENING] Starts with the opening animation and ends
/// with the full
/// snackbar display, [SnackbarStatus.CLOSING] Starts with the closing animation
/// and ends
/// with the full snackbar dispose
enum SnackbarStatus { OPEN, CLOSED, OPENING, CLOSING }
return Stack(
children: [
FutureBuilder<Size>(
future: _boxHeightCompleter.future,
builder: (context, snapshot) {
if (snapshot.hasData) {
if (widget.barBlur == 0) {
return _emptyWidget;
}
return ClipRRect(
borderRadius: BorderRadius.circular(widget.borderRadius),
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: widget.barBlur!, sigmaY: widget.barBlur!),
child: Container(
height: snapshot.data!.height,
width: snapshot.data!.width,
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(widget.borderRadius),
),
),
),
);
} else {
return _emptyWidget;
}
},
),
snack,
],
);
}
/// Indicates if snack is going to start at the [TOP] or at the [BOTTOM]
enum SnackPosition { TOP, BOTTOM }
Widget _getTitleText() {
return widget.titleText ??
Text(
widget.title ?? "",
style: TextStyle(
fontSize: 16.0, color: Colors.white, fontWeight: FontWeight.bold),
);
}
}
/// Indicates if snack will be attached to the edge of the screen or not
enum SnackStyle { FLOATING, GROUNDED }
... ...
... ... @@ -7,13 +7,14 @@ import '../../../get.dart';
class SnackbarController {
static final _snackBarQueue = _SnackBarQueue();
static bool get isSnackbarBeingShown => _snackBarQueue.isJobInProgress;
static bool get isSnackbarBeingShown => _snackBarQueue._isJobInProgress;
final key = GlobalKey<GetSnackBarState>();
late Animation<double> _filterBlurAnimation;
late Animation<Color?> _filterColorAnimation;
final GetSnackBar snackbar;
final _transitionCompleter = Completer<SnackbarController>();
final _transitionCompleter = Completer();
late SnackbarStatusCallback? _snackbarStatus;
late final Alignment? _initialAlignment;
... ... @@ -42,10 +43,14 @@ class SnackbarController {
SnackbarController(this.snackbar);
Future<SnackbarController> get future => _transitionCompleter.future;
Future<void> get future => _transitionCompleter.future;
/// Close the snackbar with animation
Future<void> close() async {
Future<void> close({bool withAnimations = true}) async {
if (!withAnimations) {
_removeOverlay();
return;
}
_removeEntry();
await future;
}
... ... @@ -54,7 +59,7 @@ class SnackbarController {
/// Only one GetSnackbar will be displayed at a time, and this method returns
/// a future to when the snackbar disappears.
Future<void> show() {
return _snackBarQueue.addJob(this);
return _snackBarQueue._addJob(this);
}
void _cancelTimer() {
... ... @@ -141,7 +146,7 @@ class SnackbarController {
return AnimationController(
duration: snackbar.animationDuration,
debugLabel: '$runtimeType',
vsync: navigator!,
vsync: _overlayState!,
);
}
... ... @@ -181,7 +186,7 @@ class SnackbarController {
onTap: () {
if (snackbar.isDismissible && !_onTappedDismiss) {
_onTappedDismiss = true;
Get.back();
close();
}
},
child: AnimatedBuilder(
... ... @@ -252,7 +257,8 @@ class SnackbarController {
},
key: const Key('dismissible'),
onDismissed: (_) {
_onDismiss();
_wasDismissedBySwipe = true;
_removeEntry();
},
child: _getSnackbarContainer(child),
);
... ... @@ -291,12 +297,6 @@ class SnackbarController {
}
}
void _onDismiss() {
_cancelTimer();
_wasDismissedBySwipe = true;
_removeEntry();
}
void _removeEntry() {
assert(
!_transitionCompleter.isCompleted,
... ... @@ -307,7 +307,6 @@ class SnackbarController {
if (_wasDismissedBySwipe) {
Timer(const Duration(milliseconds: 200), _controller.reset);
_wasDismissedBySwipe = false;
} else {
_controller.reverse();
... ... @@ -319,10 +318,11 @@ class SnackbarController {
element.remove();
}
assert(!_transitionCompleter.isCompleted, 'Cannot remove overlay twice.');
assert(!_transitionCompleter.isCompleted,
'Cannot remove overlay from a disposed snackbar');
_controller.dispose();
_overlayEntries.clear();
_transitionCompleter.complete(this);
_transitionCompleter.complete();
}
Future<void> _show() {
... ... @@ -331,38 +331,40 @@ class SnackbarController {
}
static void cancelAllSnackbars() {
_snackBarQueue.cancelAllJobs();
_snackBarQueue._cancelAllJobs();
}
static Future<void> closeCurrentSnackbar() async {
await _snackBarQueue.closeCurrentJob();
await _snackBarQueue._closeCurrentJob();
}
}
class _SnackBarQueue {
final _queue = GetQueue();
final _snackbarList = <SnackbarController>[];
bool _isJobInProgress = false;
SnackbarController? _currentSnackbar;
SnackbarController? get _currentSnackbar {
if (_snackbarList.isEmpty) return null;
return _snackbarList.first;
}
bool get isJobInProgress => _isJobInProgress;
bool get _isJobInProgress => _snackbarList.isNotEmpty;
Future<void> addJob(SnackbarController job) async {
_isJobInProgress = true;
_currentSnackbar = job;
Future<void> _addJob(SnackbarController job) async {
_snackbarList.add(job);
final data = await _queue.add(job._show);
_isJobInProgress = false;
_currentSnackbar = null;
_snackbarList.remove(job);
return data;
}
void cancelAllJobs() {
_currentSnackbar?.close();
Future<void> _cancelAllJobs() async {
await _currentSnackbar?.close();
_queue.cancelAllJobs();
_snackbarList.clear();
}
Future<void> closeCurrentJob() async {
await _currentSnackbar?.close();
Future<void> _closeCurrentJob() async {
if (_currentSnackbar == null) return;
await _currentSnackbar!.close();
}
}
... ...
... ... @@ -5,6 +5,8 @@ part of rx_types;
/// This interface is the contract that _RxImpl]<T> uses in all it's
/// subclass.
abstract class RxInterface<T> {
static RxInterface? proxy;
bool get canUpdate;
/// Adds a listener to stream
... ... @@ -13,8 +15,6 @@ abstract class RxInterface<T> {
/// Close the Rx Variable
void close();
static RxInterface? proxy;
/// Calls `callback` with current value, when the value changes.
StreamSubscription<T> listen(void Function(T event) onData,
{Function? onError, void Function()? onDone, bool? cancelOnError});
... ...
// ignore_for_file: lines_longer_than_80_chars
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import '../../get_state_manager.dart';
/// Used like `SingleTickerProviderMixin` but only with Get Controllers.
... ... @@ -7,6 +12,84 @@ import '../../get_state_manager.dart';
/// Example:
///```
///class SplashController extends GetxController with
/// GetSingleTickerProviderStateMixin {
/// AnimationController controller;
///
/// @override
/// void onInit() {
/// final duration = const Duration(seconds: 2);
/// controller =
/// AnimationController.unbounded(duration: duration, vsync: this);
/// controller.repeat();
/// controller.addListener(() =>
/// print("Animation Controller value: ${controller.value}"));
/// }
/// ...
/// ```
mixin GetSingleTickerProviderStateMixin on GetxController
implements TickerProvider {
Ticker? _ticker;
@override
Ticker createTicker(TickerCallback onTick) {
assert(() {
if (_ticker == null) return true;
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'$runtimeType is a SingleTickerProviderStateMixin but multiple tickers were created.'),
ErrorDescription(
'A SingleTickerProviderStateMixin can only be used as a TickerProvider once.'),
ErrorHint(
'If a State is used for multiple AnimationController objects, or if it is passed to other '
'objects and those objects might use it more than one time in total, then instead of '
'mixing in a SingleTickerProviderStateMixin, use a regular TickerProviderStateMixin.',
),
]);
}());
_ticker =
Ticker(onTick, debugLabel: kDebugMode ? 'created by $this' : null);
// We assume that this is called from initState, build, or some sort of
// event handler, and that thus TickerMode.of(context) would return true. We
// can't actually check that here because if we're in initState then we're
// not allowed to do inheritance checks yet.
return _ticker!;
}
void didChangeDependencies(BuildContext context) {
if (_ticker != null) _ticker!.muted = !TickerMode.of(context);
}
@override
void onClose() {
assert(() {
if (_ticker == null || !_ticker!.isActive) return true;
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('$this was disposed with an active Ticker.'),
ErrorDescription(
'$runtimeType created a Ticker via its SingleTickerProviderStateMixin, but at the time '
'dispose() was called on the mixin, that Ticker was still active. The Ticker must '
'be disposed before calling super.dispose().',
),
ErrorHint(
'Tickers used by AnimationControllers '
'should be disposed by calling dispose() on the AnimationController itself. '
'Otherwise, the ticker will leak.',
),
_ticker!.describeForError('The offending ticker was'),
]);
}());
super.onClose();
}
}
@Deprecated('use GetSingleTickerProviderStateMixin')
/// Used like `SingleTickerProviderMixin` but only with Get Controllers.
/// Simplifies AnimationController creation inside GetxController.
///
/// Example:
///```
///class SplashController extends GetxController with
/// SingleGetTickerProviderMixin {
/// AnimationController _ac;
///
... ...
... ... @@ -481,58 +481,6 @@ void main() {
expect(find.byType(FirstScreen), findsOneWidget);
});
testWidgets("Get.snackbar test", (tester) async {
await tester.pumpWidget(
GetMaterialApp(
popGesture: true,
home: ElevatedButton(
child: Text('Open Snackbar'),
onPressed: () {
Get.snackbar('title', "message", duration: Duration(seconds: 1));
},
),
),
);
expect(Get.isSnackbarOpen, false);
await tester.tap(find.text('Open Snackbar'));
expect(Get.isSnackbarOpen, true);
await tester.pump(const Duration(seconds: 1));
});
testWidgets("Get.rawSnackbar test", (tester) async {
await tester.pumpWidget(
Wrapper(
child: ElevatedButton(
child: Text('Open Snackbar'),
onPressed: () {
Get.rawSnackbar(
title: 'title',
message: "message",
onTap: (_) {
print('snackbar tapped');
},
duration: Duration(seconds: 1),
shouldIconPulse: true,
icon: Icon(Icons.alarm),
showProgressIndicator: true,
barBlur: null,
isDismissible: true,
leftBarIndicatorColor: Colors.amber,
overlayBlur: 1.0,
);
},
),
),
);
expect(Get.isSnackbarOpen, false);
await tester.tap(find.text('Open Snackbar'));
expect(Get.isSnackbarOpen, true);
await tester.pump(const Duration(seconds: 1));
});
}
class FirstScreen extends StatelessWidget {
... ...
... ... @@ -10,28 +10,49 @@ void main() {
name: '/city',
page: () => Container(),
children: [
GetPage(name: '/home', page: () => Container(), children: [
GetPage(name: '/bed-room', page: () => Container()),
GetPage(name: '/living-room', page: () => Container()),
]),
GetPage(
name: '/home',
page: () => Container(),
transition: Transition.rightToLeftWithFade,
children: [
GetPage(
name: '/bed-room',
transition: Transition.size,
page: () => Container(),
),
GetPage(
name: '/living-room',
transition: Transition.topLevel,
page: () => Container(),
),
],
),
GetPage(
name: '/work',
transition: Transition.upToDown,
page: () => Container(),
children: [
GetPage(
name: '/office',
transition: Transition.zoom,
page: () => Container(),
children: [
GetPage(
name: '/pen',
transition: Transition.cupertino,
page: () => Container(),
parameters: testParams,
),
GetPage(name: '/paper', page: () => Container()),
GetPage(
name: '/paper',
page: () => Container(),
transition: Transition.downToUp,
),
],
),
GetPage(
name: '/meeting-room',
transition: Transition.fade,
page: () => Container(),
),
],
... ... @@ -56,15 +77,42 @@ void main() {
test('Parse Page without children', () {
final pageTree = [
GetPage(name: '/city', page: () => Container()),
GetPage(name: '/city/home', page: () => Container()),
GetPage(name: '/city/home/bed-room', page: () => Container()),
GetPage(name: '/city/home/living-room', page: () => Container()),
GetPage(name: '/city/work', page: () => Container()),
GetPage(name: '/city/work/office', page: () => Container()),
GetPage(name: '/city/work/office/pen', page: () => Container()),
GetPage(name: '/city/work/office/paper', page: () => Container()),
GetPage(name: '/city/work/meeting-room', page: () => Container()),
GetPage(
name: '/city',
page: () => Container(),
transition: Transition.cupertino),
GetPage(
name: '/city/home',
page: () => Container(),
transition: Transition.downToUp),
GetPage(
name: '/city/home/bed-room',
page: () => Container(),
transition: Transition.fade),
GetPage(
name: '/city/home/living-room',
page: () => Container(),
transition: Transition.fadeIn),
GetPage(
name: '/city/work',
page: () => Container(),
transition: Transition.leftToRight),
GetPage(
name: '/city/work/office',
page: () => Container(),
transition: Transition.leftToRightWithFade),
GetPage(
name: '/city/work/office/pen',
page: () => Container(),
transition: Transition.native),
GetPage(
name: '/city/work/office/paper',
page: () => Container(),
transition: Transition.noTransition),
GetPage(
name: '/city/work/meeting-room',
page: () => Container(),
transition: Transition.rightToLeft),
];
final tree = ParseRouteTree(routes: pageTree);
... ...
// import 'package:flutter/material.dart';
// import 'package:flutter_test/flutter_test.dart';
// import 'package:get/get.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:get/get.dart';
void main() {
// testWidgets(
// 'GetPage page null',
// (tester) async {
// expect(() => GetPage(page: null, name: null), throwsAssertionError);
// },
// );
// testWidgets(
// "GetPage maintainState null",
// (tester) async {
// expect(
// () => GetPage(page: () => Scaffold(), maintainState: null, name: '/'),
// throwsAssertionError,
// );
// },
// );
// testWidgets(
// "GetPage name null",
// (tester) async {
// expect(
// () => GetPage(page: () => Scaffold(),
// maintainState: null, name: null),
// throwsAssertionError,
// );
// },
// );
// testWidgets(
// "GetPage fullscreenDialog null",
// (tester) async {
// expect(
// () =>
// GetPage(page: () => Scaffold(), fullscreenDialog: null, name: '/'),
// throwsAssertionError,
// );
// },
// );
testWidgets('Back swipe dismiss interrupted by route push', (tester) async {
// final scaffoldKey = GlobalKey();
await tester.pumpWidget(
GetCupertinoApp(
popGesture: true,
home: CupertinoPageScaffold(
// key: scaffoldKey,
child: Center(
child: CupertinoButton(
onPressed: () {
Get.to(() => CupertinoPageScaffold(
child: Center(child: Text('route')),
));
},
child: const Text('push'),
),
),
),
),
);
// Check the basic iOS back-swipe dismiss transition. Dragging the pushed
// route halfway across the screen will trigger the iOS dismiss animation
await tester.tap(find.text('push'));
await tester.pumpAndSettle();
expect(find.text('route'), findsOneWidget);
expect(find.text('push'), findsNothing);
var gesture = await tester.startGesture(const Offset(5, 300));
await gesture.moveBy(const Offset(400, 0));
await gesture.up();
await tester.pump();
expect(
// The 'route' route has been dragged to the right, halfway across the screen
tester.getTopLeft(find.ancestor(
of: find.text('route'),
matching: find.byType(CupertinoPageScaffold))),
const Offset(400, 0),
);
expect(
// The 'push' route is sliding in from the left.
tester
.getTopLeft(find.ancestor(
of: find.text('push'),
matching: find.byType(CupertinoPageScaffold)))
.dx,
0,
);
await tester.pumpAndSettle();
expect(find.text('push'), findsOneWidget);
expect(
tester.getTopLeft(find.ancestor(
of: find.text('push'), matching: find.byType(CupertinoPageScaffold))),
Offset.zero,
);
expect(find.text('route'), findsNothing);
// Run the dismiss animation 60%, which exposes the route "push" button,
// and then press the button.
await tester.tap(find.text('push'));
await tester.pumpAndSettle();
expect(find.text('route'), findsOneWidget);
expect(find.text('push'), findsNothing);
gesture = await tester.startGesture(const Offset(5, 300));
await gesture.moveBy(const Offset(400, 0)); // Drag halfway.
await gesture.up();
// Trigger the snapping animation.
// Since the back swipe drag was brought to >=50% of the screen, it will
// self snap to finish the pop transition as the gesture is lifted.
//
// This drag drop animation is 400ms when dropped exactly halfway
// (800 / [pixel distance remaining], see
// _CupertinoBackGestureController.dragEnd). It follows a curve that is very
// steep initially.
await tester.pump();
expect(
tester.getTopLeft(find.ancestor(
of: find.text('route'),
matching: find.byType(CupertinoPageScaffold))),
const Offset(400, 0),
);
// Let the dismissing snapping animation go 60%.
await tester.pump(const Duration(milliseconds: 240));
expect(
tester
.getTopLeft(find.ancestor(
of: find.text('route'),
matching: find.byType(CupertinoPageScaffold)))
.dx,
moreOrLessEquals(798, epsilon: 1),
);
// Use the navigator to push a route instead of tapping the 'push' button.
// The topmost route (the one that's animating away), ignores input while
// the pop is underway because route.navigator.userGestureInProgress.
Get.to(() => const CupertinoPageScaffold(
child: Center(child: Text('route')),
));
await tester.pumpAndSettle();
expect(find.text('route'), findsOneWidget);
expect(find.text('push'), findsNothing);
expect(
tester
.state<NavigatorState>(find.byType(Navigator))
.userGestureInProgress,
false,
);
});
}
... ...
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:get/get.dart';
void main() {
testWidgets("test if Get.isSnackbarOpen works with Get.snackbar",
(tester) async {
await tester.pumpWidget(
GetMaterialApp(
popGesture: true,
home: ElevatedButton(
child: Text('Open Snackbar'),
onPressed: () {
Get.snackbar(
'title',
"message",
duration: Duration(seconds: 1),
isDismissible: false,
);
},
),
),
);
expect(Get.isSnackbarOpen, false);
await tester.tap(find.text('Open Snackbar'));
expect(Get.isSnackbarOpen, true);
await tester.pump(const Duration(seconds: 1));
expect(Get.isSnackbarOpen, false);
});
testWidgets("Get.rawSnackbar test", (tester) async {
await tester.pumpWidget(
GetMaterialApp(
popGesture: true,
home: ElevatedButton(
child: Text('Open Snackbar'),
onPressed: () {
Get.rawSnackbar(
title: 'title',
message: "message",
onTap: (_) {
print('snackbar tapped');
},
shouldIconPulse: true,
icon: Icon(Icons.alarm),
showProgressIndicator: true,
duration: Duration(seconds: 1),
isDismissible: true,
leftBarIndicatorColor: Colors.amber,
overlayBlur: 1.0,
);
},
),
),
);
expect(Get.isSnackbarOpen, false);
await tester.tap(find.text('Open Snackbar'));
expect(Get.isSnackbarOpen, true);
await tester.pump(const Duration(seconds: 1));
expect(Get.isSnackbarOpen, false);
});
testWidgets("test snackbar queue", (tester) async {
final messageOne = Text('title');
final messageTwo = Text('titleTwo');
await tester.pumpWidget(
GetMaterialApp(
popGesture: true,
home: ElevatedButton(
child: Text('Open Snackbar'),
onPressed: () {
Get.rawSnackbar(
messageText: messageOne, duration: Duration(seconds: 1));
Get.rawSnackbar(
messageText: messageTwo, duration: Duration(seconds: 1));
},
),
),
);
expect(Get.isSnackbarOpen, false);
await tester.tap(find.text('Open Snackbar'));
expect(Get.isSnackbarOpen, true);
await tester.pump(const Duration(milliseconds: 500));
expect(find.text('title'), findsOneWidget);
expect(find.text('titleTwo'), findsNothing);
await tester.pump(const Duration(milliseconds: 500));
expect(find.text('title'), findsNothing);
expect(find.text('titleTwo'), findsOneWidget);
Get.closeAllSnackbars();
});
testWidgets("test snackbar dismissible", (tester) async {
const dismissDirection = DismissDirection.vertical;
const snackBarTapTarget = Key('snackbar-tap-target');
late final GetSnackBar getBar;
await tester.pumpWidget(GetMaterialApp(
home: Scaffold(
body: Builder(
builder: (context) {
return Column(
children: <Widget>[
GestureDetector(
key: snackBarTapTarget,
onTap: () {
getBar = GetSnackBar(
message: 'bar1',
duration: const Duration(seconds: 2),
isDismissible: true,
dismissDirection: dismissDirection,
);
Get.showSnackbar(getBar);
},
behavior: HitTestBehavior.opaque,
child: const SizedBox(
height: 100.0,
width: 100.0,
),
),
],
);
},
),
),
));
expect(Get.isSnackbarOpen, false);
expect(find.text('bar1'), findsNothing);
await tester.tap(find.byKey(snackBarTapTarget));
await tester.pumpAndSettle();
expect(Get.isSnackbarOpen, true);
await tester.pump(const Duration(milliseconds: 500));
expect(find.byWidget(getBar), findsOneWidget);
await tester.ensureVisible(find.byWidget(getBar));
await tester.drag(find.byWidget(getBar), Offset(0.0, 50.0));
await tester.pump(const Duration(milliseconds: 500));
expect(Get.isSnackbarOpen, false);
});
testWidgets("test snackbar onTap", (tester) async {
const dismissDirection = DismissDirection.vertical;
const snackBarTapTarget = Key('snackbar-tap-target');
var counter = 0;
late final GetSnackBar getBar;
late final SnackbarController getBarController;
await tester.pumpWidget(GetMaterialApp(
home: Scaffold(
body: Builder(
builder: (context) {
return Column(
children: <Widget>[
GestureDetector(
key: snackBarTapTarget,
onTap: () {
getBar = GetSnackBar(
message: 'bar1',
onTap: (_) {
counter++;
},
duration: const Duration(seconds: 2),
isDismissible: true,
dismissDirection: dismissDirection,
);
getBarController = Get.showSnackbar(getBar);
},
behavior: HitTestBehavior.opaque,
child: const SizedBox(
height: 100.0,
width: 100.0,
),
),
],
);
},
),
),
));
await tester.pumpAndSettle();
expect(Get.isSnackbarOpen, false);
expect(find.text('bar1'), findsNothing);
await tester.tap(find.byKey(snackBarTapTarget));
await tester.pumpAndSettle();
expect(Get.isSnackbarOpen, true);
await tester.pump(const Duration(milliseconds: 500));
expect(find.byWidget(getBar), findsOneWidget);
await tester.ensureVisible(find.byWidget(getBar));
await tester.tap(find.byWidget(getBar));
expect(counter, 1);
await tester.pump(const Duration(milliseconds: 3000));
await getBarController.close(withAnimations: false);
});
}
... ...