Add PopupMenuTheme to enable theming color, shape, elevation, text style of Menu (#36088)
* [Menu] Create Menu theme * [Menu] Create Menu theme * [Menu] Formatting changes for Menu theme * [Menu] Fix spacing difference in theme_data.dart. * [Menu] Fix spacing difference in theme_data.dart. * Specifying types * Formatting changes * Address PR feedback * Formatting changes * Address PR feedback * Add inherited widget * Add inherited widget * Address PR feedback and add inherited widget. * Formatting changes. * Address PR feedback * Address PR feedback * Address PR feedback * Address PR feedback
This commit is contained in:
parent
a3a350df1c
commit
63992e4fde
@ -83,6 +83,7 @@ export 'src/material/page.dart';
|
|||||||
export 'src/material/page_transitions_theme.dart';
|
export 'src/material/page_transitions_theme.dart';
|
||||||
export 'src/material/paginated_data_table.dart';
|
export 'src/material/paginated_data_table.dart';
|
||||||
export 'src/material/popup_menu.dart';
|
export 'src/material/popup_menu.dart';
|
||||||
|
export 'src/material/popup_menu_theme.dart';
|
||||||
export 'src/material/progress_indicator.dart';
|
export 'src/material/progress_indicator.dart';
|
||||||
export 'src/material/radio.dart';
|
export 'src/material/radio.dart';
|
||||||
export 'src/material/radio_list_tile.dart';
|
export 'src/material/radio_list_tile.dart';
|
||||||
|
@ -16,6 +16,7 @@ import 'ink_well.dart';
|
|||||||
import 'list_tile.dart';
|
import 'list_tile.dart';
|
||||||
import 'material.dart';
|
import 'material.dart';
|
||||||
import 'material_localizations.dart';
|
import 'material_localizations.dart';
|
||||||
|
import 'popup_menu_theme.dart';
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
|
|
||||||
// Examples can assume:
|
// Examples can assume:
|
||||||
@ -171,6 +172,7 @@ class PopupMenuItem<T> extends PopupMenuEntry<T> {
|
|||||||
this.value,
|
this.value,
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
this.height = _kMenuItemHeight,
|
this.height = _kMenuItemHeight,
|
||||||
|
this.textStyle,
|
||||||
@required this.child,
|
@required this.child,
|
||||||
}) : assert(enabled != null),
|
}) : assert(enabled != null),
|
||||||
assert(height != null),
|
assert(height != null),
|
||||||
@ -191,6 +193,12 @@ class PopupMenuItem<T> extends PopupMenuEntry<T> {
|
|||||||
@override
|
@override
|
||||||
final double height;
|
final double height;
|
||||||
|
|
||||||
|
/// The text style of the popup menu entry.
|
||||||
|
///
|
||||||
|
/// If this property is null, then [PopupMenuThemeData.textStyle] is used.
|
||||||
|
/// If [PopupMenuThemeData.textStyle] is also null, then [ThemeData.textTheme.subhead] is used.
|
||||||
|
final TextStyle textStyle;
|
||||||
|
|
||||||
/// The widget below this widget in the tree.
|
/// The widget below this widget in the tree.
|
||||||
///
|
///
|
||||||
/// Typically a single-line [ListTile] (for menus with icons) or a [Text]. An
|
/// Typically a single-line [ListTile] (for menus with icons) or a [Text]. An
|
||||||
@ -245,7 +253,9 @@ class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ThemeData theme = Theme.of(context);
|
final ThemeData theme = Theme.of(context);
|
||||||
TextStyle style = theme.textTheme.subhead;
|
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
|
||||||
|
TextStyle style = widget.textStyle ?? popupMenuTheme.textStyle ?? theme.textTheme.subhead;
|
||||||
|
|
||||||
if (!widget.enabled)
|
if (!widget.enabled)
|
||||||
style = style.copyWith(color: theme.disabledColor);
|
style = style.copyWith(color: theme.disabledColor);
|
||||||
|
|
||||||
@ -433,6 +443,7 @@ class _PopupMenu<T> extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final double unit = 1.0 / (route.items.length + 1.5); // 1.0 for the width and 0.5 for the last item's fade.
|
final double unit = 1.0 / (route.items.length + 1.5); // 1.0 for the width and 0.5 for the last item's fade.
|
||||||
final List<Widget> children = <Widget>[];
|
final List<Widget> children = <Widget>[];
|
||||||
|
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
|
||||||
|
|
||||||
for (int i = 0; i < route.items.length; i += 1) {
|
for (int i = 0; i < route.items.length; i += 1) {
|
||||||
final double start = (i + 1) * unit;
|
final double start = (i + 1) * unit;
|
||||||
@ -486,8 +497,10 @@ class _PopupMenu<T> extends StatelessWidget {
|
|||||||
return Opacity(
|
return Opacity(
|
||||||
opacity: opacity.evaluate(route.animation),
|
opacity: opacity.evaluate(route.animation),
|
||||||
child: Material(
|
child: Material(
|
||||||
|
shape: route.shape ?? popupMenuTheme.shape,
|
||||||
|
color: route.color ?? popupMenuTheme.color,
|
||||||
type: MaterialType.card,
|
type: MaterialType.card,
|
||||||
elevation: route.elevation,
|
elevation: route.elevation ?? popupMenuTheme.elevation ?? 8.0,
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: AlignmentDirectional.topEnd,
|
alignment: AlignmentDirectional.topEnd,
|
||||||
widthFactor: width.evaluate(route.animation),
|
widthFactor: width.evaluate(route.animation),
|
||||||
@ -589,8 +602,11 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
|
|||||||
this.initialValue,
|
this.initialValue,
|
||||||
this.elevation,
|
this.elevation,
|
||||||
this.theme,
|
this.theme,
|
||||||
|
this.popupMenuTheme,
|
||||||
this.barrierLabel,
|
this.barrierLabel,
|
||||||
this.semanticLabel,
|
this.semanticLabel,
|
||||||
|
this.shape,
|
||||||
|
this.color,
|
||||||
});
|
});
|
||||||
|
|
||||||
final RelativeRect position;
|
final RelativeRect position;
|
||||||
@ -599,6 +615,9 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
|
|||||||
final double elevation;
|
final double elevation;
|
||||||
final ThemeData theme;
|
final ThemeData theme;
|
||||||
final String semanticLabel;
|
final String semanticLabel;
|
||||||
|
final ShapeBorder shape;
|
||||||
|
final Color color;
|
||||||
|
final PopupMenuThemeData popupMenuTheme;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Animation<double> createAnimation() {
|
Animation<double> createAnimation() {
|
||||||
@ -636,6 +655,8 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget menu = _PopupMenu<T>(route: this, semanticLabel: semanticLabel);
|
Widget menu = _PopupMenu<T>(route: this, semanticLabel: semanticLabel);
|
||||||
|
if (popupMenuTheme != null)
|
||||||
|
menu = PopupMenuTheme(textStyle: popupMenuTheme.textStyle, child: menu);
|
||||||
if (theme != null)
|
if (theme != null)
|
||||||
menu = Theme(data: theme, child: menu);
|
menu = Theme(data: theme, child: menu);
|
||||||
|
|
||||||
@ -717,8 +738,10 @@ Future<T> showMenu<T>({
|
|||||||
@required RelativeRect position,
|
@required RelativeRect position,
|
||||||
@required List<PopupMenuEntry<T>> items,
|
@required List<PopupMenuEntry<T>> items,
|
||||||
T initialValue,
|
T initialValue,
|
||||||
double elevation = 8.0,
|
double elevation,
|
||||||
String semanticLabel,
|
String semanticLabel,
|
||||||
|
ShapeBorder shape,
|
||||||
|
Color color,
|
||||||
}) {
|
}) {
|
||||||
assert(context != null);
|
assert(context != null);
|
||||||
assert(position != null);
|
assert(position != null);
|
||||||
@ -741,7 +764,10 @@ Future<T> showMenu<T>({
|
|||||||
elevation: elevation,
|
elevation: elevation,
|
||||||
semanticLabel: label,
|
semanticLabel: label,
|
||||||
theme: Theme.of(context, shadowThemeOnly: true),
|
theme: Theme.of(context, shadowThemeOnly: true),
|
||||||
|
popupMenuTheme: PopupMenuTheme.of(context),
|
||||||
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
||||||
|
shape: shape,
|
||||||
|
color: color,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -826,12 +852,14 @@ class PopupMenuButton<T> extends StatefulWidget {
|
|||||||
this.onSelected,
|
this.onSelected,
|
||||||
this.onCanceled,
|
this.onCanceled,
|
||||||
this.tooltip,
|
this.tooltip,
|
||||||
this.elevation = 8.0,
|
this.elevation,
|
||||||
this.padding = const EdgeInsets.all(8.0),
|
this.padding = const EdgeInsets.all(8.0),
|
||||||
this.child,
|
this.child,
|
||||||
this.icon,
|
this.icon,
|
||||||
this.offset = Offset.zero,
|
this.offset = Offset.zero,
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
|
this.shape,
|
||||||
|
this.color,
|
||||||
}) : assert(itemBuilder != null),
|
}) : assert(itemBuilder != null),
|
||||||
assert(offset != null),
|
assert(offset != null),
|
||||||
assert(enabled != null),
|
assert(enabled != null),
|
||||||
@ -898,12 +926,28 @@ class PopupMenuButton<T> extends StatefulWidget {
|
|||||||
/// but doesn't currently have anything to show in the menu.
|
/// but doesn't currently have anything to show in the menu.
|
||||||
final bool enabled;
|
final bool enabled;
|
||||||
|
|
||||||
|
/// If provided, the shape used for the menu.
|
||||||
|
///
|
||||||
|
/// If this property is null, then [PopupMenuThemeData.shape] is used.
|
||||||
|
/// If [PopupMenuThemeData.shape] is also null, then the default shape for
|
||||||
|
/// [MaterialType.card] is used. This default shape is a rectangle with
|
||||||
|
/// rounded edges of BorderRadius.circular(2.0).
|
||||||
|
final ShapeBorder shape;
|
||||||
|
|
||||||
|
/// If provided, the background color used for the menu.
|
||||||
|
///
|
||||||
|
/// If this property is null, then [PopupMenuThemeData.color] is used.
|
||||||
|
/// If [PopupMenuThemeData.color] is also null, then
|
||||||
|
/// Theme.of(context).cardColor is used.
|
||||||
|
final Color color;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_PopupMenuButtonState<T> createState() => _PopupMenuButtonState<T>();
|
_PopupMenuButtonState<T> createState() => _PopupMenuButtonState<T>();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
|
class _PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
|
||||||
void showButtonMenu() {
|
void showButtonMenu() {
|
||||||
|
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
|
||||||
final RenderBox button = context.findRenderObject();
|
final RenderBox button = context.findRenderObject();
|
||||||
final RenderBox overlay = Overlay.of(context).context.findRenderObject();
|
final RenderBox overlay = Overlay.of(context).context.findRenderObject();
|
||||||
final RelativeRect position = RelativeRect.fromRect(
|
final RelativeRect position = RelativeRect.fromRect(
|
||||||
@ -918,10 +962,12 @@ class _PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
|
|||||||
if (items.isNotEmpty) {
|
if (items.isNotEmpty) {
|
||||||
showMenu<T>(
|
showMenu<T>(
|
||||||
context: context,
|
context: context,
|
||||||
elevation: widget.elevation,
|
elevation: widget.elevation ?? popupMenuTheme.elevation,
|
||||||
items: items,
|
items: items,
|
||||||
initialValue: widget.initialValue,
|
initialValue: widget.initialValue,
|
||||||
position: position,
|
position: position,
|
||||||
|
shape: widget.shape ?? popupMenuTheme.shape,
|
||||||
|
color: widget.color ?? popupMenuTheme.color,
|
||||||
)
|
)
|
||||||
.then<void>((T newValue) {
|
.then<void>((T newValue) {
|
||||||
if (!mounted)
|
if (!mounted)
|
||||||
|
161
packages/flutter/lib/src/material/popup_menu_theme.dart
Normal file
161
packages/flutter/lib/src/material/popup_menu_theme.dart
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
// 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 'dart:ui' show lerpDouble;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import 'theme.dart';
|
||||||
|
|
||||||
|
/// Defines the visual properties of the routes used to display popup menus
|
||||||
|
/// as well as [PopupMenuItem] and [PopupMenuDivider] widgets.
|
||||||
|
///
|
||||||
|
/// Descendant widgets obtain the current [PopupMenuThemeData] object
|
||||||
|
/// using `PopupMenuTheme.of(context)`. Instances of
|
||||||
|
/// [PopupMenuThemeData] can be customized with
|
||||||
|
/// [PopupMenuThemeData.copyWith].
|
||||||
|
///
|
||||||
|
/// Typically, a [PopupMenuThemeData] is specified as part of the
|
||||||
|
/// overall [Theme] with [ThemeData.popupMenuTheme]. Otherwise,
|
||||||
|
/// [PopupMenuTheme] can be used to configure its own widget subtree.
|
||||||
|
///
|
||||||
|
/// All [PopupMenuThemeData] properties are `null` by default.
|
||||||
|
/// If any of these properties are null, the popup menu will provide its
|
||||||
|
/// own defaults.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [ThemeData], which describes the overall theme information for the
|
||||||
|
/// application.
|
||||||
|
class PopupMenuThemeData extends Diagnosticable {
|
||||||
|
/// Creates the set of properties used to configure [PopupMenuTheme].
|
||||||
|
const PopupMenuThemeData({
|
||||||
|
this.color,
|
||||||
|
this.shape,
|
||||||
|
this.elevation,
|
||||||
|
this.textStyle,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The background color of the popup menu.
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
/// The shape of the popup menu.
|
||||||
|
final ShapeBorder shape;
|
||||||
|
|
||||||
|
/// The elevation of the popup menu.
|
||||||
|
final double elevation;
|
||||||
|
|
||||||
|
/// The text style of items in the popup menu.
|
||||||
|
final TextStyle textStyle;
|
||||||
|
|
||||||
|
/// Creates a copy of this object with the given fields replaced with the
|
||||||
|
/// new values.
|
||||||
|
PopupMenuThemeData copyWith({
|
||||||
|
Color color,
|
||||||
|
ShapeBorder shape,
|
||||||
|
double elevation,
|
||||||
|
TextStyle textStyle,
|
||||||
|
}) {
|
||||||
|
return PopupMenuThemeData(
|
||||||
|
color: color ?? this.color,
|
||||||
|
shape: shape ?? this.shape,
|
||||||
|
elevation: elevation ?? this.elevation,
|
||||||
|
textStyle: textStyle ?? this.textStyle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Linearly interpolate between two popup menu themes.
|
||||||
|
///
|
||||||
|
/// If both arguments are null, then null is returned.
|
||||||
|
///
|
||||||
|
/// {@macro dart.ui.shadow.lerp}
|
||||||
|
static PopupMenuThemeData lerp(PopupMenuThemeData a, PopupMenuThemeData b, double t) {
|
||||||
|
assert(t != null);
|
||||||
|
if (a == null && b == null)
|
||||||
|
return null;
|
||||||
|
return PopupMenuThemeData(
|
||||||
|
color: Color.lerp(a?.color, b?.color, t),
|
||||||
|
shape: ShapeBorder.lerp(a?.shape, b?.shape, t),
|
||||||
|
elevation: lerpDouble(a?.elevation, b?.elevation, t),
|
||||||
|
textStyle: TextStyle.lerp(a?.textStyle, b?.textStyle, t),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return hashValues(
|
||||||
|
color,
|
||||||
|
shape,
|
||||||
|
elevation,
|
||||||
|
textStyle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other))
|
||||||
|
return true;
|
||||||
|
if (other.runtimeType != runtimeType)
|
||||||
|
return false;
|
||||||
|
final PopupMenuThemeData typedOther = other;
|
||||||
|
return typedOther.elevation == elevation
|
||||||
|
&& typedOther.color == color
|
||||||
|
&& typedOther.shape == shape
|
||||||
|
&& typedOther.textStyle == textStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
super.debugFillProperties(properties);
|
||||||
|
properties.add(ColorProperty('color', color, defaultValue: null));
|
||||||
|
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
|
||||||
|
properties.add(DoubleProperty('elevation', elevation, defaultValue: null));
|
||||||
|
properties.add(DiagnosticsProperty<TextStyle>('text style', textStyle, defaultValue: null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An inherited widget that defines the configuration for
|
||||||
|
/// popup menus in this widget's subtree.
|
||||||
|
///
|
||||||
|
/// Values specified here are used for popup menu properties that are not
|
||||||
|
/// given an explicit non-null value.
|
||||||
|
class PopupMenuTheme extends InheritedWidget {
|
||||||
|
/// Creates a popup menu theme that controls the configurations for
|
||||||
|
/// popup menus in its widget subtree.
|
||||||
|
PopupMenuTheme({
|
||||||
|
Key key,
|
||||||
|
Color color,
|
||||||
|
ShapeBorder shape,
|
||||||
|
double elevation,
|
||||||
|
TextStyle textStyle,
|
||||||
|
Widget child,
|
||||||
|
}) : data = PopupMenuThemeData(
|
||||||
|
color: color,
|
||||||
|
shape: shape,
|
||||||
|
elevation: elevation,
|
||||||
|
textStyle: textStyle,
|
||||||
|
),
|
||||||
|
super(key: key, child: child);
|
||||||
|
|
||||||
|
/// The properties for descendant popup menu widgets.
|
||||||
|
final PopupMenuThemeData data;
|
||||||
|
|
||||||
|
/// The closest instance of this class's [data] value that encloses the given
|
||||||
|
/// context. If there is no ancestor, it returns [ThemeData.popupMenuTheme].
|
||||||
|
/// Applications can assume that the returned value will not be null.
|
||||||
|
///
|
||||||
|
/// Typical usage is as follows:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// PopupMenuThemeData theme = PopupMenuTheme.of(context);
|
||||||
|
/// ```
|
||||||
|
static PopupMenuThemeData of(BuildContext context) {
|
||||||
|
final PopupMenuTheme popupMenuTheme = context.inheritFromWidgetOfExactType(PopupMenuTheme);
|
||||||
|
return popupMenuTheme?.data ?? Theme.of(context).popupMenuTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(PopupMenuTheme oldWidget) => data != oldWidget.data;
|
||||||
|
}
|
@ -23,6 +23,7 @@ import 'ink_splash.dart';
|
|||||||
import 'ink_well.dart' show InteractiveInkFeatureFactory;
|
import 'ink_well.dart' show InteractiveInkFeatureFactory;
|
||||||
import 'input_decorator.dart';
|
import 'input_decorator.dart';
|
||||||
import 'page_transitions_theme.dart';
|
import 'page_transitions_theme.dart';
|
||||||
|
import 'popup_menu_theme.dart';
|
||||||
import 'slider_theme.dart';
|
import 'slider_theme.dart';
|
||||||
import 'snack_bar_theme.dart';
|
import 'snack_bar_theme.dart';
|
||||||
import 'tab_bar_theme.dart';
|
import 'tab_bar_theme.dart';
|
||||||
@ -174,6 +175,7 @@ class ThemeData extends Diagnosticable {
|
|||||||
CupertinoThemeData cupertinoOverrideTheme,
|
CupertinoThemeData cupertinoOverrideTheme,
|
||||||
SnackBarThemeData snackBarTheme,
|
SnackBarThemeData snackBarTheme,
|
||||||
BottomSheetThemeData bottomSheetTheme,
|
BottomSheetThemeData bottomSheetTheme,
|
||||||
|
PopupMenuThemeData popupMenuTheme,
|
||||||
}) {
|
}) {
|
||||||
brightness ??= Brightness.light;
|
brightness ??= Brightness.light;
|
||||||
final bool isDark = brightness == Brightness.dark;
|
final bool isDark = brightness == Brightness.dark;
|
||||||
@ -276,6 +278,7 @@ class ThemeData extends Diagnosticable {
|
|||||||
cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault();
|
cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault();
|
||||||
snackBarTheme ??= const SnackBarThemeData();
|
snackBarTheme ??= const SnackBarThemeData();
|
||||||
bottomSheetTheme ??= const BottomSheetThemeData();
|
bottomSheetTheme ??= const BottomSheetThemeData();
|
||||||
|
popupMenuTheme ??= const PopupMenuThemeData();
|
||||||
|
|
||||||
return ThemeData.raw(
|
return ThemeData.raw(
|
||||||
brightness: brightness,
|
brightness: brightness,
|
||||||
@ -336,6 +339,7 @@ class ThemeData extends Diagnosticable {
|
|||||||
cupertinoOverrideTheme: cupertinoOverrideTheme,
|
cupertinoOverrideTheme: cupertinoOverrideTheme,
|
||||||
snackBarTheme: snackBarTheme,
|
snackBarTheme: snackBarTheme,
|
||||||
bottomSheetTheme: bottomSheetTheme,
|
bottomSheetTheme: bottomSheetTheme,
|
||||||
|
popupMenuTheme: popupMenuTheme,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -408,6 +412,7 @@ class ThemeData extends Diagnosticable {
|
|||||||
@required this.cupertinoOverrideTheme,
|
@required this.cupertinoOverrideTheme,
|
||||||
@required this.snackBarTheme,
|
@required this.snackBarTheme,
|
||||||
@required this.bottomSheetTheme,
|
@required this.bottomSheetTheme,
|
||||||
|
@required this.popupMenuTheme,
|
||||||
}) : assert(brightness != null),
|
}) : assert(brightness != null),
|
||||||
assert(primaryColor != null),
|
assert(primaryColor != null),
|
||||||
assert(primaryColorBrightness != null),
|
assert(primaryColorBrightness != null),
|
||||||
@ -462,7 +467,8 @@ class ThemeData extends Diagnosticable {
|
|||||||
assert(floatingActionButtonTheme != null),
|
assert(floatingActionButtonTheme != null),
|
||||||
assert(typography != null),
|
assert(typography != null),
|
||||||
assert(snackBarTheme != null),
|
assert(snackBarTheme != null),
|
||||||
assert(bottomSheetTheme != null);
|
assert(bottomSheetTheme != null),
|
||||||
|
assert(popupMenuTheme != 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
|
||||||
@ -788,6 +794,10 @@ class ThemeData extends Diagnosticable {
|
|||||||
/// A theme for customizing the color, elevation, and shape of a bottom sheet.
|
/// A theme for customizing the color, elevation, and shape of a bottom sheet.
|
||||||
final BottomSheetThemeData bottomSheetTheme;
|
final BottomSheetThemeData bottomSheetTheme;
|
||||||
|
|
||||||
|
/// A theme for customizing the color, shape, elevation, and text style of
|
||||||
|
/// popup menus.
|
||||||
|
final PopupMenuThemeData popupMenuTheme;
|
||||||
|
|
||||||
/// 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,
|
||||||
@ -848,6 +858,7 @@ class ThemeData extends Diagnosticable {
|
|||||||
CupertinoThemeData cupertinoOverrideTheme,
|
CupertinoThemeData cupertinoOverrideTheme,
|
||||||
SnackBarThemeData snackBarTheme,
|
SnackBarThemeData snackBarTheme,
|
||||||
BottomSheetThemeData bottomSheetTheme,
|
BottomSheetThemeData bottomSheetTheme,
|
||||||
|
PopupMenuThemeData popupMenuTheme,
|
||||||
}) {
|
}) {
|
||||||
cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault();
|
cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault();
|
||||||
return ThemeData.raw(
|
return ThemeData.raw(
|
||||||
@ -909,6 +920,7 @@ class ThemeData extends Diagnosticable {
|
|||||||
cupertinoOverrideTheme: cupertinoOverrideTheme ?? this.cupertinoOverrideTheme,
|
cupertinoOverrideTheme: cupertinoOverrideTheme ?? this.cupertinoOverrideTheme,
|
||||||
snackBarTheme: snackBarTheme ?? this.snackBarTheme,
|
snackBarTheme: snackBarTheme ?? this.snackBarTheme,
|
||||||
bottomSheetTheme: bottomSheetTheme ?? this.bottomSheetTheme,
|
bottomSheetTheme: bottomSheetTheme ?? this.bottomSheetTheme,
|
||||||
|
popupMenuTheme: popupMenuTheme ?? this.popupMenuTheme,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1048,6 +1060,7 @@ class ThemeData extends Diagnosticable {
|
|||||||
cupertinoOverrideTheme: t < 0.5 ? a.cupertinoOverrideTheme : b.cupertinoOverrideTheme,
|
cupertinoOverrideTheme: t < 0.5 ? a.cupertinoOverrideTheme : b.cupertinoOverrideTheme,
|
||||||
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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1114,7 +1127,8 @@ class ThemeData extends Diagnosticable {
|
|||||||
(otherData.typography == typography) &&
|
(otherData.typography == typography) &&
|
||||||
(otherData.cupertinoOverrideTheme == cupertinoOverrideTheme) &&
|
(otherData.cupertinoOverrideTheme == cupertinoOverrideTheme) &&
|
||||||
(otherData.snackBarTheme == snackBarTheme) &&
|
(otherData.snackBarTheme == snackBarTheme) &&
|
||||||
(otherData.bottomSheetTheme == bottomSheetTheme);
|
(otherData.bottomSheetTheme == bottomSheetTheme) &&
|
||||||
|
(otherData.popupMenuTheme == popupMenuTheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -1181,6 +1195,7 @@ class ThemeData extends Diagnosticable {
|
|||||||
cupertinoOverrideTheme,
|
cupertinoOverrideTheme,
|
||||||
snackBarTheme,
|
snackBarTheme,
|
||||||
bottomSheetTheme,
|
bottomSheetTheme,
|
||||||
|
popupMenuTheme,
|
||||||
];
|
];
|
||||||
return hashList(values);
|
return hashList(values);
|
||||||
}
|
}
|
||||||
@ -1244,6 +1259,7 @@ class ThemeData extends Diagnosticable {
|
|||||||
properties.add(DiagnosticsProperty<CupertinoThemeData>('cupertinoOverrideTheme', cupertinoOverrideTheme, defaultValue: defaultData.cupertinoOverrideTheme));
|
properties.add(DiagnosticsProperty<CupertinoThemeData>('cupertinoOverrideTheme', cupertinoOverrideTheme, defaultValue: defaultData.cupertinoOverrideTheme));
|
||||||
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
314
packages/flutter/test/material/popup_menu_theme_test.dart
Normal file
314
packages/flutter/test/material/popup_menu_theme_test.dart
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
// 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';
|
||||||
|
|
||||||
|
PopupMenuThemeData _popupMenuTheme() {
|
||||||
|
return PopupMenuThemeData(
|
||||||
|
color: Colors.orange,
|
||||||
|
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
elevation: 12.0,
|
||||||
|
textStyle: const TextStyle(color: Color(0xffffffff), textBaseline: TextBaseline.alphabetic),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('PopupMenuThemeData copyWith, ==, hashCode basics', () {
|
||||||
|
expect(const PopupMenuThemeData(), const PopupMenuThemeData().copyWith());
|
||||||
|
expect(const PopupMenuThemeData().hashCode, const PopupMenuThemeData().copyWith().hashCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('PopupMenuThemeData null fields by default', () {
|
||||||
|
const PopupMenuThemeData popupMenuTheme = PopupMenuThemeData();
|
||||||
|
expect(popupMenuTheme.color, null);
|
||||||
|
expect(popupMenuTheme.shape, null);
|
||||||
|
expect(popupMenuTheme.elevation, null);
|
||||||
|
expect(popupMenuTheme.textStyle, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Default PopupMenuThemeData debugFillProperties',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
||||||
|
const PopupMenuThemeData().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('PopupMenuThemeData implements debugFillProperties',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
||||||
|
PopupMenuThemeData(
|
||||||
|
color: const Color(0xFFFFFFFF),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2.0)),
|
||||||
|
elevation: 2.0,
|
||||||
|
textStyle: const 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>[
|
||||||
|
'color: Color(0xffffffff)',
|
||||||
|
'shape: RoundedRectangleBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), BorderRadius.circular(2.0))',
|
||||||
|
'elevation: 2.0',
|
||||||
|
'text style: TextStyle(inherit: true, color: Color(0xffffffff))'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Passing no PopupMenuThemeData returns defaults', (WidgetTester tester) async {
|
||||||
|
final Key popupButtonKey = UniqueKey();
|
||||||
|
final Key popupButtonApp = UniqueKey();
|
||||||
|
final Key popupItemKey = UniqueKey();
|
||||||
|
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
theme: ThemeData(),
|
||||||
|
key: popupButtonApp,
|
||||||
|
home: Material(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
PopupMenuButton<void>(
|
||||||
|
key: popupButtonKey,
|
||||||
|
itemBuilder: (BuildContext context) {
|
||||||
|
return <PopupMenuEntry<Object>>[
|
||||||
|
PopupMenuItem<void>(
|
||||||
|
key: popupItemKey,
|
||||||
|
child: const Text('Example'),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(popupButtonKey));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
/// The last Material widget under popupButtonApp is the [PopupMenuButton]
|
||||||
|
/// specified above, so by finding the last descendent of popupButtonApp
|
||||||
|
/// that is of type Material, this code retrieves the built
|
||||||
|
/// [PopupMenuButton].
|
||||||
|
final Material button = tester.widget<Material>(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byKey(popupButtonApp),
|
||||||
|
matching: find.byType(Material),
|
||||||
|
).last,
|
||||||
|
);
|
||||||
|
expect(button.color, null);
|
||||||
|
expect(button.shape, null);
|
||||||
|
expect(button.elevation, 8.0);
|
||||||
|
|
||||||
|
/// The last DefaultTextStyle widget under popupItemKey is the
|
||||||
|
/// [PopupMenuItem] specified above, so by finding the last descendent of
|
||||||
|
/// popupItemKey that is of type DefaultTextStyle, this code retrieves the
|
||||||
|
/// built [PopupMenuItem].
|
||||||
|
final DefaultTextStyle text = tester.widget<DefaultTextStyle>(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byKey(popupItemKey),
|
||||||
|
matching: find.byType(DefaultTextStyle),
|
||||||
|
).last,
|
||||||
|
);
|
||||||
|
expect(text.style.fontFamily, 'Roboto');
|
||||||
|
expect(text.style.color, const Color(0xdd000000));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Popup menu uses values from PopupMenuThemeData', (WidgetTester tester) async {
|
||||||
|
final PopupMenuThemeData popupMenuTheme = _popupMenuTheme();
|
||||||
|
final Key popupButtonKey = UniqueKey();
|
||||||
|
final Key popupButtonApp = UniqueKey();
|
||||||
|
final Key popupItemKey = UniqueKey();
|
||||||
|
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
theme: ThemeData(popupMenuTheme: popupMenuTheme),
|
||||||
|
key: popupButtonApp,
|
||||||
|
home: Material(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
PopupMenuButton<void>(
|
||||||
|
key: popupButtonKey,
|
||||||
|
itemBuilder: (BuildContext context) {
|
||||||
|
return <PopupMenuEntry<Object>>[
|
||||||
|
PopupMenuItem<void>(
|
||||||
|
key: popupItemKey,
|
||||||
|
child: const Text('Example'),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(popupButtonKey));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
/// The last Material widget under popupButtonApp is the [PopupMenuButton]
|
||||||
|
/// specified above, so by finding the last descendent of popupButtonApp
|
||||||
|
/// that is of type Material, this code retrieves the built
|
||||||
|
/// [PopupMenuButton].
|
||||||
|
final Material button = tester.widget<Material>(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byKey(popupButtonApp),
|
||||||
|
matching: find.byType(Material),
|
||||||
|
).last,
|
||||||
|
);
|
||||||
|
expect(button.color, popupMenuTheme.color);
|
||||||
|
expect(button.shape, popupMenuTheme.shape);
|
||||||
|
expect(button.elevation, popupMenuTheme.elevation);
|
||||||
|
|
||||||
|
/// The last DefaultTextStyle widget under popupItemKey is the
|
||||||
|
/// [PopupMenuItem] specified above, so by finding the last descendent of
|
||||||
|
/// popupItemKey that is of type DefaultTextStyle, this code retrieves the
|
||||||
|
/// built [PopupMenuItem].
|
||||||
|
final DefaultTextStyle text = tester.widget<DefaultTextStyle>(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byKey(popupItemKey),
|
||||||
|
matching: find.byType(DefaultTextStyle),
|
||||||
|
).last,
|
||||||
|
);
|
||||||
|
expect(text.style, popupMenuTheme.textStyle);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Popup menu widget properties take priority over theme', (WidgetTester tester) async {
|
||||||
|
final PopupMenuThemeData popupMenuTheme = _popupMenuTheme();
|
||||||
|
final Key popupButtonKey = UniqueKey();
|
||||||
|
final Key popupButtonApp = UniqueKey();
|
||||||
|
final Key popupItemKey = UniqueKey();
|
||||||
|
|
||||||
|
const Color color = Colors.purple;
|
||||||
|
const ShapeBorder shape = RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(9.0)),
|
||||||
|
);
|
||||||
|
const double elevation = 7.0;
|
||||||
|
const TextStyle textStyle = TextStyle(color: Color(0x00000000), textBaseline: TextBaseline.alphabetic);
|
||||||
|
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
theme: ThemeData(popupMenuTheme: popupMenuTheme),
|
||||||
|
key: popupButtonApp,
|
||||||
|
home: Material(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
PopupMenuButton<void>(
|
||||||
|
key: popupButtonKey,
|
||||||
|
elevation: elevation,
|
||||||
|
color: color,
|
||||||
|
shape: shape,
|
||||||
|
itemBuilder: (BuildContext context) {
|
||||||
|
return <PopupMenuEntry<Object>>[
|
||||||
|
PopupMenuItem<void>(
|
||||||
|
key: popupItemKey,
|
||||||
|
textStyle: textStyle,
|
||||||
|
child: const Text('Example'),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(popupButtonKey));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
/// The last Material widget under popupButtonApp is the [PopupMenuButton]
|
||||||
|
/// specified above, so by finding the last descendent of popupButtonApp
|
||||||
|
/// that is of type Material, this code retrieves the built
|
||||||
|
/// [PopupMenuButton].
|
||||||
|
final Material button = tester.widget<Material>(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byKey(popupButtonApp),
|
||||||
|
matching: find.byType(Material),
|
||||||
|
).last,
|
||||||
|
);
|
||||||
|
expect(button.color, color);
|
||||||
|
expect(button.shape, shape);
|
||||||
|
expect(button.elevation, elevation);
|
||||||
|
|
||||||
|
/// The last DefaultTextStyle widget under popupItemKey is the
|
||||||
|
/// [PopupMenuItem] specified above, so by finding the last descendent of
|
||||||
|
/// popupItemKey that is of type DefaultTextStyle, this code retrieves the
|
||||||
|
/// built [PopupMenuItem].
|
||||||
|
final DefaultTextStyle text = tester.widget<DefaultTextStyle>(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byKey(popupItemKey),
|
||||||
|
matching: find.byType(DefaultTextStyle),
|
||||||
|
).last,
|
||||||
|
);
|
||||||
|
expect(text.style, textStyle);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('ThemeData.popupMenuTheme properties are utilized', (WidgetTester tester) async {
|
||||||
|
final Key popupButtonKey = UniqueKey();
|
||||||
|
final Key popupButtonApp = UniqueKey();
|
||||||
|
final Key popupItemKey = UniqueKey();
|
||||||
|
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
key: popupButtonApp,
|
||||||
|
home: Material(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
PopupMenuTheme(
|
||||||
|
color: Colors.pink,
|
||||||
|
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
|
elevation: 6.0,
|
||||||
|
textStyle: const TextStyle(color: Color(0xfffff000), textBaseline: TextBaseline.alphabetic),
|
||||||
|
child: PopupMenuButton<void>(
|
||||||
|
key: popupButtonKey,
|
||||||
|
itemBuilder: (BuildContext context) {
|
||||||
|
return <PopupMenuEntry<Object>>[
|
||||||
|
PopupMenuItem<void>(
|
||||||
|
key: popupItemKey,
|
||||||
|
child: const Text('Example'),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(popupButtonKey));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
/// The last Material widget under popupButtonApp is the [PopupMenuButton]
|
||||||
|
/// specified above, so by finding the last descendent of popupButtonApp
|
||||||
|
/// that is of type Material, this code retrieves the built
|
||||||
|
/// [PopupMenuButton].
|
||||||
|
final Material button = tester.widget<Material>(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byKey(popupButtonApp),
|
||||||
|
matching: find.byType(Material),
|
||||||
|
).last,
|
||||||
|
);
|
||||||
|
expect(button.color, Colors.pink);
|
||||||
|
expect(button.shape, BeveledRectangleBorder(borderRadius: BorderRadius.circular(10)));
|
||||||
|
expect(button.elevation, 6.0);
|
||||||
|
|
||||||
|
/// The last DefaultTextStyle widget under popupItemKey is the
|
||||||
|
/// [PopupMenuItem] specified above, so by finding the last descendent of
|
||||||
|
/// popupItemKey that is of type DefaultTextStyle, this code retrieves the
|
||||||
|
/// built [PopupMenuItem].
|
||||||
|
final DefaultTextStyle text = tester.widget<DefaultTextStyle>(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byKey(popupItemKey),
|
||||||
|
matching: find.byType(DefaultTextStyle),
|
||||||
|
).last,
|
||||||
|
);
|
||||||
|
expect(text.style.color, const Color(0xfffff000));
|
||||||
|
});
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user