[Material] Create material Banner component (#36880)

This PR creates a new material widget for the Banner component. This includes a theme as well. This widget can be dropped into any application, ideally at the top of a listview or scrollview.
This commit is contained in:
rami-a 2019-08-05 09:41:09 -04:00 committed by GitHub
parent 8fdd759225
commit 35b6d668e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 658 additions and 2 deletions

View file

@ -23,6 +23,8 @@ export 'src/material/app_bar.dart';
export 'src/material/app_bar_theme.dart';
export 'src/material/arc.dart';
export 'src/material/back_button.dart';
export 'src/material/banner.dart';
export 'src/material/banner_theme.dart';
export 'src/material/bottom_app_bar.dart';
export 'src/material/bottom_app_bar_theme.dart';
export 'src/material/bottom_navigation_bar.dart';

View file

@ -0,0 +1,166 @@
// Copyright 2019 The Chromium 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 'button_bar.dart';
import 'button_theme.dart';
import 'divider.dart';
import 'theme.dart';
/// 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 nonmodal, allowing the user to either ignore them or
/// interact with them at any time.
///
/// 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.
///
/// 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 StatelessWidget {
/// Creates a [MaterialBanner].
///
/// The [actions], [content], and [forceActionsBelow] must be non-null.
/// The [actions.length] must be greater than 0.
const MaterialBanner({
Key key,
@required this.content,
this.contentTextStyle,
@required this.actions,
this.leading,
this.backgroundColor,
this.padding,
this.leadingPadding,
this.forceActionsBelow = false,
}) : assert(content != null),
assert(actions != null),
assert(forceActionsBelow != null),
super(key: key);
/// 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`, [ThemeData.textTheme.body1] 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 [FlatButton] widgets.
///
/// These widgets will be wrapped in a [ButtonBar], which introduces 8 pixels
/// of padding on each side.
final List<Widget> actions;
/// 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`, [ThemeData.colorScheme.surface] 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.length] is `1` and below the [content] if greater
/// than `1`.
final bool forceActionsBelow;
@override
Widget build(BuildContext context) {
assert(actions.isNotEmpty);
final ThemeData theme = Theme.of(context);
final MaterialBannerThemeData bannerTheme = MaterialBannerTheme.of(context);
final bool isSingleRow = actions.length == 1 && !forceActionsBelow;
final EdgeInsetsGeometry padding = this.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 = this.leadingPadding
?? bannerTheme.padding
?? const EdgeInsetsDirectional.only(end: 16.0);
final Widget buttonBar = ButtonTheme.bar(
layoutBehavior: ButtonBarLayoutBehavior.constrained,
child: ButtonBar(
children: actions,
),
);
final Color backgroundColor = this.backgroundColor
?? bannerTheme.backgroundColor
?? theme.colorScheme.surface;
final TextStyle textStyle = contentTextStyle
?? bannerTheme.contentTextStyle
?? theme.textTheme.body1;
return Container(
color: backgroundColor,
child: Column(
children: <Widget>[
Padding(
padding: padding,
child: Row(
children: <Widget>[
if (leading != null)
Padding(
padding: leadingPadding,
child: leading,
),
Flexible(
child: DefaultTextStyle(
style: textStyle,
child: content,
),
),
if (isSingleRow)
buttonBar,
],
),
),
if (!isSingleRow)
buttonBar,
const Divider(height: 0),
],
),
);
}
}

View file

@ -0,0 +1,149 @@
// Copyright 2019 The Chromium 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/foundation.dart';
import 'package:flutter/widgets.dart';
import 'theme.dart';
/// Defines the visual properties of [MaterialBanner] widgets.
///
/// Descendant widgets obtain the current [MaterialBannerThemeData] object using
/// `MaterialBannerTheme.of(context)`. Instances of [MaterialBannerThemeData]
/// can be customized with [MaterialBannerThemeData.copyWith].
///
/// Typically a [MaterialBannerThemeData] is specified as part of the overall
/// [Theme] with [ThemeData.bannerTheme].
///
/// All [MaterialBannerThemeData] properties are `null` by default. When null,
/// the [MaterialBanner] will provide its own defaults.
///
/// See also:
///
/// * [ThemeData], which describes the overall theme information for the
/// application.
class MaterialBannerThemeData extends Diagnosticable {
/// Creates a theme that can be used for [MaterialBannerTheme] or
/// [ThemeData.bannerTheme].
const MaterialBannerThemeData({
this.backgroundColor,
this.contentTextStyle,
this.padding,
this.leadingPadding,
});
/// The background color of a [MaterialBanner].
final Color backgroundColor;
/// Used to configure the [DefaultTextStyle] for the [MaterialBanner.content]
/// widget.
final TextStyle contentTextStyle;
/// The amount of space by which to inset [MaterialBanner.content].
final EdgeInsetsGeometry padding;
/// The amount of space by which to inset [MaterialBanner.leading].
final EdgeInsetsGeometry leadingPadding;
/// Creates a copy of this object with the given fields replaced with the
/// new values.
MaterialBannerThemeData copyWith({
Color backgroundColor,
TextStyle contentTextStyle,
EdgeInsetsGeometry padding,
EdgeInsetsGeometry leadingPadding,
}) {
return MaterialBannerThemeData(
backgroundColor: backgroundColor ?? this.backgroundColor,
contentTextStyle: contentTextStyle ?? this.contentTextStyle,
padding: padding ?? this.padding,
leadingPadding: leadingPadding ?? this.leadingPadding,
);
}
/// Linearly interpolate between two Banner themes.
///
/// The argument `t` must not be null.
///
/// {@macro dart.ui.shadow.lerp}
static MaterialBannerThemeData lerp(MaterialBannerThemeData a, MaterialBannerThemeData b, double t) {
assert(t != null);
return MaterialBannerThemeData(
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
contentTextStyle: TextStyle.lerp(a?.contentTextStyle, b?.contentTextStyle, t),
padding: EdgeInsets.lerp(a?.padding, b?.padding, t),
leadingPadding: EdgeInsets.lerp(a?.leadingPadding, b?.leadingPadding, t),
);
}
@override
int get hashCode {
return hashValues(
backgroundColor,
contentTextStyle,
padding,
leadingPadding,
);
}
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (other.runtimeType != runtimeType)
return false;
final MaterialBannerThemeData typedOther = other;
return typedOther.backgroundColor == backgroundColor
&& typedOther.contentTextStyle == contentTextStyle
&& typedOther.padding == padding
&& typedOther.leadingPadding == leadingPadding;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(ColorProperty('backgroundColor', backgroundColor, defaultValue: null));
properties.add(DiagnosticsProperty<TextStyle>('contentTextStyle', contentTextStyle, defaultValue: null));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('leadingPadding', leadingPadding, defaultValue: null));
}
}
/// An inherited widget that defines the configuration for
/// [MaterialBanner]s in this widget's subtree.
///
/// Values specified here are used for [MaterialBanner] properties that are not
/// given an explicit non-null value.
class MaterialBannerTheme extends InheritedWidget {
/// Creates a banner theme that controls the configurations for
/// [MaterialBanner]s in its widget subtree.
const MaterialBannerTheme({
Key key,
this.data,
Widget child,
}) : super(key: key, child: child);
/// The properties for descendant [MaterialBanner] widgets.
final MaterialBannerThemeData data;
/// The closest instance of this class's [data] value that encloses the given
/// context.
///
/// If there is no ancestor, it returns [ThemeData.bannerTheme]. Applications
/// can assume that the returned value will not be null.
///
/// Typical usage is as follows:
///
/// ```dart
/// MaterialBannerThemeData theme = MaterialBannerTheme.of(context);
/// ```
static MaterialBannerThemeData of(BuildContext context) {
final MaterialBannerTheme popupMenuTheme = context.inheritFromWidgetOfExactType(MaterialBannerTheme);
return popupMenuTheme?.data ?? Theme.of(context).bannerTheme;
}
@override
bool updateShouldNotify(MaterialBannerTheme oldWidget) => data != oldWidget.data;
}

View file

@ -10,6 +10,7 @@ import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'app_bar_theme.dart';
import 'banner_theme.dart';
import 'bottom_app_bar_theme.dart';
import 'bottom_sheet_theme.dart';
import 'button_theme.dart';
@ -176,6 +177,7 @@ class ThemeData extends Diagnosticable {
SnackBarThemeData snackBarTheme,
BottomSheetThemeData bottomSheetTheme,
PopupMenuThemeData popupMenuTheme,
MaterialBannerThemeData bannerTheme,
}) {
brightness ??= Brightness.light;
final bool isDark = brightness == Brightness.dark;
@ -279,6 +281,7 @@ class ThemeData extends Diagnosticable {
snackBarTheme ??= const SnackBarThemeData();
bottomSheetTheme ??= const BottomSheetThemeData();
popupMenuTheme ??= const PopupMenuThemeData();
bannerTheme ??= const MaterialBannerThemeData();
return ThemeData.raw(
brightness: brightness,
@ -340,6 +343,7 @@ class ThemeData extends Diagnosticable {
snackBarTheme: snackBarTheme,
bottomSheetTheme: bottomSheetTheme,
popupMenuTheme: popupMenuTheme,
bannerTheme: bannerTheme,
);
}
@ -413,6 +417,7 @@ class ThemeData extends Diagnosticable {
@required this.snackBarTheme,
@required this.bottomSheetTheme,
@required this.popupMenuTheme,
@required this.bannerTheme,
}) : assert(brightness != null),
assert(primaryColor != null),
assert(primaryColorBrightness != null),
@ -468,7 +473,8 @@ class ThemeData extends Diagnosticable {
assert(typography != null),
assert(snackBarTheme != null),
assert(bottomSheetTheme != null),
assert(popupMenuTheme != null);
assert(popupMenuTheme != null),
assert(bannerTheme != null);
// Warning: make sure these properties are in the exact same order as in
// hashValues() and in the raw constructor and in the order of fields in
@ -798,6 +804,9 @@ class ThemeData extends Diagnosticable {
/// popup menus.
final PopupMenuThemeData popupMenuTheme;
/// A theme for customizing the color and text style of a [MaterialBanner].
final MaterialBannerThemeData bannerTheme;
/// Creates a copy of this theme but with the given fields replaced with the new values.
ThemeData copyWith({
Brightness brightness,
@ -859,6 +868,7 @@ class ThemeData extends Diagnosticable {
SnackBarThemeData snackBarTheme,
BottomSheetThemeData bottomSheetTheme,
PopupMenuThemeData popupMenuTheme,
MaterialBannerThemeData bannerTheme,
}) {
cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault();
return ThemeData.raw(
@ -921,6 +931,7 @@ class ThemeData extends Diagnosticable {
snackBarTheme: snackBarTheme ?? this.snackBarTheme,
bottomSheetTheme: bottomSheetTheme ?? this.bottomSheetTheme,
popupMenuTheme: popupMenuTheme ?? this.popupMenuTheme,
bannerTheme: bannerTheme ?? this.bannerTheme,
);
}
@ -1061,6 +1072,7 @@ class ThemeData extends Diagnosticable {
snackBarTheme: SnackBarThemeData.lerp(a.snackBarTheme, b.snackBarTheme, t),
bottomSheetTheme: BottomSheetThemeData.lerp(a.bottomSheetTheme, b.bottomSheetTheme, t),
popupMenuTheme: PopupMenuThemeData.lerp(a.popupMenuTheme, b.popupMenuTheme, t),
bannerTheme: MaterialBannerThemeData.lerp(a.bannerTheme, b.bannerTheme, t),
);
}
@ -1128,7 +1140,8 @@ class ThemeData extends Diagnosticable {
(otherData.cupertinoOverrideTheme == cupertinoOverrideTheme) &&
(otherData.snackBarTheme == snackBarTheme) &&
(otherData.bottomSheetTheme == bottomSheetTheme) &&
(otherData.popupMenuTheme == popupMenuTheme);
(otherData.popupMenuTheme == popupMenuTheme) &&
(otherData.bannerTheme == bannerTheme);
}
@override
@ -1196,6 +1209,7 @@ class ThemeData extends Diagnosticable {
snackBarTheme,
bottomSheetTheme,
popupMenuTheme,
bannerTheme,
];
return hashList(values);
}
@ -1260,6 +1274,7 @@ class ThemeData extends Diagnosticable {
properties.add(DiagnosticsProperty<SnackBarThemeData>('snackBarTheme', snackBarTheme, defaultValue: defaultData.snackBarTheme));
properties.add(DiagnosticsProperty<BottomSheetThemeData>('bottomSheetTheme', bottomSheetTheme, defaultValue: defaultData.bottomSheetTheme));
properties.add(DiagnosticsProperty<PopupMenuThemeData>('popupMenuTheme', popupMenuTheme, defaultValue: defaultData.popupMenuTheme));
properties.add(DiagnosticsProperty<MaterialBannerThemeData>('bannerTheme', bannerTheme, defaultValue: defaultData.bannerTheme));
}
}

View file

@ -0,0 +1,134 @@
// Copyright 2019 The Chromium 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/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Custom background color respected', (WidgetTester tester) async {
const Color color = Colors.pink;
await tester.pumpWidget(
MaterialApp(
home: MaterialBanner(
backgroundColor: color,
content: const Text('I am a banner'),
actions: <Widget>[
FlatButton(
child: const Text('Action'),
onPressed: () { },
),
],
),
),
);
final Container container = _getContainerFromBanner(tester);
expect(container.decoration, const BoxDecoration(color: color));
});
testWidgets('Custom content TextStyle respected', (WidgetTester tester) async {
const String contentText = 'Content';
const TextStyle contentTextStyle = TextStyle(color: Colors.pink);
await tester.pumpWidget(
MaterialApp(
home: MaterialBanner(
contentTextStyle: contentTextStyle,
content: const Text(contentText),
actions: <Widget>[
FlatButton(
child: const Text('Action'),
onPressed: () { },
),
],
),
),
);
final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText);
expect(content.text.style, contentTextStyle);
});
testWidgets('Actions laid out below content if more than one action', (WidgetTester tester) async {
const String contentText = 'Content';
await tester.pumpWidget(
MaterialApp(
home: MaterialBanner(
content: const Text(contentText),
actions: <Widget>[
FlatButton(
child: const Text('Action 1'),
onPressed: () { },
),
FlatButton(
child: const Text('Action 2'),
onPressed: () { },
),
],
),
),
);
final Offset contentBottomLeft = tester.getBottomLeft(find.text(contentText));
final Offset actionsTopRight = tester.getTopLeft(find.byType(ButtonBar));
expect(contentBottomLeft.dy, lessThan(actionsTopRight.dy));
expect(contentBottomLeft.dx, greaterThan(actionsTopRight.dx));
});
testWidgets('Actions laid out beside content if only one action', (WidgetTester tester) async {
const String contentText = 'Content';
await tester.pumpWidget(
MaterialApp(
home: MaterialBanner(
content: const Text(contentText),
actions: <Widget>[
FlatButton(
child: const Text('Action'),
onPressed: () { },
),
],
),
),
);
final Offset contentBottomLeft = tester.getBottomLeft(find.text(contentText));
final Offset actionsTopRight = tester.getTopRight(find.byType(ButtonBar));
expect(contentBottomLeft.dy, greaterThan(actionsTopRight.dy));
expect(contentBottomLeft.dx, lessThan(actionsTopRight.dx));
});
testWidgets('Actions laid out below content if forced override', (WidgetTester tester) async {
const String contentText = 'Content';
await tester.pumpWidget(
MaterialApp(
home: MaterialBanner(
forceActionsBelow: true,
content: const Text(contentText),
actions: <Widget>[
FlatButton(
child: const Text('Action'),
onPressed: () { },
),
],
),
),
);
final Offset contentBottomLeft = tester.getBottomLeft(find.text(contentText));
final Offset actionsTopRight = tester.getTopLeft(find.byType(ButtonBar));
expect(contentBottomLeft.dy, lessThan(actionsTopRight.dy));
expect(contentBottomLeft.dx, greaterThan(actionsTopRight.dx));
});
}
Container _getContainerFromBanner(WidgetTester tester) {
return tester.widget<Container>(find.descendant(of: find.byType(MaterialBanner), matching: find.byType(Container)).first);
}
RenderParagraph _getTextRenderObjectFromDialog(WidgetTester tester, String text) {
return tester.element<StatelessElement>(find.descendant(of: find.byType(MaterialBanner), matching: find.text(text))).renderObject;
}

View file

@ -0,0 +1,190 @@
// Copyright 2019 The Chromium 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/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('MaterialBannerThemeData copyWith, ==, hashCode basics', () {
expect(const MaterialBannerThemeData(), const MaterialBannerThemeData().copyWith());
expect(const MaterialBannerThemeData().hashCode, const MaterialBannerThemeData().copyWith().hashCode);
});
test('MaterialBannerThemeData null fields by default', () {
const MaterialBannerThemeData bannerTheme = MaterialBannerThemeData();
expect(bannerTheme.backgroundColor, null);
expect(bannerTheme.contentTextStyle, null);
});
testWidgets('Default MaterialBannerThemeData debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const MaterialBannerThemeData().debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(description, <String>[]);
});
testWidgets('MaterialBannerThemeData implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const MaterialBannerThemeData(
backgroundColor: Color(0xFFFFFFFF),
contentTextStyle: TextStyle(color: Color(0xFFFFFFFF)),
).debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(description, <String>[
'backgroundColor: Color(0xffffffff)',
'contentTextStyle: TextStyle(inherit: true, color: Color(0xffffffff))',
]);
});
testWidgets('Passing no MaterialBannerThemeData returns defaults', (WidgetTester tester) async {
const String contentText = 'Content';
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: MaterialBanner(
content: const Text(contentText),
actions: <Widget>[
FlatButton(
child: const Text('Action'),
onPressed: () { },
),
],
),
),
));
final Container container = _getContainerFromBanner(tester);
final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText);
expect(container.decoration, const BoxDecoration(color: Color(0xffffffff)));
expect(content.text.style, Typography().englishLike.body1.merge(Typography().black.body1));
});
testWidgets('MaterialBanner uses values from MaterialBannerThemeData', (WidgetTester tester) async {
final MaterialBannerThemeData bannerTheme = _bannerTheme();
const String contentText = 'Content';
await tester.pumpWidget(MaterialApp(
theme: ThemeData(bannerTheme: bannerTheme),
home: Scaffold(
body: MaterialBanner(
leading: const Icon(Icons.ac_unit),
content: const Text(contentText),
actions: <Widget>[
FlatButton(
child: const Text('Action'),
onPressed: () { },
),
],
),
),
));
final Container container = _getContainerFromBanner(tester);
final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText);
expect(container.decoration, BoxDecoration(color: bannerTheme.backgroundColor));
expect(content.text.style, bannerTheme.contentTextStyle);
final Offset contentTopLeft = tester.getTopLeft(_textFinder(contentText));
final Offset containerTopLeft = tester.getTopLeft(_containerFinder());
final Offset leadingTopLeft = tester.getTopLeft(find.byIcon(Icons.ac_unit));
expect(contentTopLeft.dy - containerTopLeft.dy, 24);
expect(contentTopLeft.dx - containerTopLeft.dx, 39);
expect(leadingTopLeft.dy - containerTopLeft.dy, 19);
expect(leadingTopLeft.dx - containerTopLeft.dx, 10);
});
testWidgets('MaterialBanner widget properties take priority over theme', (WidgetTester tester) async {
const Color backgroundColor = Colors.purple;
const TextStyle textStyle = TextStyle(color: Colors.green);
final MaterialBannerThemeData bannerTheme = _bannerTheme();
const String contentText = 'Content';
await tester.pumpWidget(MaterialApp(
theme: ThemeData(bannerTheme: bannerTheme),
home: Scaffold(
body: MaterialBanner(
backgroundColor: backgroundColor,
leading: const Icon(Icons.ac_unit),
contentTextStyle: textStyle,
content: const Text(contentText),
padding: const EdgeInsets.all(10),
leadingPadding: const EdgeInsets.all(10),
actions: <Widget>[
FlatButton(
child: const Text('Action'),
onPressed: () { },
),
],
),
),
));
final Container container = _getContainerFromBanner(tester);
final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText);
expect(container.decoration, const BoxDecoration(color: backgroundColor));
expect(content.text.style, textStyle);
final Offset contentTopLeft = tester.getTopLeft(_textFinder(contentText));
final Offset containerTopLeft = tester.getTopLeft(_containerFinder());
final Offset leadingTopLeft = tester.getTopLeft(find.byIcon(Icons.ac_unit));
expect(contentTopLeft.dy - containerTopLeft.dy, 29);
expect(contentTopLeft.dx - containerTopLeft.dx, 54);
expect(leadingTopLeft.dy - containerTopLeft.dy, 24);
expect(leadingTopLeft.dx - containerTopLeft.dx, 20);
});
testWidgets('MaterialBanner uses color scheme when necessary', (WidgetTester tester) async {
final ColorScheme colorScheme = const ColorScheme.light().copyWith(surface: Colors.purple);
await tester.pumpWidget(MaterialApp(
theme: ThemeData(colorScheme: colorScheme),
home: Scaffold(
body: MaterialBanner(
content: const Text('Content'),
actions: <Widget>[
FlatButton(
child: const Text('Action'),
onPressed: () { },
),
],
),
),
));
final Container container = _getContainerFromBanner(tester);
expect(container.decoration, BoxDecoration(color: colorScheme.surface));
});
}
MaterialBannerThemeData _bannerTheme() {
return const MaterialBannerThemeData(
backgroundColor: Colors.orange,
contentTextStyle: TextStyle(color: Colors.pink),
padding: EdgeInsets.all(5),
leadingPadding: EdgeInsets.all(5),
);
}
Container _getContainerFromBanner(WidgetTester tester) {
return tester.widget<Container>(_containerFinder());
}
Finder _containerFinder() {
return find.descendant(of: find.byType(MaterialBanner), matching: find.byType(Container)).first;
}
RenderParagraph _getTextRenderObjectFromDialog(WidgetTester tester, String text) {
return tester.element<StatelessElement>(_textFinder(text)).renderObject;
}
Finder _textFinder(String text) {
return find.descendant(of: find.byType(MaterialBanner), matching: find.text(text));
}