[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/app_bar_theme.dart';
|
||||||
export 'src/material/arc.dart';
|
export 'src/material/arc.dart';
|
||||||
export 'src/material/back_button.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.dart';
|
||||||
export 'src/material/bottom_app_bar_theme.dart';
|
export 'src/material/bottom_app_bar_theme.dart';
|
||||||
export 'src/material/bottom_navigation_bar.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 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import 'app_bar_theme.dart';
|
import 'app_bar_theme.dart';
|
||||||
|
import 'banner_theme.dart';
|
||||||
import 'bottom_app_bar_theme.dart';
|
import 'bottom_app_bar_theme.dart';
|
||||||
import 'bottom_sheet_theme.dart';
|
import 'bottom_sheet_theme.dart';
|
||||||
import 'button_theme.dart';
|
import 'button_theme.dart';
|
||||||
@ -176,6 +177,7 @@ class ThemeData extends Diagnosticable {
|
|||||||
SnackBarThemeData snackBarTheme,
|
SnackBarThemeData snackBarTheme,
|
||||||
BottomSheetThemeData bottomSheetTheme,
|
BottomSheetThemeData bottomSheetTheme,
|
||||||
PopupMenuThemeData popupMenuTheme,
|
PopupMenuThemeData popupMenuTheme,
|
||||||
|
MaterialBannerThemeData bannerTheme,
|
||||||
}) {
|
}) {
|
||||||
brightness ??= Brightness.light;
|
brightness ??= Brightness.light;
|
||||||
final bool isDark = brightness == Brightness.dark;
|
final bool isDark = brightness == Brightness.dark;
|
||||||
@ -279,6 +281,7 @@ class ThemeData extends Diagnosticable {
|
|||||||
snackBarTheme ??= const SnackBarThemeData();
|
snackBarTheme ??= const SnackBarThemeData();
|
||||||
bottomSheetTheme ??= const BottomSheetThemeData();
|
bottomSheetTheme ??= const BottomSheetThemeData();
|
||||||
popupMenuTheme ??= const PopupMenuThemeData();
|
popupMenuTheme ??= const PopupMenuThemeData();
|
||||||
|
bannerTheme ??= const MaterialBannerThemeData();
|
||||||
|
|
||||||
return ThemeData.raw(
|
return ThemeData.raw(
|
||||||
brightness: brightness,
|
brightness: brightness,
|
||||||
@ -340,6 +343,7 @@ class ThemeData extends Diagnosticable {
|
|||||||
snackBarTheme: snackBarTheme,
|
snackBarTheme: snackBarTheme,
|
||||||
bottomSheetTheme: bottomSheetTheme,
|
bottomSheetTheme: bottomSheetTheme,
|
||||||
popupMenuTheme: popupMenuTheme,
|
popupMenuTheme: popupMenuTheme,
|
||||||
|
bannerTheme: bannerTheme,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -413,6 +417,7 @@ class ThemeData extends Diagnosticable {
|
|||||||
@required this.snackBarTheme,
|
@required this.snackBarTheme,
|
||||||
@required this.bottomSheetTheme,
|
@required this.bottomSheetTheme,
|
||||||
@required this.popupMenuTheme,
|
@required this.popupMenuTheme,
|
||||||
|
@required this.bannerTheme,
|
||||||
}) : assert(brightness != null),
|
}) : assert(brightness != null),
|
||||||
assert(primaryColor != null),
|
assert(primaryColor != null),
|
||||||
assert(primaryColorBrightness != null),
|
assert(primaryColorBrightness != null),
|
||||||
@ -468,7 +473,8 @@ class ThemeData extends Diagnosticable {
|
|||||||
assert(typography != null),
|
assert(typography != null),
|
||||||
assert(snackBarTheme != null),
|
assert(snackBarTheme != null),
|
||||||
assert(bottomSheetTheme != 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
|
// 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
|
// hashValues() and in the raw constructor and in the order of fields in
|
||||||
@ -798,6 +804,9 @@ class ThemeData extends Diagnosticable {
|
|||||||
/// popup menus.
|
/// popup menus.
|
||||||
final PopupMenuThemeData popupMenuTheme;
|
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.
|
/// Creates a copy of this theme but with the given fields replaced with the new values.
|
||||||
ThemeData copyWith({
|
ThemeData copyWith({
|
||||||
Brightness brightness,
|
Brightness brightness,
|
||||||
@ -859,6 +868,7 @@ class ThemeData extends Diagnosticable {
|
|||||||
SnackBarThemeData snackBarTheme,
|
SnackBarThemeData snackBarTheme,
|
||||||
BottomSheetThemeData bottomSheetTheme,
|
BottomSheetThemeData bottomSheetTheme,
|
||||||
PopupMenuThemeData popupMenuTheme,
|
PopupMenuThemeData popupMenuTheme,
|
||||||
|
MaterialBannerThemeData bannerTheme,
|
||||||
}) {
|
}) {
|
||||||
cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault();
|
cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault();
|
||||||
return ThemeData.raw(
|
return ThemeData.raw(
|
||||||
@ -921,6 +931,7 @@ class ThemeData extends Diagnosticable {
|
|||||||
snackBarTheme: snackBarTheme ?? this.snackBarTheme,
|
snackBarTheme: snackBarTheme ?? this.snackBarTheme,
|
||||||
bottomSheetTheme: bottomSheetTheme ?? this.bottomSheetTheme,
|
bottomSheetTheme: bottomSheetTheme ?? this.bottomSheetTheme,
|
||||||
popupMenuTheme: popupMenuTheme ?? this.popupMenuTheme,
|
popupMenuTheme: popupMenuTheme ?? this.popupMenuTheme,
|
||||||
|
bannerTheme: bannerTheme ?? this.bannerTheme,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1061,6 +1072,7 @@ class ThemeData extends Diagnosticable {
|
|||||||
snackBarTheme: SnackBarThemeData.lerp(a.snackBarTheme, b.snackBarTheme, t),
|
snackBarTheme: SnackBarThemeData.lerp(a.snackBarTheme, b.snackBarTheme, t),
|
||||||
bottomSheetTheme: BottomSheetThemeData.lerp(a.bottomSheetTheme, b.bottomSheetTheme, t),
|
bottomSheetTheme: BottomSheetThemeData.lerp(a.bottomSheetTheme, b.bottomSheetTheme, t),
|
||||||
popupMenuTheme: PopupMenuThemeData.lerp(a.popupMenuTheme, b.popupMenuTheme, 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.cupertinoOverrideTheme == cupertinoOverrideTheme) &&
|
||||||
(otherData.snackBarTheme == snackBarTheme) &&
|
(otherData.snackBarTheme == snackBarTheme) &&
|
||||||
(otherData.bottomSheetTheme == bottomSheetTheme) &&
|
(otherData.bottomSheetTheme == bottomSheetTheme) &&
|
||||||
(otherData.popupMenuTheme == popupMenuTheme);
|
(otherData.popupMenuTheme == popupMenuTheme) &&
|
||||||
|
(otherData.bannerTheme == bannerTheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -1196,6 +1209,7 @@ class ThemeData extends Diagnosticable {
|
|||||||
snackBarTheme,
|
snackBarTheme,
|
||||||
bottomSheetTheme,
|
bottomSheetTheme,
|
||||||
popupMenuTheme,
|
popupMenuTheme,
|
||||||
|
bannerTheme,
|
||||||
];
|
];
|
||||||
return hashList(values);
|
return hashList(values);
|
||||||
}
|
}
|
||||||
@ -1260,6 +1274,7 @@ class ThemeData extends Diagnosticable {
|
|||||||
properties.add(DiagnosticsProperty<SnackBarThemeData>('snackBarTheme', snackBarTheme, defaultValue: defaultData.snackBarTheme));
|
properties.add(DiagnosticsProperty<SnackBarThemeData>('snackBarTheme', snackBarTheme, defaultValue: defaultData.snackBarTheme));
|
||||||
properties.add(DiagnosticsProperty<BottomSheetThemeData>('bottomSheetTheme', bottomSheetTheme, defaultValue: defaultData.bottomSheetTheme));
|
properties.add(DiagnosticsProperty<BottomSheetThemeData>('bottomSheetTheme', bottomSheetTheme, defaultValue: defaultData.bottomSheetTheme));
|
||||||
properties.add(DiagnosticsProperty<PopupMenuThemeData>('popupMenuTheme', popupMenuTheme, defaultValue: defaultData.popupMenuTheme));
|
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…
x
Reference in New Issue
Block a user