This repository has been archived by the owner on Feb 22, 2023. It is now read-only.
/
banner.dart
483 lines (422 loc) · 16.1 KB
/
banner.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
// 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';
// Examples can assume:
// late BuildContext context;
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(
/// const MaterialBanner(
/// content: Text('Message...'),
/// actions: <Widget>[
/// // ...
/// ],
/// )
/// ).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.surfaceTintColor,
this.shadowColor,
this.dividerColor,
this.padding,
this.margin,
this.leadingPadding,
this.forceActionsBelow = false,
this.overflowAlignment = OverflowBarAlignment.end,
this.animation,
this.onVisible,
}) : assert(elevation == null || elevation >= 0.0);
/// 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.bodyMedium] 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 color used as an overlay on [backgroundColor] to indicate elevation.
///
/// If null, [MaterialBannerThemeData.surfaceTintColor] is used. If that
/// is also null, the default value is [ColorScheme.surfaceTint].
///
/// See [Material.surfaceTintColor] for more details on how this
/// overlay is applied.
final Color? surfaceTintColor;
/// The color of the shadow below the [MaterialBanner].
///
/// If this property is null, then [MaterialBannerThemeData.shadowColor] of
/// [ThemeData.bannerTheme] is used. If that is also null, the default value
/// is null.
final Color? shadowColor;
/// The color of the divider.
///
/// If this property is null, then [MaterialBannerThemeData.dividerColor] of
/// [ThemeData.bannerTheme] is used. If that is also null, the default value
/// is [ColorScheme.surfaceVariant].
final Color? dividerColor;
/// 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;
/// Empty space to surround the [MaterialBanner].
///
/// If the [margin] is null then this defaults to
/// 0 if the banner's [elevation] is 0, 10 otherwise.
final EdgeInsetsGeometry? margin;
/// 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,
margin: margin,
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 bool accessibleNavigation = MediaQuery.accessibleNavigationOf(context);
assert(widget.actions.isNotEmpty);
final ThemeData theme = Theme.of(context);
final MaterialBannerThemeData bannerTheme = MaterialBannerTheme.of(context);
final MaterialBannerThemeData defaults = theme.useMaterial3 ? _BannerDefaultsM3(context) : _BannerDefaultsM2(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 EdgeInsetsGeometry margin = widget.margin ?? EdgeInsets.only(bottom: elevation > 0 ? 10.0 : 0.0);
final Color backgroundColor = widget.backgroundColor
?? bannerTheme.backgroundColor
?? defaults.backgroundColor!;
final Color? surfaceTintColor = widget.surfaceTintColor
?? bannerTheme.surfaceTintColor
?? defaults.surfaceTintColor;
final Color? shadowColor = widget.shadowColor
?? bannerTheme.shadowColor;
final Color? dividerColor = widget.dividerColor
?? bannerTheme.dividerColor
?? defaults.dividerColor;
final TextStyle? textStyle = widget.contentTextStyle
?? bannerTheme.contentTextStyle
?? defaults.contentTextStyle;
Widget materialBanner = Container(
margin: margin,
child: Material(
elevation: elevation,
color: backgroundColor,
surfaceTintColor: surfaceTintColor,
shadowColor: shadowColor,
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)
Divider(height: 0, color: dividerColor),
],
),
),
);
// 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: accessibleNavigation
? materialBanner
: SlideTransition(
position: slideOutAnimation,
child: materialBanner,
),
);
final Widget materialBannerTransition;
if (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),
);
}
}
class _BannerDefaultsM2 extends MaterialBannerThemeData {
_BannerDefaultsM2(this.context)
: _theme = Theme.of(context),
super(elevation: 0.0);
final BuildContext context;
final ThemeData _theme;
@override
Color? get backgroundColor => _theme.colorScheme.surface;
@override
TextStyle? get contentTextStyle => _theme.textTheme.bodyMedium;
}
// BEGIN GENERATED TOKEN PROPERTIES - Banner
// 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_152
class _BannerDefaultsM3 extends MaterialBannerThemeData {
const _BannerDefaultsM3(this.context)
: super(elevation: 1.0);
final BuildContext context;
@override
Color? get backgroundColor => Theme.of(context).colorScheme.surface;
@override
Color? get surfaceTintColor => Theme.of(context).colorScheme.surfaceTint;
@override
Color? get dividerColor => Theme.of(context).colorScheme.outlineVariant;
@override
TextStyle? get contentTextStyle => Theme.of(context).textTheme.bodyMedium;
}
// END GENERATED TOKEN PROPERTIES - Banner