From 6dd929ab289c90cdb22e07a3c650115cbdc55728 Mon Sep 17 00:00:00 2001 From: Qun Cheng <36861262+QuncCccccc@users.noreply.github.com> Date: Thu, 5 Sep 2024 15:00:23 -0700 Subject: [PATCH] Normalize Dialog theme (#153982) This PR is to make preparations to make `DialogTheme` conform to Flutter's conventions for component themes: * Added a `DialogThemeData` class which defines overrides for the defaults for `Dialog` properties. * Added 2 `DialogTheme` constructor parameters: `DialogThemeData? data` and `Widget? child`. This is now the preferred way to configure a `DialogTheme`: ``` DialogTheme( data: DialogThemeData(color: xxx, elevation: xxx, ...), child: Dialog(...) ) ``` These two properties are made nullable to not break existing apps which has customized `ThemeData.dialogTheme`. * Changed the type of theme defaults from `DialogTheme` to `DialogThemeData`. TODO: * Fix internal failures. * Change the type of `ThemeData.dialogTheme` from `DialogTheme` to `DialogThemeData`. This may cause breaking changes, a migration guide will be created. Addresses the "theme normalization" sub project within https://github.com/flutter/flutter/issues/91772 --- .../gen_defaults/lib/dialog_template.dart | 4 +- packages/flutter/lib/src/material/dialog.dart | 37 ++- .../lib/src/material/dialog_theme.dart | 310 +++++++++++++++++- .../test/material/dialog_theme_test.dart | 200 +++++++++-- 4 files changed, 487 insertions(+), 64 deletions(-) diff --git a/dev/tools/gen_defaults/lib/dialog_template.dart b/dev/tools/gen_defaults/lib/dialog_template.dart index dd54d3978e..d0162a32e7 100644 --- a/dev/tools/gen_defaults/lib/dialog_template.dart +++ b/dev/tools/gen_defaults/lib/dialog_template.dart @@ -12,7 +12,7 @@ class DialogTemplate extends TokenTemplate { @override String generate() => ''' -class _${blockName}DefaultsM3 extends DialogTheme { +class _${blockName}DefaultsM3 extends DialogThemeData { _${blockName}DefaultsM3(this.context) : super( alignment: Alignment.center, @@ -54,7 +54,7 @@ class DialogFullscreenTemplate extends TokenTemplate { @override String generate() => ''' -class _${blockName}DefaultsM3 extends DialogTheme { +class _${blockName}DefaultsM3 extends DialogThemeData { const _${blockName}DefaultsM3(this.context): super(clipBehavior: Clip.none); final BuildContext context; diff --git a/packages/flutter/lib/src/material/dialog.dart b/packages/flutter/lib/src/material/dialog.dart index 518b58ec38..dc52b0cdf1 100644 --- a/packages/flutter/lib/src/material/dialog.dart +++ b/packages/flutter/lib/src/material/dialog.dart @@ -108,7 +108,7 @@ class Dialog extends StatelessWidget { /// and a surface tint overlay on the background color if [surfaceTintColor] is /// non null. /// - /// If null then [DialogTheme.elevation] is used, and if that is null then + /// If null then [DialogThemeData.elevation] is used, and if that is null then /// the elevation will match the Material Design specification for Dialogs. /// /// See also: @@ -197,7 +197,7 @@ class Dialog extends StatelessWidget { /// See the enum [Clip] for details of all possible options and their common /// use cases. /// - /// If null, then [DialogTheme.clipBehavior] is used. If that is also null, + /// If null, then [DialogThemeData.clipBehavior] is used. If that is also null, /// defaults to [Clip.none]. /// {@endtemplate} final Clip? clipBehavior; @@ -214,7 +214,7 @@ class Dialog extends StatelessWidget { /// {@template flutter.material.dialog.alignment} /// How to align the [Dialog]. /// - /// If null, then [DialogTheme.alignment] is used. If that is also null, the + /// If null, then [DialogThemeData.alignment] is used. If that is also null, the /// default is [Alignment.center]. /// {@endtemplate} final AlignmentGeometry? alignment; @@ -230,10 +230,10 @@ class Dialog extends StatelessWidget { @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); - final DialogTheme dialogTheme = DialogTheme.of(context); + final DialogThemeData dialogTheme = DialogTheme.of(context).data; final EdgeInsets effectivePadding = MediaQuery.viewInsetsOf(context) + (insetPadding ?? dialogTheme.insetPadding ?? _defaultInsetPadding); - final DialogTheme defaults = theme.useMaterial3 + final DialogThemeData defaults = theme.useMaterial3 ? (_fullscreen ? _DialogFullscreenDefaultsM3(context) : _DialogDefaultsM3(context)) : _DialogDefaultsM2(context); @@ -481,7 +481,7 @@ class AlertDialog extends StatelessWidget { /// Color for the [Icon] in the [icon] of this [AlertDialog]. /// - /// If null, [DialogTheme.iconColor] is used. If that is null, defaults to + /// If null, [DialogThemeData.iconColor] is used. If that is null, defaults to /// color scheme's [ColorScheme.secondary] if [ThemeData.useMaterial3] is /// true, black otherwise. final Color? iconColor; @@ -519,7 +519,7 @@ class AlertDialog extends StatelessWidget { /// Style for the text in the [title] of this [AlertDialog]. /// - /// If null, [DialogTheme.titleTextStyle] is used. If that's null, defaults to + /// If null, [DialogThemeData.titleTextStyle] is used. If that's null, defaults to /// [TextTheme.headlineSmall] of [ThemeData.textTheme] if /// [ThemeData.useMaterial3] is true, [TextTheme.titleLarge] otherwise. final TextStyle? titleTextStyle; @@ -554,7 +554,7 @@ class AlertDialog extends StatelessWidget { /// Style for the text in the [content] of this [AlertDialog]. /// - /// If null, [DialogTheme.contentTextStyle] is used. If that's null, defaults + /// If null, [DialogThemeData.contentTextStyle] is used. If that's null, defaults /// to [TextTheme.bodyMedium] of [ThemeData.textTheme] if /// [ThemeData.useMaterial3] is true, [TextTheme.titleMedium] otherwise. final TextStyle? contentTextStyle; @@ -720,8 +720,8 @@ class AlertDialog extends StatelessWidget { assert(debugCheckHasMaterialLocalizations(context)); final ThemeData theme = Theme.of(context); - final DialogTheme dialogTheme = DialogTheme.of(context); - final DialogTheme defaults = theme.useMaterial3 ? _DialogDefaultsM3(context) : _DialogDefaultsM2(context); + final DialogThemeData dialogTheme = DialogTheme.of(context).data; + final DialogThemeData defaults = theme.useMaterial3 ? _DialogDefaultsM3(context) : _DialogDefaultsM2(context); String? label = semanticLabel; switch (theme.platform) { @@ -1155,7 +1155,7 @@ class SimpleDialog extends StatelessWidget { /// Style for the text in the [title] of this [SimpleDialog]. /// - /// If null, [DialogTheme.titleTextStyle] is used. If that's null, defaults to + /// If null, [DialogThemeData.titleTextStyle] is used. If that's null, defaults to /// [TextTheme.titleLarge] of [ThemeData.textTheme]. final TextStyle? titleTextStyle; @@ -1235,7 +1235,7 @@ class SimpleDialog extends StatelessWidget { // The paddingScaleFactor is used to adjust the padding of Dialog // children. - final TextStyle defaultTextStyle = titleTextStyle ?? DialogTheme.of(context).titleTextStyle ?? theme.textTheme.titleLarge!; + final TextStyle defaultTextStyle = titleTextStyle ?? DialogTheme.of(context).data.titleTextStyle ?? theme.textTheme.titleLarge!; final double fontSize = defaultTextStyle.fontSize ?? kDefaultFontSize; final double fontSizeToScale = fontSize == 0.0 ? kDefaultFontSize : fontSize; final double effectiveTextScale = MediaQuery.textScalerOf(context).scale(fontSizeToScale) / fontSizeToScale; @@ -1342,7 +1342,7 @@ Widget _buildMaterialDialogTransitions(BuildContext context, Animation a /// /// The `barrierColor` argument is used to specify the color of the modal /// barrier that darkens everything below the dialog. If `null` the `barrierColor` -/// field from `DialogTheme` is used. If that is `null` the default color +/// field from `DialogThemeData` is used. If that is `null` the default color /// `Colors.black54` is used. /// /// The `useSafeArea` argument is used to indicate if the dialog should only @@ -1443,7 +1443,10 @@ Future showDialog({ return Navigator.of(context, rootNavigator: useRootNavigator).push(DialogRoute( context: context, builder: builder, - barrierColor: barrierColor ?? Theme.of(context).dialogTheme.barrierColor ?? Colors.black54, + barrierColor: barrierColor + ?? DialogTheme.of(context).barrierColor + ?? Theme.of(context).dialogTheme.data.barrierColor + ?? Colors.black54, barrierDismissible: barrierDismissible, barrierLabel: barrierLabel, useSafeArea: useSafeArea, @@ -1633,7 +1636,7 @@ double _scalePadding(double textScaleFactor) { } // Hand coded defaults based on Material Design 2. -class _DialogDefaultsM2 extends DialogTheme { +class _DialogDefaultsM2 extends DialogThemeData { _DialogDefaultsM2(this.context) : _textTheme = Theme.of(context).textTheme, _iconTheme = Theme.of(context).iconTheme, @@ -1674,7 +1677,7 @@ class _DialogDefaultsM2 extends DialogTheme { // Design token database by the script: // dev/tools/gen_defaults/bin/gen_defaults.dart. -class _DialogDefaultsM3 extends DialogTheme { +class _DialogDefaultsM3 extends DialogThemeData { _DialogDefaultsM3(this.context) : super( alignment: Alignment.center, @@ -1718,7 +1721,7 @@ class _DialogDefaultsM3 extends DialogTheme { // Design token database by the script: // dev/tools/gen_defaults/bin/gen_defaults.dart. -class _DialogFullscreenDefaultsM3 extends DialogTheme { +class _DialogFullscreenDefaultsM3 extends DialogThemeData { const _DialogFullscreenDefaultsM3(this.context): super(clipBehavior: Clip.none); final BuildContext context; diff --git a/packages/flutter/lib/src/material/dialog_theme.dart b/packages/flutter/lib/src/material/dialog_theme.dart index b5eb93615c..ce100dc1a9 100644 --- a/packages/flutter/lib/src/material/dialog_theme.dart +++ b/packages/flutter/lib/src/material/dialog_theme.dart @@ -28,9 +28,288 @@ import 'theme.dart'; /// * [ThemeData], which describes the overall theme information for the /// application. @immutable -class DialogTheme with Diagnosticable { +class DialogTheme extends InheritedTheme with Diagnosticable { /// Creates a dialog theme that can be used for [ThemeData.dialogTheme]. const DialogTheme({ + super.key, + Color? backgroundColor, + double? elevation, + Color? shadowColor, + Color? surfaceTintColor, + ShapeBorder? shape, + AlignmentGeometry? alignment, + Color? iconColor, + TextStyle? titleTextStyle, + TextStyle? contentTextStyle, + EdgeInsetsGeometry? actionsPadding, + Color? barrierColor, + EdgeInsets? insetPadding, + Clip? clipBehavior, + DialogThemeData? data, + Widget? child, + }) : assert( + data == null || + (backgroundColor ?? + elevation ?? + shadowColor ?? + surfaceTintColor ?? + shape ?? + alignment ?? + iconColor ?? + titleTextStyle ?? + contentTextStyle ?? + actionsPadding ?? + barrierColor ?? + insetPadding ?? + clipBehavior) == null), + _data = data, + _backgroundColor = backgroundColor, + _elevation = elevation, + _shadowColor = shadowColor, + _surfaceTintColor = surfaceTintColor, + _shape = shape, + _alignment = alignment, + _iconColor = iconColor, + _titleTextStyle = titleTextStyle, + _contentTextStyle = contentTextStyle, + _actionsPadding = actionsPadding, + _barrierColor = barrierColor, + _insetPadding = insetPadding, + _clipBehavior = clipBehavior, + super(child: child ?? const SizedBox()); + + final DialogThemeData? _data; + final Color? _backgroundColor; + final double? _elevation; + final Color? _shadowColor; + final Color? _surfaceTintColor; + final ShapeBorder? _shape; + final AlignmentGeometry? _alignment; + final TextStyle? _titleTextStyle; + final TextStyle? _contentTextStyle; + final EdgeInsetsGeometry? _actionsPadding; + final Color? _iconColor; + final Color? _barrierColor; + final EdgeInsets? _insetPadding; + final Clip? _clipBehavior; + + /// Overrides the default value for [Dialog.backgroundColor]. + /// + /// This property is obsolete and will be deprecated in a future release: + /// please use the [DialogThemeData.backgroundColor] property in [data] instead. + Color? get backgroundColor => _data != null ? _data.backgroundColor : _backgroundColor; + + /// Overrides the default value for [Dialog.elevation]. + /// + /// This property is obsolete and will be deprecated in a future release: + /// please use the [DialogThemeData.elevation] property in [data] instead. + double? get elevation => _data != null ? _data.elevation : _elevation; + + /// Overrides the default value for [Dialog.shadowColor]. + /// + /// This property is obsolete and will be deprecated in a future release: + /// please use the [DialogThemeData.shadowColor] property in [data] instead. + Color? get shadowColor => _data != null ? _data.shadowColor : _shadowColor; + + /// Overrides the default value for [Dialog.surfaceTintColor]. + /// + /// This property is obsolete and will be deprecated in a future release: + /// please use the [DialogThemeData.surfaceTintColor] property in [data] instead. + Color? get surfaceTintColor => _data != null ? _data.surfaceTintColor : _surfaceTintColor; + + /// Overrides the default value for [Dialog.shape]. + /// + /// This property is obsolete and will be deprecated in a future release: + /// please use the [DialogThemeData.shape] property in [data] instead. + ShapeBorder? get shape => _data != null ? _data.shape : _shape; + + /// Overrides the default value for [Dialog.alignment]. + /// + /// This property is obsolete and will be deprecated in a future release: + /// please use the [DialogThemeData.alignment] property in [data] instead. + AlignmentGeometry? get alignment => _data != null ? _data.alignment : _alignment; + + /// Overrides the default value for [DefaultTextStyle] for [SimpleDialog.title] and + /// [AlertDialog.title]. + /// + /// This property is obsolete and will be deprecated in a future release: + /// please use the [DialogThemeData.titleTextStyle] property in [data] instead. + TextStyle? get titleTextStyle => _data != null ? _data.titleTextStyle : _titleTextStyle; + + /// Overrides the default value for [DefaultTextStyle] for [SimpleDialog.children] and + /// [AlertDialog.content]. + /// + /// This property is obsolete and will be deprecated in a future release: + /// please use the [DialogThemeData.contentTextStyle] property in [data] instead. + TextStyle? get contentTextStyle => _data != null ? _data.contentTextStyle : _contentTextStyle; + + /// Overrides the default value for [AlertDialog.actionsPadding]. + /// + /// This property is obsolete and will be deprecated in a future release: + /// please use the [DialogThemeData.actionsPadding] property in [data] instead. + EdgeInsetsGeometry? get actionsPadding => _data != null ? _data.actionsPadding : _actionsPadding; + + /// Used to configure the [IconTheme] for the [AlertDialog.icon] widget. + /// + /// This property is obsolete and will be deprecated in a future release: + /// please use the [DialogThemeData.iconColor] property in [data] instead. + Color? get iconColor => _data != null ? _data.iconColor : _iconColor; + + /// Overrides the default value for [barrierColor] in [showDialog]. + /// + /// This property is obsolete and will be deprecated in a future release: + /// please use the [DialogThemeData.barrierColor] property in [data] instead. + Color? get barrierColor => _data != null ? _data.barrierColor : _barrierColor; + + /// Overrides the default value for [Dialog.insetPadding]. + EdgeInsets? get insetPadding => _data != null ? _data.insetPadding : _insetPadding; + + /// Overrides the default value of [Dialog.clipBehavior]. + /// + /// This property is obsolete and will be deprecated in a future release: + /// please use the [DialogThemeData.clipBehavior] property in [data] instead. + Clip? get clipBehavior => _data != null ? _data.clipBehavior : _clipBehavior; + + /// The properties used for all descendant [Dialog] widgets. + DialogThemeData get data { + return _data ?? DialogThemeData( + backgroundColor: _backgroundColor, + elevation: _elevation, + shadowColor: _shadowColor, + surfaceTintColor: _surfaceTintColor, + shape: _shape, + alignment: _alignment, + iconColor: _iconColor, + titleTextStyle: _titleTextStyle, + contentTextStyle: _contentTextStyle, + actionsPadding: _actionsPadding, + barrierColor: _barrierColor, + insetPadding: _insetPadding, + clipBehavior: _clipBehavior, + ); + } + + /// The [ThemeData.dialogTheme] property of the ambient [Theme]. + static DialogTheme of(BuildContext context) { + final DialogTheme? dialogTheme = context.dependOnInheritedWidgetOfExactType(); + return dialogTheme ?? Theme.of(context).dialogTheme; + } + + @override + Widget wrap(BuildContext context, Widget child) { + return DialogTheme(data: data, child: child); + } + + @override + bool updateShouldNotify(DialogTheme oldWidget) => data != oldWidget.data; + + /// Creates a copy of this object but with the given fields replaced with the + /// new values. + /// + /// This method is obsolete and will be deprecated in a future release: + /// please use the [DialogThemeData.copyWith] instead. + DialogTheme copyWith({ + Color? backgroundColor, + double? elevation, + Color? shadowColor, + Color? surfaceTintColor, + ShapeBorder? shape, + AlignmentGeometry? alignment, + Color? iconColor, + TextStyle? titleTextStyle, + TextStyle? contentTextStyle, + EdgeInsetsGeometry? actionsPadding, + Color? barrierColor, + EdgeInsets? insetPadding, + Clip? clipBehavior, + }) { + return DialogTheme( + backgroundColor: backgroundColor ?? this.backgroundColor, + elevation: elevation ?? this.elevation, + shadowColor: shadowColor ?? this.shadowColor, + surfaceTintColor: surfaceTintColor ?? this.surfaceTintColor, + shape: shape ?? this.shape, + alignment: alignment ?? this.alignment, + iconColor: iconColor ?? this.iconColor, + titleTextStyle: titleTextStyle ?? this.titleTextStyle, + contentTextStyle: contentTextStyle ?? this.contentTextStyle, + actionsPadding: actionsPadding ?? this.actionsPadding, + barrierColor: barrierColor ?? this.barrierColor, + insetPadding: insetPadding ?? this.insetPadding, + clipBehavior: clipBehavior ?? this.clipBehavior, + ); + } + + /// Linearly interpolate between two dialog themes. + /// + /// {@macro dart.ui.shadow.lerp} + /// + /// This method is obsolete and will be deprecated in a future release: + /// please use the [DialogThemeData.lerp] instead. + static DialogTheme lerp(DialogTheme? a, DialogTheme? b, double t) { + if (identical(a, b) && a != null) { + return a; + } + return DialogTheme( + backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t), + elevation: lerpDouble(a?.elevation, b?.elevation, t), + shadowColor: Color.lerp(a?.shadowColor, b?.shadowColor, t), + surfaceTintColor: Color.lerp(a?.surfaceTintColor, b?.surfaceTintColor, t), + shape: ShapeBorder.lerp(a?.shape, b?.shape, t), + alignment: AlignmentGeometry.lerp(a?.alignment, b?.alignment, t), + iconColor: Color.lerp(a?.iconColor, b?.iconColor, t), + titleTextStyle: TextStyle.lerp(a?.titleTextStyle, b?.titleTextStyle, t), + contentTextStyle: TextStyle.lerp(a?.contentTextStyle, b?.contentTextStyle, t), + actionsPadding: EdgeInsetsGeometry.lerp(a?.actionsPadding, b?.actionsPadding, t), + barrierColor: Color.lerp(a?.barrierColor, b?.barrierColor, t), + insetPadding: EdgeInsets.lerp(a?.insetPadding, b?.insetPadding, t), + clipBehavior: t < 0.5 ? a?.clipBehavior : b?.clipBehavior, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ColorProperty('backgroundColor', backgroundColor, defaultValue: null)); + properties.add(DoubleProperty('elevation', elevation, defaultValue: null)); + properties.add(ColorProperty('shadowColor', shadowColor, defaultValue: null)); + properties.add(ColorProperty('surfaceTintColor', surfaceTintColor, defaultValue: null)); + properties.add(DiagnosticsProperty('shape', shape, defaultValue: null)); + properties.add(DiagnosticsProperty('alignment', alignment, defaultValue: null)); + properties.add(ColorProperty('iconColor', iconColor, defaultValue: null)); + properties.add(DiagnosticsProperty('titleTextStyle', titleTextStyle, defaultValue: null)); + properties.add(DiagnosticsProperty('contentTextStyle', contentTextStyle, defaultValue: null)); + properties.add(DiagnosticsProperty('actionsPadding', actionsPadding, defaultValue: null)); + properties.add(ColorProperty('barrierColor', barrierColor, defaultValue: null)); + properties.add(DiagnosticsProperty('insetPadding', insetPadding, defaultValue: null)); + properties.add(DiagnosticsProperty('clipBehavior', clipBehavior, defaultValue: null)); + } +} + +/// Defines default property values for descendant [Dialog] widgets. +/// +/// Descendant widgets obtain the current [DialogThemeData] object using +/// `CardTheme.of(context).data`. Instances of [DialogThemeData] can be +/// customized with [DialogThemeData.copyWith]. +/// +/// Typically a [DialogThemeData] is specified as part of the overall [Theme] +/// with [ThemeData.dialogTheme]. +/// +/// All [DialogThemeData] properties are `null` by default. When null, the [Dialog] +/// will use the values from [ThemeData] if they exist, otherwise it will +/// provide its own defaults. See the individual [Dialog] properties for details. +/// +/// See also: +/// +/// * [Dialog], a dialog that can be customized using this [DialogTheme]. +/// * [AlertDialog], a dialog that can be customized using this [DialogTheme]. +/// * [SimpleDialog], a dialog that can be customized using this [DialogTheme]. +/// * [ThemeData], which describes the overall theme information for the +/// application. +@immutable +class DialogThemeData with Diagnosticable { + /// Creates a dialog theme that can be used for [ThemeData.dialogTheme]. + const DialogThemeData({ this.backgroundColor, this.elevation, this.shadowColor, @@ -89,7 +368,7 @@ class DialogTheme with Diagnosticable { /// Creates a copy of this object but with the given fields replaced with the /// new values. - DialogTheme copyWith({ + DialogThemeData copyWith({ Color? backgroundColor, double? elevation, Color? shadowColor, @@ -104,7 +383,7 @@ class DialogTheme with Diagnosticable { EdgeInsets? insetPadding, Clip? clipBehavior, }) { - return DialogTheme( + return DialogThemeData( backgroundColor: backgroundColor ?? this.backgroundColor, elevation: elevation ?? this.elevation, shadowColor: shadowColor ?? this.shadowColor, @@ -121,19 +400,14 @@ class DialogTheme with Diagnosticable { ); } - /// The data from the closest [DialogTheme] instance given the build context. - static DialogTheme of(BuildContext context) { - return Theme.of(context).dialogTheme; - } - - /// Linearly interpolate between two dialog themes. + /// Linearly interpolate between two [DialogThemeData]. /// /// {@macro dart.ui.shadow.lerp} - static DialogTheme lerp(DialogTheme? a, DialogTheme? b, double t) { + static DialogThemeData lerp(DialogThemeData? a, DialogThemeData? b, double t) { if (identical(a, b) && a != null) { return a; } - return DialogTheme( + return DialogThemeData( backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t), elevation: lerpDouble(a?.elevation, b?.elevation, t), shadowColor: Color.lerp(a?.shadowColor, b?.shadowColor, t), @@ -175,7 +449,7 @@ class DialogTheme with Diagnosticable { if (other.runtimeType != runtimeType) { return false; } - return other is DialogTheme + return other is DialogThemeData && other.backgroundColor == backgroundColor && other.elevation == elevation && other.shadowColor == shadowColor @@ -194,17 +468,17 @@ class DialogTheme with Diagnosticable { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(ColorProperty('backgroundColor', backgroundColor)); - properties.add(DoubleProperty('elevation', elevation)); - properties.add(ColorProperty('shadowColor', shadowColor)); - properties.add(ColorProperty('surfaceTintColor', surfaceTintColor)); + properties.add(ColorProperty('backgroundColor', backgroundColor, defaultValue: null)); + properties.add(DoubleProperty('elevation', elevation, defaultValue: null)); + properties.add(ColorProperty('shadowColor', shadowColor, defaultValue: null)); + properties.add(ColorProperty('surfaceTintColor', surfaceTintColor, defaultValue: null)); properties.add(DiagnosticsProperty('shape', shape, defaultValue: null)); properties.add(DiagnosticsProperty('alignment', alignment, defaultValue: null)); - properties.add(ColorProperty('iconColor', iconColor)); + properties.add(ColorProperty('iconColor', iconColor, defaultValue: null)); properties.add(DiagnosticsProperty('titleTextStyle', titleTextStyle, defaultValue: null)); properties.add(DiagnosticsProperty('contentTextStyle', contentTextStyle, defaultValue: null)); properties.add(DiagnosticsProperty('actionsPadding', actionsPadding, defaultValue: null)); - properties.add(ColorProperty('barrierColor', barrierColor)); + properties.add(ColorProperty('barrierColor', barrierColor, defaultValue: null)); properties.add(DiagnosticsProperty('insetPadding', insetPadding, defaultValue: null)); properties.add(DiagnosticsProperty('clipBehavior', clipBehavior, defaultValue: null)); } diff --git a/packages/flutter/test/material/dialog_theme_test.dart b/packages/flutter/test/material/dialog_theme_test.dart index 9aeb532f40..e43a40c2d1 100644 --- a/packages/flutter/test/material/dialog_theme_test.dart +++ b/packages/flutter/test/material/dialog_theme_test.dart @@ -12,27 +12,45 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; -MaterialApp _appWithDialog(WidgetTester tester, Widget dialog, { ThemeData? theme }) { +MaterialApp _appWithDialog( + WidgetTester tester, + Widget dialog, { + ThemeData? theme, + DialogThemeData? dialogTheme + } +) { + Widget dialogBuilder = Builder( + builder: (BuildContext context) { + return Center( + child: ElevatedButton( + child: const Text('X'), + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return RepaintBoundary( + key: _painterKey, + child: dialog + ); + }, + ); + }, + ), + ); + }, + ); + + if (dialogTheme != null) { + dialogBuilder = DialogTheme( + data: dialogTheme, + child: dialogBuilder, + ); + } + return MaterialApp( theme: theme, home: Material( - child: Builder( - builder: (BuildContext context) { - return Center( - child: ElevatedButton( - child: const Text('X'), - onPressed: () { - showDialog( - context: context, - builder: (BuildContext context) { - return RepaintBoundary(key: _painterKey, child: dialog); - }, - ); - }, - ), - ); - }, - ), + child: dialogBuilder, ), ); } @@ -51,25 +69,78 @@ RenderParagraph _getTextRenderObject(WidgetTester tester, String text) { return tester.element(find.text(text)).renderObject! as RenderParagraph; } +RenderParagraph _getIconRenderObject(WidgetTester tester, IconData icon) { + return tester.renderObject(find.descendant( + of: find.byIcon(icon), + matching: find.byType(RichText) + )); +} + void main() { - test('DialogTheme copyWith, ==, hashCode basics', () { - expect(const DialogTheme(), const DialogTheme().copyWith()); - expect(const DialogTheme().hashCode, const DialogTheme().copyWith().hashCode); + test('DialogThemeData copyWith, ==, hashCode basics', () { + expect(const DialogThemeData(), const DialogThemeData().copyWith()); + expect(const DialogThemeData().hashCode, const DialogThemeData().copyWith().hashCode); }); - test('DialogTheme lerp special cases', () { - expect(DialogTheme.lerp(null, null, 0), const DialogTheme()); - const DialogTheme theme = DialogTheme(); - expect(identical(DialogTheme.lerp(theme, theme, 0.5), theme), true); + test('DialogThemeData lerp special cases', () { + expect(DialogThemeData.lerp(null, null, 0), const DialogThemeData()); + const DialogThemeData theme = DialogThemeData(); + expect(identical(DialogThemeData.lerp(theme, theme, 0.5), theme), true); }); - testWidgets('Dialog Theme implements debugFillProperties', (WidgetTester tester) async { + test('DialogThemeData defaults', () { + const DialogThemeData dialogThemeData = DialogThemeData(); + + expect(dialogThemeData.backgroundColor, null); + expect(dialogThemeData.elevation, null); + expect(dialogThemeData.shadowColor, null); + expect(dialogThemeData.surfaceTintColor, null); + expect(dialogThemeData.shape, null); + expect(dialogThemeData.alignment, null); + expect(dialogThemeData.iconColor, null); + expect(dialogThemeData.titleTextStyle, null); + expect(dialogThemeData.contentTextStyle, null); + expect(dialogThemeData.actionsPadding, null); + expect(dialogThemeData.barrierColor, null); + expect(dialogThemeData.insetPadding, null); + expect(dialogThemeData.clipBehavior, null); + + const DialogTheme dialogTheme = DialogTheme(data: DialogThemeData(), child: SizedBox()); + expect(dialogTheme.backgroundColor, null); + expect(dialogTheme.elevation, null); + expect(dialogTheme.shadowColor, null); + expect(dialogTheme.surfaceTintColor, null); + expect(dialogTheme.shape, null); + expect(dialogTheme.alignment, null); + expect(dialogTheme.iconColor, null); + expect(dialogTheme.titleTextStyle, null); + expect(dialogTheme.contentTextStyle, null); + expect(dialogTheme.actionsPadding, null); + expect(dialogTheme.barrierColor, null); + expect(dialogTheme.insetPadding, null); + expect(dialogTheme.clipBehavior, null); + }); + + testWidgets('Default DialogThemeData debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); - const DialogTheme( + const DialogThemeData().debugFillProperties(builder); + + final List description = builder.properties + .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) + .map((DiagnosticsNode node) => node.toString()) + .toList(); + + expect(description, []); + }); + + testWidgets('DialogThemeData implements debugFillProperties', (WidgetTester tester) async { + final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); + const DialogThemeData( backgroundColor: Color(0xff123456), elevation: 8.0, shadowColor: Color(0xff000001), surfaceTintColor: Color(0xff000002), + shape: BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(20.5))), alignment: Alignment.bottomLeft, iconColor: Color(0xff654321), titleTextStyle: TextStyle(color: Color(0xffffffff)), @@ -87,6 +158,7 @@ void main() { 'elevation: 8.0', 'shadowColor: Color(0xff000001)', 'surfaceTintColor: Color(0xff000002)', + 'shape: BeveledRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.circular(20.5))', 'alignment: Alignment.bottomLeft', 'iconColor: Color(0xff654321)', 'titleTextStyle: TextStyle(inherit: true, color: Color(0xffffffff))', @@ -98,6 +170,80 @@ void main() { ]); }); + testWidgets('Local DialogThemeData overrides dialog defaults', (WidgetTester tester) async { + const Color themeBackgroundColor = Color(0xff123456); + const double themeElevation = 8.0; + const Color themeShadowColor = Color(0xff000001); + const Color themeSurfaceTintColor = Color(0xff000002); + const BeveledRectangleBorder themeShape = BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(20.5))); + const AlignmentGeometry themeAlignment = Alignment.bottomLeft; + const Color themeIconColor = Color(0xff654321); + const TextStyle themeTitleTextStyle = TextStyle(color: Color(0xffffffff)); + const TextStyle themeContentTextStyle = TextStyle(color: Color(0xff000000)); + const EdgeInsetsGeometry themeActionsPadding = EdgeInsets.all(8.0); + const Color themeBarrierColor = Color(0xff000005); + const EdgeInsets themeInsetPadding = EdgeInsets.all(30.0); + const Clip themeClipBehavior = Clip.antiAlias; + const AlertDialog dialog = AlertDialog( + title: Text('Title'), + content: Text('Content'), + icon: Icon(Icons.search), + actions: [ + Icon(Icons.cancel) + ], + ); + + const DialogThemeData dialogTheme = DialogThemeData( + backgroundColor: themeBackgroundColor, + elevation: themeElevation, + shadowColor: themeShadowColor, + surfaceTintColor: themeSurfaceTintColor, + shape: themeShape, + alignment: themeAlignment, + iconColor: themeIconColor, + titleTextStyle: themeTitleTextStyle, + contentTextStyle: themeContentTextStyle, + actionsPadding: themeActionsPadding, + barrierColor: themeBarrierColor, + insetPadding: themeInsetPadding, + clipBehavior: themeClipBehavior, + ); + + await tester.pumpWidget(_appWithDialog( + tester, + dialog, + dialogTheme: dialogTheme + )); + await tester.tap(find.text('X')); + await tester.pumpAndSettle(); + + final Material materialWidget = _getMaterialAlertDialog(tester); + expect(materialWidget.color, themeBackgroundColor); + expect(materialWidget.elevation, themeElevation); + expect(materialWidget.shadowColor, themeShadowColor); + expect(materialWidget.surfaceTintColor, themeSurfaceTintColor); + expect(materialWidget.shape, themeShape); + expect(materialWidget.clipBehavior, Clip.antiAlias); + final Offset bottomLeft = tester.getBottomLeft(find.descendant( + of: find.byType(Dialog), + matching: find.byType(Material) + )); + expect(bottomLeft.dx, 30.0); // 30 is the padding value. + expect(bottomLeft.dy, 570.0); // 600 - 30 + expect(_getIconRenderObject(tester, Icons.search).text.style?.color, themeIconColor); + expect(_getTextRenderObject(tester, 'Title').text.style?.color, themeTitleTextStyle.color); + expect(_getTextRenderObject(tester, 'Content').text.style?.color, themeContentTextStyle.color); + final ModalBarrier modalBarrier = tester.widget(find.byType(ModalBarrier).last); + expect(modalBarrier.color, themeBarrierColor); + + final Finder findPadding = find.ancestor( + of: find.byIcon(Icons.cancel), + matching: find.byType(Padding) + ).first; + final Padding padding = tester.widget(findPadding); + expect(padding.padding, themeActionsPadding); + }); + testWidgets('Dialog background color', (WidgetTester tester) async { const Color customColor = Colors.pink; const AlertDialog dialog = AlertDialog(