From 92efec3998da7d3f5f1b5b83f6a6d10f46b89402 Mon Sep 17 00:00:00 2001 From: MH Johnson Date: Thu, 13 Dec 2018 09:11:01 -0500 Subject: [PATCH] [Material] Theme-able elevation on dialogs. (#24169) * Themable elevation on dialogs. * Added `BackgroundColor` in widget + theme * Addressing Comments * Fix test name * Add debugFillProperties test --- packages/flutter/lib/src/material/dialog.dart | 69 ++++++++++++++---- .../lib/src/material/dialog_theme.dart | 30 ++++++-- .../flutter/lib/src/material/material.dart | 2 + .../flutter/test/material/dialog_test.dart | 71 ++++++++++++------- .../test/material/dialog_theme_test.dart | 62 ++++++++++++++-- 5 files changed, 187 insertions(+), 47 deletions(-) diff --git a/packages/flutter/lib/src/material/dialog.dart b/packages/flutter/lib/src/material/dialog.dart index 06a02e3bf6..36fce95f8c 100644 --- a/packages/flutter/lib/src/material/dialog.dart +++ b/packages/flutter/lib/src/material/dialog.dart @@ -41,16 +41,31 @@ class Dialog extends StatelessWidget { /// Typically used in conjunction with [showDialog]. const Dialog({ Key key, - this.child, + this.backgroundColor, + this.elevation, this.insetAnimationDuration = const Duration(milliseconds: 100), this.insetAnimationCurve = Curves.decelerate, this.shape, + this.child, }) : 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} - final Widget child; + /// This sets the [Material.color] on this [Dialog]'s [Material]. + /// + /// 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 /// into the space that the dialog is placed in. @@ -73,13 +88,15 @@ class Dialog extends StatelessWidget { /// {@endtemplate} final ShapeBorder shape; - Color _getColor(BuildContext context) { - return Theme.of(context).dialogBackgroundColor; - } + /// The widget below this widget in the tree. + /// + /// {@macro flutter.widgets.child} + final Widget child; // TODO(johnsonmh): Update default dialog border radius to 4.0 to match material spec. static const RoundedRectangleBorder _defaultDialogShape = RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))); + static const double _defaultElevation = 24.0; @override Widget build(BuildContext context) { @@ -98,11 +115,11 @@ class Dialog extends StatelessWidget { child: ConstrainedBox( constraints: const BoxConstraints(minWidth: 280.0), child: Material( - elevation: 24.0, - color: _getColor(context), + color: backgroundColor ?? dialogTheme.backgroundColor ?? Theme.of(context).dialogBackgroundColor, + elevation: elevation ?? dialogTheme.elevation ?? _defaultElevation, + shape: shape ?? dialogTheme.shape ?? _defaultDialogShape, type: MaterialType.card, child: child, - shape: shape ?? dialogTheme.shape ?? _defaultDialogShape, ), ), ), @@ -190,6 +207,8 @@ class AlertDialog extends StatelessWidget { this.content, this.contentPadding = const EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 24.0), this.actions, + this.backgroundColor, + this.elevation, this.semanticLabel, this.shape, }) : assert(contentPadding != null), @@ -242,6 +261,13 @@ class AlertDialog extends StatelessWidget { /// from the [actions]. final List 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 /// announce screen transitions when the dialog is opened and closed. /// @@ -318,7 +344,12 @@ class AlertDialog extends StatelessWidget { 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 { this.titlePadding = const EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 0.0), this.children, this.contentPadding = const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0), + this.backgroundColor, + this.elevation, this.semanticLabel, this.shape, }) : assert(titlePadding != null), @@ -507,6 +540,13 @@ class SimpleDialog extends StatelessWidget { /// the top padding ends up being 24 pixels. 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 /// announce screen transitions when the dialog is opened and closed. /// @@ -575,7 +615,12 @@ class SimpleDialog extends StatelessWidget { label: label, child: dialogChild, ); - return Dialog(child: dialogChild, shape: shape); + return Dialog( + backgroundColor: backgroundColor, + elevation: elevation, + shape: shape, + child: dialogChild, + ); } } diff --git a/packages/flutter/lib/src/material/dialog_theme.dart b/packages/flutter/lib/src/material/dialog_theme.dart index 515f8eb957..5020c7249d 100644 --- a/packages/flutter/lib/src/material/dialog_theme.dart +++ b/packages/flutter/lib/src/material/dialog_theme.dart @@ -2,6 +2,8 @@ // 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'; @@ -23,15 +25,27 @@ import 'theme.dart'; /// application. class DialogTheme extends Diagnosticable { /// 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]. final ShapeBorder shape; /// Creates a copy of this object but with the given fields replaced with the /// new values. - DialogTheme copyWith({ ShapeBorder shape }) { - return DialogTheme(shape: shape ?? this.shape); + DialogTheme copyWith({ Color backgroundColor, double elevation, ShapeBorder 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. @@ -47,7 +61,9 @@ class DialogTheme extends Diagnosticable { static DialogTheme lerp(DialogTheme a, DialogTheme b, double t) { assert(t != null); 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 { if (other.runtimeType != runtimeType) return false; final DialogTheme typedOther = other; - return typedOther.shape == shape; + return typedOther.backgroundColor == backgroundColor + && typedOther.elevation == elevation + && typedOther.shape == shape; } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('backgroundColor', backgroundColor)); properties.add(DiagnosticsProperty('shape', shape, defaultValue: null)); + properties.add(DiagnosticsProperty('elevation', elevation)); } } diff --git a/packages/flutter/lib/src/material/material.dart b/packages/flutter/lib/src/material/material.dart index d522a070b1..489b95a04e 100644 --- a/packages/flutter/lib/src/material/material.dart +++ b/packages/flutter/lib/src/material/material.dart @@ -194,6 +194,7 @@ class Material extends StatefulWidget { /// the shape is rectangular, and the default color. final MaterialType type; + /// {@template flutter.material.material.elevation} /// The z-coordinate at which to place this material. This controls the size /// of the shadow below the material. /// @@ -202,6 +203,7 @@ class Material extends StatefulWidget { /// /// Defaults to 0. Changing this value will cause the shadow to animate over /// [animationDuration]. + /// {@endtemplate} final double elevation; /// The color to paint the material. diff --git a/packages/flutter/test/material/dialog_test.dart b/packages/flutter/test/material/dialog_test.dart index 92158cf420..409463affd 100644 --- a/packages/flutter/test/material/dialog_test.dart +++ b/packages/flutter/test/material/dialog_test.dart @@ -35,6 +35,10 @@ MaterialApp _appWithAlertDialog(WidgetTester tester, AlertDialog dialog, {ThemeD ); } +Material _getMaterialFromDialog(WidgetTester tester) { + return tester.widget(find.descendant(of: find.byType(AlertDialog), matching: find.byType(Material))); +} + const ShapeBorder _defaultDialogShape = RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))); void main() { @@ -58,15 +62,29 @@ void main() { await tester.pumpWidget(_appWithAlertDialog(tester, dialog)); await tester.tap(find.text('X')); - await tester.pump(); // start animation - await tester.pump(const Duration(seconds: 1)); + await tester.pumpAndSettle(); expect(didPressOk, false); await tester.tap(find.text('OK')); 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: [ ], + ); + 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( title: Text('Title'), content: Text('Y'), @@ -75,14 +93,27 @@ void main() { await tester.pumpWidget(_appWithAlertDialog(tester, dialog, theme: ThemeData(brightness: Brightness.dark))); await tester.tap(find.text('X')); - await tester.pump(); // start animation - await tester.pump(const Duration(seconds: 1)); + await tester.pumpAndSettle(); - final StatefulElement widget = tester.element( - find.descendant(of: find.byType(AlertDialog), matching: find.byType(Material))); - final Material materialWidget = widget.state.widget; + final Material materialWidget = _getMaterialFromDialog(tester); expect(materialWidget.color, Colors.grey[800]); 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: [ ], + 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 { @@ -95,12 +126,9 @@ void main() { await tester.pumpWidget(_appWithAlertDialog(tester, dialog)); await tester.tap(find.text('X')); - await tester.pump(); // start animation - await tester.pump(const Duration(seconds: 1)); + await tester.pumpAndSettle(); - final StatefulElement widget = tester.element( - find.descendant(of: find.byType(AlertDialog), matching: find.byType(Material))); - final Material materialWidget = widget.state.widget; + final Material materialWidget = _getMaterialFromDialog(tester); expect(materialWidget.shape, customBorder); }); @@ -112,12 +140,9 @@ void main() { await tester.pumpWidget(_appWithAlertDialog(tester, dialog)); await tester.tap(find.text('X')); - await tester.pump(); // start animation - await tester.pump(const Duration(seconds: 1)); + await tester.pumpAndSettle(); - final StatefulElement widget = tester.element( - find.descendant(of: find.byType(AlertDialog), matching: find.byType(Material))); - final Material materialWidget = widget.state.widget; + final Material materialWidget = _getMaterialFromDialog(tester); expect(materialWidget.shape, _defaultDialogShape); }); @@ -130,12 +155,9 @@ void main() { await tester.pumpWidget(_appWithAlertDialog(tester, dialog)); await tester.tap(find.text('X')); - await tester.pump(); // start animation - await tester.pump(const Duration(seconds: 1)); + await tester.pumpAndSettle(); - final StatefulElement widget = tester.element( - find.descendant(of: find.byType(AlertDialog), matching: find.byType(Material))); - final Material materialWidget = widget.state.widget; + final Material materialWidget = _getMaterialFromDialog(tester); expect(materialWidget.shape, customBorder); }); @@ -406,8 +428,7 @@ void main() { ))); await tester.tap(find.text('X')); - await tester.pump(); // start animation - await tester.pump(const Duration(seconds: 1)); + await tester.pumpAndSettle(); expect(semantics, includesNodeWith( label: 'Title', diff --git a/packages/flutter/test/material/dialog_theme_test.dart b/packages/flutter/test/material/dialog_theme_test.dart index ac02acc2a4..77917e3bd7 100644 --- a/packages/flutter/test/material/dialog_theme_test.dart +++ b/packages/flutter/test/material/dialog_theme_test.dart @@ -5,6 +5,7 @@ import 'dart:io' show Platform; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; MaterialApp _appWithAlertDialog(WidgetTester tester, AlertDialog dialog, {ThemeData theme}) { @@ -34,7 +35,61 @@ MaterialApp _appWithAlertDialog(WidgetTester tester, AlertDialog dialog, {ThemeD final Key _painterKey = UniqueKey(); +Material _getMaterialFromDialog(WidgetTester tester) { + return tester.widget(find.descendant(of: find.byType(AlertDialog), matching: find.byType(Material))); +} + 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 description = builder.properties + .where((DiagnosticsNode n) => !n.isFiltered(DiagnosticLevel.info)) + .map((DiagnosticsNode n) => n.toString()).toList(); + expect(description, [ + '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: [ ], + ); + 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: [ ], + ); + 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 { const RoundedRectangleBorder customBorder = RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16.0))); @@ -48,12 +103,9 @@ void main() { _appWithAlertDialog(tester, dialog, theme: theme) ); await tester.tap(find.text('X')); - await tester.pump(); // start animation - await tester.pump(const Duration(seconds: 1)); + await tester.pumpAndSettle(); - final StatefulElement widget = tester.element( - find.descendant(of: find.byType(AlertDialog), matching: find.byType(Material))); - final Material materialWidget = widget.state.widget; + final Material materialWidget = _getMaterialFromDialog(tester); expect(materialWidget.shape, customBorder); });