Jonny Borges

add snackbars tests and fix bugs

... ... @@ -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,
... ...
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''',
);
return _previousTitle!;
}
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();
@override
void didChangePrevious(Route<dynamic>? previousRoute) {
final previousTitleString = previousRoute is CupertinoRouteTransitionMixin
? previousRoute.title
: null;
if (_previousTitle == null) {
_previousTitle = ValueNotifier<String?>(previousTitleString);
// 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);
}
}
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);
/// 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!;
}
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;
bool get showCupertinoParallax;
// Looks like a back gesture would be welcome!
return true;
}
/// {@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,
... ... @@ -173,17 +312,36 @@ 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));
@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return buildPageTransitions<T>(
this, context, animation, secondaryAnimation, child);
}
return CupertinoBackGestureController<T>(
navigator: route.navigator!,
controller: route.controller!, // protected access
);
@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();
}
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;
/// 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;
}
/// 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;
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;
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();
// Looks like a back gesture would be welcome!
return true;
}
// 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
);
}
}
... ...
... ... @@ -145,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.
... ... @@ -201,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]
... ... @@ -210,30 +210,7 @@ class GetSnackBar extends StatefulWidget {
}
}
enum RowStyle {
icon,
action,
all,
none,
}
/// 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;
... ... @@ -306,7 +283,7 @@ class _GetSnackBarState extends State<GetSnackBar>
borderRadius: BorderRadius.circular(widget.borderRadius),
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: widget.barBlur!, sigmaY: widget.barBlur!),
sigmaX: widget.barBlur, sigmaY: widget.barBlur),
child: Container(
height: snapshot.data!.height,
width: snapshot.data!.width,
... ... @@ -579,3 +556,26 @@ You need to either use message[String], or messageText[Widget] or define a userI
void _updateProgress() => setState(() {});
}
enum RowStyle {
icon,
action,
all,
none,
}
/// 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 }
... ...
... ... @@ -8,12 +8,13 @@ import '../../../get.dart';
class SnackbarController {
static final _snackBarQueue = _SnackBarQueue();
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;
}
... ... @@ -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();
... ... @@ -323,7 +322,7 @@ class SnackbarController {
'Cannot remove overlay from a disposed snackbar');
_controller.dispose();
_overlayEntries.clear();
_transitionCompleter.complete(this);
_transitionCompleter.complete();
}
Future<void> _show() {
... ... @@ -365,6 +364,7 @@ class _SnackBarQueue {
}
Future<void> _closeCurrentJob() async {
await _currentSnackbar?.close();
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);
});
}
... ...