// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/widgets.dart'; import 'banner_theme.dart'; import 'divider.dart'; import 'material.dart'; import 'scaffold.dart'; import 'theme.dart'; const Duration _materialBannerTransitionDuration = Duration(milliseconds: 250); const Curve _materialBannerHeightCurve = Curves.fastOutSlowIn; /// Specify how a [MaterialBanner] was closed. /// /// The [ScaffoldMessengerState.showMaterialBanner] function returns a /// [ScaffoldFeatureController]. The value of the controller's closed property /// is a Future that resolves to a MaterialBannerClosedReason. Applications that need /// to know how a [MaterialBanner] was closed can use this value. /// /// Example: /// /// ```dart /// ScaffoldMessenger.of(context).showMaterialBanner( /// MaterialBanner( ... ) /// ).closed.then((MaterialBannerClosedReason reason) { /// ... /// }); /// ``` enum MaterialBannerClosedReason { /// The material banner was closed through a [SemanticsAction.dismiss]. dismiss, /// The material banner was closed by a user's swipe. swipe, /// The material banner was closed by the [ScaffoldFeatureController] close callback /// or by calling [ScaffoldMessengerState.hideCurrentMaterialBanner] directly. hide, /// The material banner was closed by a call to [ScaffoldMessengerState.removeCurrentMaterialBanner]. remove, } /// A Material Design banner. /// /// A banner displays an important, succinct message, and provides actions for /// users to address (or dismiss the banner). A user action is required for it /// to be dismissed. /// /// Banners should be displayed at the top of the screen, below a top app bar. /// They are persistent and non-modal, allowing the user to either ignore them or /// interact with them at any time. /// /// {@tool dartpad} /// Banners placed directly into the widget tree are static. /// /// ** See code in examples/api/lib/material/banner/material_banner.0.dart ** /// {@end-tool} /// /// {@tool dartpad} /// MaterialBanner's can also be presented through a [ScaffoldMessenger]. /// Here is an example where ScaffoldMessengerState.showMaterialBanner() is used to show the MaterialBanner. /// /// ** See code in examples/api/lib/material/banner/material_banner.1.dart ** /// {@end-tool} /// /// The [actions] will be placed beside the [content] if there is only one. /// Otherwise, the [actions] will be placed below the [content]. Use /// [forceActionsBelow] to override this behavior. /// /// If the [actions] placed below the [content], they will be laid out in a row. /// If there isn't sufficient room to display everything, they are laid out /// in a column instead. /// /// The [actions] and [content] must be provided. An optional leading widget /// (typically an [Image]) can also be provided. The [contentTextStyle] and /// [backgroundColor] can be provided to customize the banner. /// /// This widget is unrelated to the widgets library [Banner] widget. class MaterialBanner extends StatefulWidget { /// Creates a [MaterialBanner]. /// /// The [actions], [content], and [forceActionsBelow] must be non-null. /// The [actions].length must be greater than 0. The [elevation] must be null or /// non-negative. const MaterialBanner({ super.key, required this.content, this.contentTextStyle, required this.actions, this.elevation, this.leading, this.backgroundColor, this.padding, this.leadingPadding, this.forceActionsBelow = false, this.overflowAlignment = OverflowBarAlignment.end, this.animation, this.onVisible }) : assert(elevation == null || elevation >= 0.0), assert(content != null), assert(actions != null), assert(forceActionsBelow != null); /// The content of the [MaterialBanner]. /// /// Typically a [Text] widget. final Widget content; /// Style for the text in the [content] of the [MaterialBanner]. /// /// If `null`, [MaterialBannerThemeData.contentTextStyle] is used. If that is /// also `null`, [TextTheme.bodyText2] of [ThemeData.textTheme] is used. final TextStyle? contentTextStyle; /// The set of actions that are displayed at the bottom or trailing side of /// the [MaterialBanner]. /// /// Typically this is a list of [TextButton] widgets. final List<Widget> actions; /// The z-coordinate at which to place the material banner. /// /// This controls the size of the shadow below the material banner. /// /// Defines the banner's [Material.elevation]. /// /// If this property is null, then [MaterialBannerThemeData.elevation] of /// [ThemeData.bannerTheme] is used, if that is also null, the default value is 0. /// If the elevation is 0, the [Scaffold]'s body will be pushed down by the /// MaterialBanner when used with [ScaffoldMessenger]. final double? elevation; /// The (optional) leading widget of the [MaterialBanner]. /// /// Typically an [Icon] widget. final Widget? leading; /// The color of the surface of this [MaterialBanner]. /// /// If `null`, [MaterialBannerThemeData.backgroundColor] is used. If that is /// also `null`, [ColorScheme.surface] of [ThemeData.colorScheme] is used. final Color? backgroundColor; /// The amount of space by which to inset the [content]. /// /// If the [actions] are below the [content], this defaults to /// `EdgeInsetsDirectional.only(start: 16.0, top: 24.0, end: 16.0, bottom: 4.0)`. /// /// If the [actions] are trailing the [content], this defaults to /// `EdgeInsetsDirectional.only(start: 16.0, top: 2.0)`. final EdgeInsetsGeometry? padding; /// The amount of space by which to inset the [leading] widget. /// /// This defaults to `EdgeInsetsDirectional.only(end: 16.0)`. final EdgeInsetsGeometry? leadingPadding; /// An override to force the [actions] to be below the [content] regardless of /// how many there are. /// /// If this is true, the [actions] will be placed below the [content]. If /// this is false, the [actions] will be placed on the trailing side of the /// [content] if [actions]'s length is 1 and below the [content] if greater /// than 1. /// /// Defaults to false. final bool forceActionsBelow; /// The horizontal alignment of the [actions] when the [actions] laid out in a column. /// /// Defaults to [OverflowBarAlignment.end]. final OverflowBarAlignment overflowAlignment; /// The animation driving the entrance and exit of the material banner when presented by the [ScaffoldMessenger]. final Animation<double>? animation; /// Called the first time that the material banner is visible within a [Scaffold] when presented by the [ScaffoldMessenger]. final VoidCallback? onVisible; // API for ScaffoldMessengerState.showMaterialBanner(): /// Creates an animation controller useful for driving a [MaterialBanner]'s entrance and exit animation. static AnimationController createAnimationController({ required TickerProvider vsync }) { return AnimationController( duration: _materialBannerTransitionDuration, debugLabel: 'MaterialBanner', vsync: vsync, ); } /// Creates a copy of this material banner but with the animation replaced with the given animation. /// /// If the original material banner lacks a key, the newly created material banner will /// use the given fallback key. MaterialBanner withAnimation(Animation<double> newAnimation, { Key? fallbackKey }) { return MaterialBanner( key: key ?? fallbackKey, content: content, contentTextStyle: contentTextStyle, actions: actions, elevation: elevation, leading: leading, backgroundColor: backgroundColor, padding: padding, leadingPadding: leadingPadding, forceActionsBelow: forceActionsBelow, overflowAlignment: overflowAlignment, animation: newAnimation, onVisible: onVisible, ); } @override State<MaterialBanner> createState() => _MaterialBannerState(); } class _MaterialBannerState extends State<MaterialBanner> { bool _wasVisible = false; @override void initState() { super.initState(); widget.animation?.addStatusListener(_onAnimationStatusChanged); } @override void didUpdateWidget(MaterialBanner oldWidget) { super.didUpdateWidget(oldWidget); if (widget.animation != oldWidget.animation) { oldWidget.animation?.removeStatusListener(_onAnimationStatusChanged); widget.animation?.addStatusListener(_onAnimationStatusChanged); } } @override void dispose() { widget.animation?.removeStatusListener(_onAnimationStatusChanged); super.dispose(); } void _onAnimationStatusChanged(AnimationStatus animationStatus) { switch (animationStatus) { case AnimationStatus.dismissed: case AnimationStatus.forward: case AnimationStatus.reverse: break; case AnimationStatus.completed: if (widget.onVisible != null && !_wasVisible) { widget.onVisible!(); } _wasVisible = true; } } @override Widget build(BuildContext context) { assert(debugCheckHasMediaQuery(context)); final MediaQueryData mediaQueryData = MediaQuery.of(context); assert(widget.actions.isNotEmpty); final ThemeData theme = Theme.of(context); final MaterialBannerThemeData bannerTheme = MaterialBannerTheme.of(context); final bool isSingleRow = widget.actions.length == 1 && !widget.forceActionsBelow; final EdgeInsetsGeometry padding = widget.padding ?? bannerTheme.padding ?? (isSingleRow ? const EdgeInsetsDirectional.only(start: 16.0, top: 2.0) : const EdgeInsetsDirectional.only(start: 16.0, top: 24.0, end: 16.0, bottom: 4.0)); final EdgeInsetsGeometry leadingPadding = widget.leadingPadding ?? bannerTheme.leadingPadding ?? const EdgeInsetsDirectional.only(end: 16.0); final Widget buttonBar = Container( alignment: AlignmentDirectional.centerEnd, constraints: const BoxConstraints(minHeight: 52.0), padding: const EdgeInsets.symmetric(horizontal: 8), child: OverflowBar( overflowAlignment: widget.overflowAlignment, spacing: 8, children: widget.actions, ), ); final double elevation = widget.elevation ?? bannerTheme.elevation ?? 0.0; final Color backgroundColor = widget.backgroundColor ?? bannerTheme.backgroundColor ?? theme.colorScheme.surface; final TextStyle? textStyle = widget.contentTextStyle ?? bannerTheme.contentTextStyle ?? theme.textTheme.bodyText2; Widget materialBanner = Container( margin: EdgeInsets.only(bottom: elevation > 0 ? 10.0 : 0.0), child: Material( elevation: elevation, color: backgroundColor, child: Column( mainAxisSize: MainAxisSize.min, children: <Widget>[ Padding( padding: padding, child: Row( children: <Widget>[ if (widget.leading != null) Padding( padding: leadingPadding, child: widget.leading, ), Expanded( child: DefaultTextStyle( style: textStyle!, child: widget.content, ), ), if (isSingleRow) buttonBar, ], ), ), if (!isSingleRow) buttonBar, if (elevation == 0) const Divider(height: 0), ], ), ), ); // This provides a static banner for backwards compatibility. if (widget.animation == null) { return materialBanner; } materialBanner = SafeArea( child: materialBanner, ); final CurvedAnimation heightAnimation = CurvedAnimation(parent: widget.animation!, curve: _materialBannerHeightCurve); final Animation<Offset> slideOutAnimation = Tween<Offset>( begin: const Offset(0.0, -1.0), end: Offset.zero, ).animate(CurvedAnimation( parent: widget.animation!, curve: const Threshold(0.0), )); materialBanner = Semantics( container: true, liveRegion: true, onDismiss: () { ScaffoldMessenger.of(context).removeCurrentMaterialBanner(reason: MaterialBannerClosedReason.dismiss); }, child: mediaQueryData.accessibleNavigation ? materialBanner : SlideTransition( position: slideOutAnimation, child: materialBanner, ), ); final Widget materialBannerTransition; if (mediaQueryData.accessibleNavigation) { materialBannerTransition = materialBanner; } else { materialBannerTransition = AnimatedBuilder( animation: heightAnimation, builder: (BuildContext context, Widget? child) { return Align( alignment: AlignmentDirectional.bottomStart, heightFactor: heightAnimation.value, child: child, ); }, child: materialBanner, ); } return Hero( tag: '<MaterialBanner Hero tag - ${widget.content}>', child: ClipRect(child: materialBannerTransition), ); } }