aves/lib/widgets/common/action_mixins/overlay_snack_bar.dart

262 lines
8.2 KiB
Dart

import 'package:aves/widgets/common/action_mixins/feedback.dart';
import 'package:flutter/material.dart';
// adapted from Flutter `SnackBar` in `/material/snack_bar.dart`
// As of Flutter v3.0.1, `SnackBar` is not customizable enough to add margin
// and ignore pointers in that area, so we use an overlay entry instead.
// This overlay entry is not below a `Scaffold` (which is expected by `SnackBar`
// and `SnackBarAction`), and is not dismissed the same way.
// This adaptation assumes the `SnackBarBehavior.floating` behavior and no animation.
class OverlaySnackBar extends StatefulWidget {
final Widget content;
final Widget? action;
final DismissDirection dismissDirection;
final VoidCallback onDismiss;
final Clip clipBehavior;
const OverlaySnackBar({
super.key,
required this.content,
this.action,
this.dismissDirection = DismissDirection.down,
this.clipBehavior = Clip.hardEdge,
required this.onDismiss,
});
@override
State<OverlaySnackBar> createState() => _OverlaySnackBarState();
}
class _OverlaySnackBarState extends State<OverlaySnackBar> {
@override
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
final ThemeData theme = Theme.of(context);
final ColorScheme colorScheme = theme.colorScheme;
final SnackBarThemeData snackBarTheme = theme.snackBarTheme;
final bool isThemeDark = theme.brightness == Brightness.dark;
final Color buttonColor = isThemeDark ? colorScheme.primary : colorScheme.secondary;
final SnackBarThemeData defaults = theme.useMaterial3 ? _SnackbarDefaultsM3(context) : _SnackbarDefaultsM2(context);
// SnackBar uses a theme that is the opposite brightness from
// the surrounding theme.
final Brightness brightness = isThemeDark ? Brightness.light : Brightness.dark;
// Invert the theme values for Material 2. Material 3 values are tokenized to pre-inverted values.
final ThemeData effectiveTheme = theme.useMaterial3
? theme
: theme.copyWith(
colorScheme: ColorScheme(
primary: colorScheme.onPrimary,
secondary: buttonColor,
surface: colorScheme.onSurface,
background: defaults.backgroundColor!,
error: colorScheme.onError,
onPrimary: colorScheme.primary,
onSecondary: colorScheme.secondary,
onSurface: colorScheme.surface,
onBackground: colorScheme.background,
onError: colorScheme.error,
brightness: brightness,
),
);
final TextStyle? contentTextStyle = snackBarTheme.contentTextStyle ?? defaults.contentTextStyle;
final horizontalPadding = FeedbackMixin.snackBarHorizontalPadding(snackBarTheme);
final padding = EdgeInsetsDirectional.only(start: horizontalPadding, end: widget.action != null ? 0 : horizontalPadding);
const singleLineVerticalPadding = 14.0;
final EdgeInsets margin = snackBarTheme.insetPadding ?? defaults.insetPadding!;
Widget snackBar = Padding(
padding: padding,
child: Row(
children: <Widget>[
Expanded(
child: Container(
padding: widget.action != null ? null : const EdgeInsets.symmetric(vertical: singleLineVerticalPadding),
child: DefaultTextStyle(
style: contentTextStyle!,
child: widget.content,
),
),
),
if (widget.action != null)
TextButtonTheme(
data: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: buttonColor,
padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
),
),
child: widget.action!,
),
],
),
);
final double elevation = snackBarTheme.elevation ?? defaults.elevation!;
final Color backgroundColor = snackBarTheme.backgroundColor ?? defaults.backgroundColor!;
final ShapeBorder? shape = snackBarTheme.shape ?? defaults.shape;
snackBar = Material(
shape: shape,
elevation: elevation,
color: backgroundColor,
child: Theme(
data: effectiveTheme,
child: snackBar,
),
);
snackBar = Padding(
padding: margin,
child: snackBar,
);
snackBar = SafeArea(
top: false,
bottom: false,
child: snackBar,
);
snackBar = Semantics(
container: true,
liveRegion: true,
onDismiss: widget.onDismiss,
child: Dismissible(
key: const Key('dismissible'),
direction: widget.dismissDirection,
resizeDuration: null,
onDismissed: (direction) => widget.onDismiss(),
child: snackBar,
),
);
final Widget snackBarTransition = snackBar;
return Hero(
tag: '<SnackBar Hero tag - ${widget.content}>',
transitionOnUserGestures: true,
child: ClipRect(
clipBehavior: widget.clipBehavior,
child: snackBarTransition,
),
);
}
}
// Hand coded defaults based on Material Design 2.
class _SnackbarDefaultsM2 extends SnackBarThemeData {
_SnackbarDefaultsM2(BuildContext context)
: _theme = Theme.of(context),
_colors = Theme.of(context).colorScheme,
super(elevation: 6.0);
late final ThemeData _theme;
late final ColorScheme _colors;
@override
Color get backgroundColor => _theme.brightness == Brightness.light ? Color.alphaBlend(_colors.onSurface.withOpacity(0.80), _colors.surface) : _colors.onSurface;
@override
TextStyle? get contentTextStyle => ThemeData(brightness: _theme.brightness == Brightness.light ? Brightness.dark : Brightness.light).textTheme.titleMedium;
@override
SnackBarBehavior get behavior => SnackBarBehavior.fixed;
@override
Color get actionTextColor => _colors.secondary;
@override
Color get disabledActionTextColor => _colors.onSurface.withOpacity(_theme.brightness == Brightness.light ? 0.38 : 0.3);
@override
ShapeBorder get shape => const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(4.0),
),
);
@override
EdgeInsets get insetPadding => const EdgeInsets.fromLTRB(15.0, 5.0, 15.0, 10.0);
@override
bool get showCloseIcon => false;
@override
Color get closeIconColor => _colors.onSurface;
@override
double get actionOverflowThreshold => 0.25;
}
// BEGIN GENERATED TOKEN PROPERTIES - Snackbar
// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
// Token database version: v0_162
class _SnackbarDefaultsM3 extends SnackBarThemeData {
_SnackbarDefaultsM3(this.context);
final BuildContext context;
late final ThemeData _theme = Theme.of(context);
late final ColorScheme _colors = _theme.colorScheme;
@override
Color get backgroundColor => _colors.inverseSurface;
@override
Color get actionTextColor => MaterialStateColor.resolveWith((states) {
if (states.contains(MaterialState.disabled)) {
return _colors.inversePrimary;
}
if (states.contains(MaterialState.pressed)) {
return _colors.inversePrimary;
}
if (states.contains(MaterialState.hovered)) {
return _colors.inversePrimary;
}
if (states.contains(MaterialState.focused)) {
return _colors.inversePrimary;
}
return _colors.inversePrimary;
});
@override
Color get disabledActionTextColor => _colors.inversePrimary;
@override
TextStyle get contentTextStyle => Theme.of(context).textTheme.bodyMedium!.copyWith(
color: _colors.onInverseSurface,
);
@override
double get elevation => 6.0;
@override
ShapeBorder get shape => const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)));
@override
SnackBarBehavior get behavior => SnackBarBehavior.fixed;
@override
EdgeInsets get insetPadding => const EdgeInsets.fromLTRB(15.0, 5.0, 15.0, 10.0);
@override
bool get showCloseIcon => false;
@override
Color? get closeIconColor => _colors.onInverseSurface;
@override
double get actionOverflowThreshold => 0.25;
}
// END GENERATED TOKEN PROPERTIES - Snackbar