Unverified Commit 92efec39 authored by MH Johnson's avatar MH Johnson Committed by GitHub

[Material] Theme-able elevation on dialogs. (#24169)

* Themable elevation on dialogs.

* Added `BackgroundColor` in widget + theme

* Addressing Comments

* Fix test name

* Add debugFillProperties test
parent e6d216cc
...@@ -41,16 +41,31 @@ class Dialog extends StatelessWidget { ...@@ -41,16 +41,31 @@ class Dialog extends StatelessWidget {
/// Typically used in conjunction with [showDialog]. /// Typically used in conjunction with [showDialog].
const Dialog({ const Dialog({
Key key, Key key,
this.child, this.backgroundColor,
this.elevation,
this.insetAnimationDuration = const Duration(milliseconds: 100), this.insetAnimationDuration = const Duration(milliseconds: 100),
this.insetAnimationCurve = Curves.decelerate, this.insetAnimationCurve = Curves.decelerate,
this.shape, this.shape,
this.child,
}) : super(key: key); }) : super(key: key);
/// The widget below this widget in the tree. /// {@template flutter.material.dialog.backgroundColor}
/// The background color of the surface of this [Dialog].
/// ///
/// {@macro flutter.widgets.child} /// This sets the [Material.color] on this [Dialog]'s [Material].
final Widget child; ///
/// If `null`, [ThemeData.cardColor] is used.
/// {@endtemplate}
final Color backgroundColor;
/// {@template flutter.material.dialog.elevation}
/// The z-coordinate of this [Dialog].
///
/// If null then [DialogTheme.elevation] is used, and if that's null then the
/// dialog's elevation is 24.0.
/// {@endtemplate}
/// {@macro flutter.material.material.elevation}
final double elevation;
/// The duration of the animation to show when the system keyboard intrudes /// The duration of the animation to show when the system keyboard intrudes
/// into the space that the dialog is placed in. /// into the space that the dialog is placed in.
...@@ -73,13 +88,15 @@ class Dialog extends StatelessWidget { ...@@ -73,13 +88,15 @@ class Dialog extends StatelessWidget {
/// {@endtemplate} /// {@endtemplate}
final ShapeBorder shape; final ShapeBorder shape;
Color _getColor(BuildContext context) { /// The widget below this widget in the tree.
return Theme.of(context).dialogBackgroundColor; ///
} /// {@macro flutter.widgets.child}
final Widget child;
// TODO(johnsonmh): Update default dialog border radius to 4.0 to match material spec. // TODO(johnsonmh): Update default dialog border radius to 4.0 to match material spec.
static const RoundedRectangleBorder _defaultDialogShape = static const RoundedRectangleBorder _defaultDialogShape =
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))); RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0)));
static const double _defaultElevation = 24.0;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
...@@ -98,11 +115,11 @@ class Dialog extends StatelessWidget { ...@@ -98,11 +115,11 @@ class Dialog extends StatelessWidget {
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 280.0), constraints: const BoxConstraints(minWidth: 280.0),
child: Material( child: Material(
elevation: 24.0, color: backgroundColor ?? dialogTheme.backgroundColor ?? Theme.of(context).dialogBackgroundColor,
color: _getColor(context), elevation: elevation ?? dialogTheme.elevation ?? _defaultElevation,
shape: shape ?? dialogTheme.shape ?? _defaultDialogShape,
type: MaterialType.card, type: MaterialType.card,
child: child, child: child,
shape: shape ?? dialogTheme.shape ?? _defaultDialogShape,
), ),
), ),
), ),
...@@ -190,6 +207,8 @@ class AlertDialog extends StatelessWidget { ...@@ -190,6 +207,8 @@ class AlertDialog extends StatelessWidget {
this.content, this.content,
this.contentPadding = const EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 24.0), this.contentPadding = const EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 24.0),
this.actions, this.actions,
this.backgroundColor,
this.elevation,
this.semanticLabel, this.semanticLabel,
this.shape, this.shape,
}) : assert(contentPadding != null), }) : assert(contentPadding != null),
...@@ -242,6 +261,13 @@ class AlertDialog extends StatelessWidget { ...@@ -242,6 +261,13 @@ class AlertDialog extends StatelessWidget {
/// from the [actions]. /// from the [actions].
final List<Widget> actions; final List<Widget> actions;
/// {@macro flutter.material.dialog.backgroundColor}
final Color backgroundColor;
/// {@macro flutter.material.dialog.elevation}
/// {@macro flutter.material.material.elevation}
final double elevation;
/// The semantic label of the dialog used by accessibility frameworks to /// The semantic label of the dialog used by accessibility frameworks to
/// announce screen transitions when the dialog is opened and closed. /// announce screen transitions when the dialog is opened and closed.
/// ///
...@@ -318,7 +344,12 @@ class AlertDialog extends StatelessWidget { ...@@ -318,7 +344,12 @@ class AlertDialog extends StatelessWidget {
child: dialogChild child: dialogChild
); );
return Dialog(child: dialogChild, shape: shape); return Dialog(
backgroundColor: backgroundColor,
elevation: elevation,
shape: shape,
child: dialogChild,
);
} }
} }
...@@ -464,6 +495,8 @@ class SimpleDialog extends StatelessWidget { ...@@ -464,6 +495,8 @@ class SimpleDialog extends StatelessWidget {
this.titlePadding = const EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 0.0), this.titlePadding = const EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 0.0),
this.children, this.children,
this.contentPadding = const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0), this.contentPadding = const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0),
this.backgroundColor,
this.elevation,
this.semanticLabel, this.semanticLabel,
this.shape, this.shape,
}) : assert(titlePadding != null), }) : assert(titlePadding != null),
...@@ -507,6 +540,13 @@ class SimpleDialog extends StatelessWidget { ...@@ -507,6 +540,13 @@ class SimpleDialog extends StatelessWidget {
/// the top padding ends up being 24 pixels. /// the top padding ends up being 24 pixels.
final EdgeInsetsGeometry contentPadding; final EdgeInsetsGeometry contentPadding;
/// {@macro flutter.material.dialog.backgroundColor}
final Color backgroundColor;
/// {@macro flutter.material.dialog.elevation}
/// {@macro flutter.material.material.elevation}
final double elevation;
/// The semantic label of the dialog used by accessibility frameworks to /// The semantic label of the dialog used by accessibility frameworks to
/// announce screen transitions when the dialog is opened and closed. /// announce screen transitions when the dialog is opened and closed.
/// ///
...@@ -575,7 +615,12 @@ class SimpleDialog extends StatelessWidget { ...@@ -575,7 +615,12 @@ class SimpleDialog extends StatelessWidget {
label: label, label: label,
child: dialogChild, child: dialogChild,
); );
return Dialog(child: dialogChild, shape: shape); return Dialog(
backgroundColor: backgroundColor,
elevation: elevation,
shape: shape,
child: dialogChild,
);
} }
} }
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui' show lerpDouble;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -23,15 +25,27 @@ import 'theme.dart'; ...@@ -23,15 +25,27 @@ import 'theme.dart';
/// application. /// application.
class DialogTheme extends Diagnosticable { class DialogTheme extends Diagnosticable {
/// Creates a dialog theme that can be used for [ThemeData.dialogTheme]. /// Creates a dialog theme that can be used for [ThemeData.dialogTheme].
const DialogTheme({ this.shape }); const DialogTheme({ this.backgroundColor, this.elevation, this.shape });
/// Default value for [Dialog.backgroundColor].
final Color backgroundColor;
/// Default value for [Dialog.elevation].
///
/// If null, the [Dialog] elevation defaults to `24.0`.
final double elevation;
/// Default value for [Dialog.shape]. /// Default value for [Dialog.shape].
final ShapeBorder shape; final ShapeBorder shape;
/// Creates a copy of this object but with the given fields replaced with the /// Creates a copy of this object but with the given fields replaced with the
/// new values. /// new values.
DialogTheme copyWith({ ShapeBorder shape }) { DialogTheme copyWith({ Color backgroundColor, double elevation, ShapeBorder shape }) {
return DialogTheme(shape: shape ?? this.shape); return DialogTheme(
backgroundColor: backgroundColor ?? this.backgroundColor,
elevation: elevation ?? this.elevation,
shape: shape ?? this.shape,
);
} }
/// The data from the closest [DialogTheme] instance given the build context. /// The data from the closest [DialogTheme] instance given the build context.
...@@ -47,7 +61,9 @@ class DialogTheme extends Diagnosticable { ...@@ -47,7 +61,9 @@ class DialogTheme extends Diagnosticable {
static DialogTheme lerp(DialogTheme a, DialogTheme b, double t) { static DialogTheme lerp(DialogTheme a, DialogTheme b, double t) {
assert(t != null); assert(t != null);
return DialogTheme( return DialogTheme(
shape: ShapeBorder.lerp(a?.shape, b?.shape, t) backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
elevation: lerpDouble(a?.elevation, b?.elevation, t),
shape: ShapeBorder.lerp(a?.shape, b?.shape, t),
); );
} }
...@@ -61,12 +77,16 @@ class DialogTheme extends Diagnosticable { ...@@ -61,12 +77,16 @@ class DialogTheme extends Diagnosticable {
if (other.runtimeType != runtimeType) if (other.runtimeType != runtimeType)
return false; return false;
final DialogTheme typedOther = other; final DialogTheme typedOther = other;
return typedOther.shape == shape; return typedOther.backgroundColor == backgroundColor
&& typedOther.elevation == elevation
&& typedOther.shape == shape;
} }
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Color>('backgroundColor', backgroundColor));
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null)); properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
properties.add(DiagnosticsProperty<double>('elevation', elevation));
} }
} }
...@@ -194,6 +194,7 @@ class Material extends StatefulWidget { ...@@ -194,6 +194,7 @@ class Material extends StatefulWidget {
/// the shape is rectangular, and the default color. /// the shape is rectangular, and the default color.
final MaterialType type; final MaterialType type;
/// {@template flutter.material.material.elevation}
/// The z-coordinate at which to place this material. This controls the size /// The z-coordinate at which to place this material. This controls the size
/// of the shadow below the material. /// of the shadow below the material.
/// ///
...@@ -202,6 +203,7 @@ class Material extends StatefulWidget { ...@@ -202,6 +203,7 @@ class Material extends StatefulWidget {
/// ///
/// Defaults to 0. Changing this value will cause the shadow to animate over /// Defaults to 0. Changing this value will cause the shadow to animate over
/// [animationDuration]. /// [animationDuration].
/// {@endtemplate}
final double elevation; final double elevation;
/// The color to paint the material. /// The color to paint the material.
......
...@@ -35,6 +35,10 @@ MaterialApp _appWithAlertDialog(WidgetTester tester, AlertDialog dialog, {ThemeD ...@@ -35,6 +35,10 @@ MaterialApp _appWithAlertDialog(WidgetTester tester, AlertDialog dialog, {ThemeD
); );
} }
Material _getMaterialFromDialog(WidgetTester tester) {
return tester.widget<Material>(find.descendant(of: find.byType(AlertDialog), matching: find.byType(Material)));
}
const ShapeBorder _defaultDialogShape = RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))); const ShapeBorder _defaultDialogShape = RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0)));
void main() { void main() {
...@@ -58,15 +62,29 @@ void main() { ...@@ -58,15 +62,29 @@ void main() {
await tester.pumpWidget(_appWithAlertDialog(tester, dialog)); await tester.pumpWidget(_appWithAlertDialog(tester, dialog));
await tester.tap(find.text('X')); await tester.tap(find.text('X'));
await tester.pump(); // start animation await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 1));
expect(didPressOk, false); expect(didPressOk, false);
await tester.tap(find.text('OK')); await tester.tap(find.text('OK'));
expect(didPressOk, true); expect(didPressOk, true);
}); });
testWidgets('Dialog background color', (WidgetTester tester) async { testWidgets('Dialog background color from AlertDialog', (WidgetTester tester) async {
const Color customColor = Colors.pink;
const AlertDialog dialog = AlertDialog(
backgroundColor: customColor,
actions: <Widget>[ ],
);
await tester.pumpWidget(_appWithAlertDialog(tester, dialog, theme: ThemeData(brightness: Brightness.dark)));
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
final Material materialWidget = _getMaterialFromDialog(tester);
expect(materialWidget.color, customColor);
});
testWidgets('Dialog Defaults', (WidgetTester tester) async {
const AlertDialog dialog = AlertDialog( const AlertDialog dialog = AlertDialog(
title: Text('Title'), title: Text('Title'),
content: Text('Y'), content: Text('Y'),
...@@ -75,14 +93,27 @@ void main() { ...@@ -75,14 +93,27 @@ void main() {
await tester.pumpWidget(_appWithAlertDialog(tester, dialog, theme: ThemeData(brightness: Brightness.dark))); await tester.pumpWidget(_appWithAlertDialog(tester, dialog, theme: ThemeData(brightness: Brightness.dark)));
await tester.tap(find.text('X')); await tester.tap(find.text('X'));
await tester.pump(); // start animation await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 1));
final StatefulElement widget = tester.element( final Material materialWidget = _getMaterialFromDialog(tester);
find.descendant(of: find.byType(AlertDialog), matching: find.byType(Material)));
final Material materialWidget = widget.state.widget;
expect(materialWidget.color, Colors.grey[800]); expect(materialWidget.color, Colors.grey[800]);
expect(materialWidget.shape, _defaultDialogShape); expect(materialWidget.shape, _defaultDialogShape);
expect(materialWidget.elevation, 24.0);
});
testWidgets('Custom dialog elevation', (WidgetTester tester) async {
const double customElevation = 12.0;
const AlertDialog dialog = AlertDialog(
actions: <Widget>[ ],
elevation: customElevation,
);
await tester.pumpWidget(_appWithAlertDialog(tester, dialog));
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
final Material materialWidget = _getMaterialFromDialog(tester);
expect(materialWidget.elevation, customElevation);
}); });
testWidgets('Custom dialog shape', (WidgetTester tester) async { testWidgets('Custom dialog shape', (WidgetTester tester) async {
...@@ -95,12 +126,9 @@ void main() { ...@@ -95,12 +126,9 @@ void main() {
await tester.pumpWidget(_appWithAlertDialog(tester, dialog)); await tester.pumpWidget(_appWithAlertDialog(tester, dialog));
await tester.tap(find.text('X')); await tester.tap(find.text('X'));
await tester.pump(); // start animation await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 1));
final StatefulElement widget = tester.element( final Material materialWidget = _getMaterialFromDialog(tester);
find.descendant(of: find.byType(AlertDialog), matching: find.byType(Material)));
final Material materialWidget = widget.state.widget;
expect(materialWidget.shape, customBorder); expect(materialWidget.shape, customBorder);
}); });
...@@ -112,12 +140,9 @@ void main() { ...@@ -112,12 +140,9 @@ void main() {
await tester.pumpWidget(_appWithAlertDialog(tester, dialog)); await tester.pumpWidget(_appWithAlertDialog(tester, dialog));
await tester.tap(find.text('X')); await tester.tap(find.text('X'));
await tester.pump(); // start animation await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 1));
final StatefulElement widget = tester.element( final Material materialWidget = _getMaterialFromDialog(tester);
find.descendant(of: find.byType(AlertDialog), matching: find.byType(Material)));
final Material materialWidget = widget.state.widget;
expect(materialWidget.shape, _defaultDialogShape); expect(materialWidget.shape, _defaultDialogShape);
}); });
...@@ -130,12 +155,9 @@ void main() { ...@@ -130,12 +155,9 @@ void main() {
await tester.pumpWidget(_appWithAlertDialog(tester, dialog)); await tester.pumpWidget(_appWithAlertDialog(tester, dialog));
await tester.tap(find.text('X')); await tester.tap(find.text('X'));
await tester.pump(); // start animation await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 1));
final StatefulElement widget = tester.element( final Material materialWidget = _getMaterialFromDialog(tester);
find.descendant(of: find.byType(AlertDialog), matching: find.byType(Material)));
final Material materialWidget = widget.state.widget;
expect(materialWidget.shape, customBorder); expect(materialWidget.shape, customBorder);
}); });
...@@ -406,8 +428,7 @@ void main() { ...@@ -406,8 +428,7 @@ void main() {
))); )));
await tester.tap(find.text('X')); await tester.tap(find.text('X'));
await tester.pump(); // start animation await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 1));
expect(semantics, includesNodeWith( expect(semantics, includesNodeWith(
label: 'Title', label: 'Title',
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'dart:io' show Platform; import 'dart:io' show Platform;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
MaterialApp _appWithAlertDialog(WidgetTester tester, AlertDialog dialog, {ThemeData theme}) { MaterialApp _appWithAlertDialog(WidgetTester tester, AlertDialog dialog, {ThemeData theme}) {
...@@ -34,7 +35,61 @@ MaterialApp _appWithAlertDialog(WidgetTester tester, AlertDialog dialog, {ThemeD ...@@ -34,7 +35,61 @@ MaterialApp _appWithAlertDialog(WidgetTester tester, AlertDialog dialog, {ThemeD
final Key _painterKey = UniqueKey(); final Key _painterKey = UniqueKey();
Material _getMaterialFromDialog(WidgetTester tester) {
return tester.widget<Material>(find.descendant(of: find.byType(AlertDialog), matching: find.byType(Material)));
}
void main() { void main() {
testWidgets('Dialog Theme implements debugFillDescription', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const DialogTheme(
backgroundColor: Color(0xff123456),
elevation: 8.0,
shape: null,
).debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode n) => !n.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode n) => n.toString()).toList();
expect(description, <String>[
'backgroundColor: Color(0xff123456)',
'elevation: 8.0'
]);
});
testWidgets('Dialog background color', (WidgetTester tester) async {
const Color customColor = Colors.pink;
const AlertDialog dialog = AlertDialog(
title: Text('Title'),
actions: <Widget>[ ],
);
final ThemeData theme = ThemeData(dialogTheme: const DialogTheme(backgroundColor: customColor));
await tester.pumpWidget(_appWithAlertDialog(tester, dialog, theme: theme));
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
final Material materialWidget = _getMaterialFromDialog(tester);
expect(materialWidget.color, customColor);
});
testWidgets('Custom dialog elevation', (WidgetTester tester) async {
const double customElevation = 12.0;
const AlertDialog dialog = AlertDialog(
title: Text('Title'),
actions: <Widget>[ ],
);
final ThemeData theme = ThemeData(dialogTheme: const DialogTheme(elevation: customElevation));
await tester.pumpWidget(
_appWithAlertDialog(tester, dialog, theme: theme)
);
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
final Material materialWidget = _getMaterialFromDialog(tester);
expect(materialWidget.elevation, customElevation);
});
testWidgets('Custom dialog shape', (WidgetTester tester) async { testWidgets('Custom dialog shape', (WidgetTester tester) async {
const RoundedRectangleBorder customBorder = const RoundedRectangleBorder customBorder =
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16.0))); RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16.0)));
...@@ -48,12 +103,9 @@ void main() { ...@@ -48,12 +103,9 @@ void main() {
_appWithAlertDialog(tester, dialog, theme: theme) _appWithAlertDialog(tester, dialog, theme: theme)
); );
await tester.tap(find.text('X')); await tester.tap(find.text('X'));
await tester.pump(); // start animation await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 1));
final StatefulElement widget = tester.element( final Material materialWidget = _getMaterialFromDialog(tester);
find.descendant(of: find.byType(AlertDialog), matching: find.byType(Material)));
final Material materialWidget = widget.state.widget;
expect(materialWidget.shape, customBorder); expect(materialWidget.shape, customBorder);
}); });
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment