mirror of
https://github.com/flutter/flutter
synced 2024-10-13 03:32:55 +00:00
[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:
parent
8fdd759225
commit
35b6d668e1
|
@ -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';
|
||||
|
|
166
packages/flutter/lib/src/material/banner.dart
Normal file
166
packages/flutter/lib/src/material/banner.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
149
packages/flutter/lib/src/material/banner_theme.dart
Normal file
149
packages/flutter/lib/src/material/banner_theme.dart
Normal 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;
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
134
packages/flutter/test/material/banner_test.dart
Normal file
134
packages/flutter/test/material/banner_test.dart
Normal 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;
|
||||
}
|
190
packages/flutter/test/material/banner_theme_test.dart
Normal file
190
packages/flutter/test/material/banner_theme_test.dart
Normal 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));
|
||||
}
|
Loading…
Reference in a new issue