Skip to content

Commit

Permalink
fix: [MDS-611] Fix Toast queueing mechanism and add more properties (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
Kypsis authored Jul 25, 2023
1 parent 7bceaa5 commit 4470593
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 72 deletions.
51 changes: 41 additions & 10 deletions example/lib/src/storybook/stories/toast.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,21 @@ class ToastStory extends Story {
initial: "This is a custom MoonToast text",
);

final toastPositionKnob = context.knobs.nullable.options(
label: "position",
description: "The position of MoonToast.",
final toastAlignmentKnob = context.knobs.nullable.options(
label: "toastAlignment",
description: "The alignment (position) of MoonToast.",
enabled: false,
initial: MoonToastPosition.bottom,
initial: Alignment.bottomCenter,
options: const [
Option(label: "top", value: MoonToastPosition.top),
Option(label: "bottom", value: MoonToastPosition.bottom),
Option(label: "topLeft", value: Alignment.topLeft),
Option(label: "topCenter", value: Alignment.topCenter),
Option(label: "topRight", value: Alignment.topRight),
Option(label: "centerLeft", value: Alignment.centerLeft),
Option(label: "center", value: Alignment.center),
Option(label: "centerRight", value: Alignment.centerRight),
Option(label: "bottomLeft", value: Alignment.bottomLeft),
Option(label: "bottomCenter", value: Alignment.bottomCenter),
Option(label: "bottomRight", value: Alignment.bottomRight),
],
);

Expand Down Expand Up @@ -80,11 +87,32 @@ class ToastStory extends Story {
label: "displayDuration",
description: "The duration of showing MoonToast.",
enabled: false,
initial: 5,
initial: 3,
min: 1,
max: 10,
);

final widthKnob = context.knobs.nullable.slider(
label: "width",
description: "The width of MoonToast. If null the toast will be as wide as its children.",
enabled: false,
initial: 230,
max: MediaQuery.of(context).size.width,
);

final isPersistentKnob = context.knobs.boolean(
label: "isPersistent",
description:
"Whether the toast is persistent across screens (this will not behave as expected only in Storybook).",
);

final useSafeAreaKnob = context.knobs.boolean(
label: "useSafeArea",
description:
"Whether the toast respects the SafeArea (eg takes into account notches and native system bars)",
initial: true,
);

return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
Expand All @@ -95,11 +123,14 @@ class ToastStory extends Story {
return MoonFilledButton(
label: const Text("Tap me"),
onTap: () {
MoonToast().show(
MoonToast.show(
context,
position: toastPositionKnob ?? MoonToastPosition.bottom,
variant: toastVariantKnob ?? MoonToastVariant.original,
backgroundColor: backgroundColor,
isPersistent: isPersistentKnob,
useSafeArea: useSafeAreaKnob,
width: widthKnob,
toastAlignment: toastAlignmentKnob ?? Alignment.bottomCenter,
variant: toastVariantKnob ?? MoonToastVariant.original,
displayDuration:
displayDurationKnob != null ? Duration(seconds: displayDurationKnob) : null,
borderRadius:
Expand Down
2 changes: 1 addition & 1 deletion lib/src/theme/toast/toast_theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class MoonToastTheme extends ThemeExtension<MoonToastTheme> with DiagnosticableT
MoonToastProperties(
borderRadius: tokens.borders.surfaceSm,
gap: tokens.sizes.x2s,
displayDuration: const Duration(seconds: 5),
displayDuration: const Duration(seconds: 3),
transitionDuration: tokens.transitions.defaultTransitionDuration,
transitionCurve: tokens.transitions.defaultTransitionCurve,
contentPadding: EdgeInsets.all(tokens.sizes.x2s),
Expand Down
144 changes: 83 additions & 61 deletions lib/src/widgets/toast/toast.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,6 @@ import 'package:moon_design/src/utils/extensions.dart';
import 'package:moon_design/src/utils/shape_decoration_premul.dart';
import 'package:moon_design/src/utils/squircle/squircle_border.dart';

enum MoonToastPosition {
top,
bottom,
}

enum MoonToastVariant {
original,
inverted,
Expand All @@ -26,26 +21,27 @@ enum MoonToastVariant {
class MoonToast {
static const double _toastTravelDistance = 64.0;
static const Duration _timeBetweenToasts = Duration(milliseconds: 200);
static final MoonToast _singleton = MoonToast._internal();

final _toastQueue = <_ToastEntry>[];
static final _toastQueue = <_ToastEntry>[];

Timer? _timer;
OverlayEntry? _entry;
static Timer? _timer;
static OverlayEntry? _entry;

/// MDS toast.
factory MoonToast() {
return _singleton;
}

MoonToast._internal();
const MoonToast();

/// Shows a toast.
void show(
/// Show a MoonToast.
static void show(
BuildContext context, {
/// The alignment (position) of the toast.
AlignmentGeometry toastAlignment = Alignment.bottomCenter,

/// Whether the toast is persistent (attaches to root navigator).
bool isPersistent = true,

/// Whether the toast respects the SafeArea (eg takes into account notches and native system bars).
bool useSafeArea = true,

/// The border radius of the toast.
BorderRadiusGeometry? borderRadius,

Expand All @@ -55,6 +51,9 @@ class MoonToast {
/// The horizontal space between toast children.
double? gap,

/// The width of the toast. If null the toast will be as wide as its children.
double? width,

/// Toast display duration.
Duration? displayDuration,

Expand All @@ -67,15 +66,12 @@ class MoonToast {
/// The margin around toast.
EdgeInsetsGeometry? margin,

///The padding around toast children.
/// The padding around toast children.
EdgeInsetsGeometry? padding,

/// Toast shadows.
List<BoxShadow>? toastShadows,

/// The position of the toast.
MoonToastPosition position = MoonToastPosition.bottom,

/// The variant of the toast. Inverted variant flips the color scheme from theming, eg instead of light colors,
/// uses dark colors.
MoonToastVariant variant = MoonToastVariant.original,
Expand Down Expand Up @@ -116,9 +112,8 @@ class MoonToast {

final double effectiveGap = gap ?? context.moonTheme?.toastTheme.properties.gap ?? MoonSizes.sizes.x2s;

final Duration effectiveDisplayDuration = displayDuration ??
context.moonTheme?.toastTheme.properties.displayDuration ??
const Duration(milliseconds: 5000);
final Duration effectiveDisplayDuration =
displayDuration ?? context.moonTheme?.toastTheme.properties.displayDuration ?? const Duration(seconds: 3);

final Duration effectiveTransitionDuration = transitionDuration ??
context.moonTheme?.toastTheme.properties.transitionDuration ??
Expand All @@ -136,32 +131,54 @@ class MoonToast {
final List<BoxShadow> effectiveToastShadows =
toastShadows ?? context.moonTheme?.toastTheme.shadows.toastShadows ?? MoonShadows.light.lg;

final effectiveContext =
isPersistent ? (Navigator.maybeOf(context, rootNavigator: true)?.context ?? context) : context;

final CapturedThemes themes = InheritedTheme.capture(
from: context,
to: Navigator.of(context, rootNavigator: isPersistent).context,
to: Navigator.of(effectiveContext).context,
);

final OverlayEntry entry = OverlayEntry(
builder: (_) {
builder: (BuildContext context) {
return TweenAnimationBuilder(
duration: effectiveTransitionDuration,
curve: effectiveTransitionCurve,
tween: Tween(begin: 0.0, end: 1.0),
builder: (context, progress, child) {
return Align(
alignment: position == MoonToastPosition.bottom ? Alignment.bottomCenter : Alignment.topCenter,
child: RepaintBoundary(
child: Transform(
transform: Matrix4.translationValues(
0,
position == MoonToastPosition.bottom
? ((1 - progress) * _toastTravelDistance)
: (-_toastTravelDistance + progress * _toastTravelDistance),
0,
),
child: Opacity(
opacity: progress,
child: child,
builder: (BuildContext context, double progress, Widget? child) {
return SafeArea(
left: useSafeArea,
top: useSafeArea,
right: useSafeArea,
bottom: useSafeArea,
maintainBottomViewPadding: true,
child: Align(
alignment: toastAlignment,
child: RepaintBoundary(
child: Transform(
transform: Matrix4.translationValues(
switch (toastAlignment) {
Alignment.topLeft ||
Alignment.centerLeft ||
Alignment.bottomLeft =>
-_toastTravelDistance + progress * _toastTravelDistance,
Alignment.topRight ||
Alignment.centerRight ||
Alignment.bottomRight =>
(1 - progress) * _toastTravelDistance,
_ => 0
},
switch (toastAlignment) {
Alignment.topCenter => -_toastTravelDistance + progress * _toastTravelDistance,
Alignment.bottomCenter => (1 - progress) * _toastTravelDistance,
_ => 0
},
0,
),
child: Opacity(
opacity: progress,
child: child,
),
),
),
),
Expand All @@ -177,6 +194,7 @@ class MoonToast {
child: Container(
margin: margin ?? resolvedContentPadding,
padding: resolvedContentPadding,
width: width,
decoration: decoration ??
ShapeDecorationWithPremultipliedAlpha(
color: effectiveBackgroundColor,
Expand All @@ -187,13 +205,14 @@ class MoonToast {
),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: width != null ? MainAxisAlignment.spaceBetween : MainAxisAlignment.center,
textDirection: Directionality.of(context),
children: [
if (leading != null) ...[
leading,
SizedBox(width: effectiveGap),
],
title,
Flexible(child: title),
if (trailing != null) ...[
SizedBox(width: effectiveGap),
trailing,
Expand All @@ -210,7 +229,7 @@ class MoonToast {
);

final toastEntry = _ToastEntry(
buildContext: context,
buildContext: effectiveContext,
overlayEntry: entry,
);

Expand All @@ -219,37 +238,40 @@ class MoonToast {
if (_timer == null) _showToastOverlay(duration: effectiveDisplayDuration);
}

void _showToastOverlay({
required Duration duration,
bool isPersistent = false,
}) {
/// Clear the toast queue.
static void clearToastQueue() {
_timer?.cancel();
_timer = null;

_entry?.remove();
_entry = null;

_toastQueue.clear();
}

static void _showToastOverlay({required Duration duration}) {
if (_toastQueue.isEmpty) {
_entry = null;
return;
}

final toastEntry = _toastQueue.removeAt(0);

if (!toastEntry.buildContext.mounted) {
clearToastQueue();
return;
}

_entry = toastEntry.overlayEntry;
_timer = Timer(duration, () => _removeToastOverlay(duration: duration));

Future.delayed(_timeBetweenToasts, () {
OverlayState? overlay;

if (isPersistent) {
overlay = Navigator.of(
toastEntry.buildContext,
rootNavigator: true,
).overlay;
} else {
overlay = Overlay.of(toastEntry.buildContext);
}

overlay?.insert(_entry!);
});
Future.delayed(
_timeBetweenToasts,
() => Navigator.of(toastEntry.buildContext).overlay?.insert(_entry!),
);
}

void _removeToastOverlay({required Duration duration}) {
static void _removeToastOverlay({required Duration duration}) {
_timer?.cancel();
_timer = null;

Expand Down

0 comments on commit 4470593

Please sign in to comment.