diff --git a/dev/tools/gen_defaults/lib/card_template.dart b/dev/tools/gen_defaults/lib/card_template.dart index 202903fff9..8002756822 100644 --- a/dev/tools/gen_defaults/lib/card_template.dart +++ b/dev/tools/gen_defaults/lib/card_template.dart @@ -26,7 +26,7 @@ class CardTemplate extends TokenTemplate { @override String generate() => ''' -class _${blockName}DefaultsM3 extends CardTheme { +class _${blockName}DefaultsM3 extends CardThemeData { _${blockName}DefaultsM3(this.context) : super( clipBehavior: Clip.none, diff --git a/packages/flutter/lib/src/material/card.dart b/packages/flutter/lib/src/material/card.dart index 04cb0541a5..176a87c6f2 100644 --- a/packages/flutter/lib/src/material/card.dart +++ b/packages/flutter/lib/src/material/card.dart @@ -216,7 +216,7 @@ class Card extends StatelessWidget { @override Widget build(BuildContext context) { final CardTheme cardTheme = CardTheme.of(context); - final CardTheme defaults; + final CardThemeData defaults; if (Theme.of(context).useMaterial3) { defaults = switch (_variant) { _CardVariant.elevated => _CardDefaultsM3(context), @@ -251,7 +251,7 @@ class Card extends StatelessWidget { } // Hand coded defaults based on Material Design 2. -class _CardDefaultsM2 extends CardTheme { +class _CardDefaultsM2 extends CardThemeData { const _CardDefaultsM2(this.context) : super( clipBehavior: Clip.none, @@ -278,7 +278,7 @@ class _CardDefaultsM2 extends CardTheme { // Design token database by the script: // dev/tools/gen_defaults/bin/gen_defaults.dart. -class _CardDefaultsM3 extends CardTheme { +class _CardDefaultsM3 extends CardThemeData { _CardDefaultsM3(this.context) : super( clipBehavior: Clip.none, @@ -311,7 +311,7 @@ class _CardDefaultsM3 extends CardTheme { // Design token database by the script: // dev/tools/gen_defaults/bin/gen_defaults.dart. -class _FilledCardDefaultsM3 extends CardTheme { +class _FilledCardDefaultsM3 extends CardThemeData { _FilledCardDefaultsM3(this.context) : super( clipBehavior: Clip.none, @@ -344,7 +344,7 @@ class _FilledCardDefaultsM3 extends CardTheme { // Design token database by the script: // dev/tools/gen_defaults/bin/gen_defaults.dart. -class _OutlinedCardDefaultsM3 extends CardTheme { +class _OutlinedCardDefaultsM3 extends CardThemeData { _OutlinedCardDefaultsM3(this.context) : super( clipBehavior: Clip.none, diff --git a/packages/flutter/lib/src/material/card_theme.dart b/packages/flutter/lib/src/material/card_theme.dart index 2476bd311a..197289c415 100644 --- a/packages/flutter/lib/src/material/card_theme.dart +++ b/packages/flutter/lib/src/material/card_theme.dart @@ -30,64 +30,111 @@ import 'theme.dart'; /// /// * [ThemeData], which describes the overall theme information for the /// application. -@immutable -class CardTheme with Diagnosticable { +class CardTheme extends InheritedWidget with Diagnosticable { /// Creates a theme that can be used for [ThemeData.cardTheme]. /// /// The [elevation] must be null or non-negative. const CardTheme({ - this.clipBehavior, - this.color, - this.shadowColor, - this.surfaceTintColor, - this.elevation, - this.margin, - this.shape, - }) : assert(elevation == null || elevation >= 0.0); + super.key, + Clip? clipBehavior, + Color? color, + Color? surfaceTintColor, + Color? shadowColor, + double? elevation, + EdgeInsetsGeometry? margin, + ShapeBorder? shape, + CardThemeData? data, + Widget? child, + }) : assert( + data == null || + (clipBehavior ?? + color ?? + surfaceTintColor ?? + shadowColor ?? + elevation ?? + margin ?? + shape) == null), + assert(elevation == null || elevation >= 0.0), + _data = data, + _clipBehavior = clipBehavior, + _color = color, + _surfaceTintColor = surfaceTintColor, + _shadowColor = shadowColor, + _elevation = elevation, + _margin = margin, + _shape = shape, + super(child: child ?? const SizedBox()); + + final CardThemeData? _data; + final Clip? _clipBehavior; + final Color? _color; + final Color? _surfaceTintColor; + final Color? _shadowColor; + final double? _elevation; + final EdgeInsetsGeometry? _margin; + final ShapeBorder? _shape; /// Overrides the default value for [Card.clipBehavior]. /// - /// If null, [Card] uses [Clip.none]. - final Clip? clipBehavior; + /// This property is obsolete and will be deprecated in a future release: + /// please use the [CardThemeData.clipBehavior] property in [data] instead. + Clip? get clipBehavior => _data != null ? _data.clipBehavior : _clipBehavior; /// Overrides the default value for [Card.color]. /// - /// If null, [Card] uses [ThemeData.cardColor]. - final Color? color; - - /// Overrides the default value for [Card.shadowColor]. - /// - /// If null, [Card] defaults to fully opaque black. - final Color? shadowColor; + /// This property is obsolete and will be deprecated in a future release: + /// please use the [CardThemeData.color] property in [data] instead. + Color? get color => _data != null ? _data.color : _color; /// Overrides the default value for [Card.surfaceTintColor]. /// - /// If null, [Card] will not display an overlay color. + /// This property is obsolete and will be deprecated in a future release: + /// please use the [CardThemeData.surfaceTintColor] property in [data] instead. + Color? get surfaceTintColor => _data != null ? _data.surfaceTintColor : _surfaceTintColor; + + /// Overrides the default value for [Card.shadowColor]. /// - /// See [Material.surfaceTintColor] for more details. - final Color? surfaceTintColor; + /// This property is obsolete and will be deprecated in a future release: + /// please use the [CardThemeData.shadowColor] property in [data] instead. + Color? get shadowColor => _data != null ? _data.shadowColor : _shadowColor; /// Overrides the default value for [Card.elevation]. /// - /// If null, [Card] uses a default of 1.0. - final double? elevation; + /// This property is obsolete and will be deprecated in a future release: + /// please use the [CardThemeData.elevation] property in [data] instead. + double? get elevation => _data != null ? _data.elevation : _elevation; /// Overrides the default value for [Card.margin]. /// - /// If null, [Card] uses a default margin of 4.0 logical pixels on all sides: - /// `EdgeInsets.all(4.0)`. - final EdgeInsetsGeometry? margin; + /// This property is obsolete and will be deprecated in a future release: + /// please use the [CardThemeData.margin] property in [data] instead. + EdgeInsetsGeometry? get margin => _data != null ? _data.margin : _margin; /// Overrides the default value for [Card.shape]. /// - /// If null, [Card] then uses a [RoundedRectangleBorder] with a circular - /// corner radius of 12.0 and if [ThemeData.useMaterial3] is false, - /// then the circular corner radius will be 4.0. - final ShapeBorder? shape; + /// This property is obsolete and will be deprecated in a future release: + /// please use the [CardThemeData.shape] property in [data] instead. + ShapeBorder? get shape => _data != null ? _data.shape : _shape; + + /// The properties used for all descendant [Card] widgets. + CardThemeData get data { + return _data ?? CardThemeData( + clipBehavior: _clipBehavior, + color: _color, + surfaceTintColor: _surfaceTintColor, + shadowColor: _shadowColor, + elevation: _elevation, + margin: _margin, + shape: _shape, + ); + } /// Creates a copy of this object with the given fields replaced with the /// new values. + /// + /// This method is obsolete and will be deprecated in a future release: + /// please use the [CardThemeData.copyWith] instead. CardTheme copyWith({ Clip? clipBehavior, Color? color, @@ -110,12 +157,19 @@ class CardTheme with Diagnosticable { /// The [ThemeData.cardTheme] property of the ambient [Theme]. static CardTheme of(BuildContext context) { - return Theme.of(context).cardTheme; + final CardTheme? cardTheme = context.dependOnInheritedWidgetOfExactType(); + return cardTheme ?? Theme.of(context).cardTheme; } + @override + bool updateShouldNotify(CardTheme oldWidget) => data != oldWidget.data; + /// Linearly interpolate between two Card themes. /// /// {@macro dart.ui.shadow.lerp} + /// + /// This method is obsolete and will be deprecated in a future release: + /// please use the [CardThemeData.lerp] instead. static CardTheme lerp(CardTheme? a, CardTheme? b, double t) { if (identical(a, b) && a != null) { return a; @@ -131,6 +185,112 @@ class CardTheme with Diagnosticable { ); } + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('clipBehavior', clipBehavior, defaultValue: null)); + properties.add(ColorProperty('color', color, defaultValue: null)); + properties.add(ColorProperty('shadowColor', shadowColor, defaultValue: null)); + properties.add(ColorProperty('surfaceTintColor', surfaceTintColor, defaultValue: null)); + properties.add(DiagnosticsProperty('elevation', elevation, defaultValue: null)); + properties.add(DiagnosticsProperty('margin', margin, defaultValue: null)); + properties.add(DiagnosticsProperty('shape', shape, defaultValue: null)); + } +} + +/// Defines default property values for descendant [Card] widgets. +/// +/// Descendant widgets obtain the current [CardThemeData] object using +/// `CardTheme.of(context)`. Instances of [CardThemeData] can be +/// customized with [CardThemeData.copyWith]. +/// +/// Typically a [CardThemeData] is specified as part of the overall [Theme] +/// with [ThemeData.cardTheme]. +/// +/// All [CardThemeData] properties are `null` by default. When null, the [Card] +/// will use the values from [ThemeData] if they exist, otherwise it will +/// provide its own defaults. See the individual [Card] properties for details. +/// +/// See also: +/// +/// * [ThemeData], which describes the overall theme information for the +/// application. +@immutable +class CardThemeData with Diagnosticable { + /// Creates a theme that can be used for [ThemeData.cardTheme]. + /// + /// The [elevation] must be null or non-negative. + const CardThemeData({ + this.clipBehavior, + this.color, + this.shadowColor, + this.surfaceTintColor, + this.elevation, + this.margin, + this.shape, + }) : assert(elevation == null || elevation >= 0.0); + + /// Overrides the default value for [Card.clipBehavior]. + final Clip? clipBehavior; + + /// Overrides the default value for [Card.color]. + final Color? color; + + /// Overrides the default value for [Card.shadowColor]. + final Color? shadowColor; + + /// Overrides the default value for [Card.surfaceTintColor]. + final Color? surfaceTintColor; + + /// Overrides the default value for [Card.elevation]. + final double? elevation; + + /// Overrides the default value for [Card.margin]. + final EdgeInsetsGeometry? margin; + + /// Overrides the default value for [Card.shape]. + final ShapeBorder? shape; + + /// Creates a copy of this object with the given fields replaced with the + /// new values. + CardThemeData copyWith({ + Clip? clipBehavior, + Color? color, + Color? shadowColor, + Color? surfaceTintColor, + double? elevation, + EdgeInsetsGeometry? margin, + ShapeBorder? shape, + }) { + return CardThemeData( + clipBehavior: clipBehavior ?? this.clipBehavior, + color: color ?? this.color, + shadowColor: shadowColor ?? this.shadowColor, + surfaceTintColor: surfaceTintColor ?? this.surfaceTintColor, + elevation: elevation ?? this.elevation, + margin: margin ?? this.margin, + shape: shape ?? this.shape, + ); + } + + /// Linearly interpolate between two Card themes. + /// + /// {@macro dart.ui.shadow.lerp} + static CardThemeData lerp(CardThemeData? a, CardThemeData? b, double t) { + if (identical(a, b) && a != null) { + return a; + } + return CardThemeData( + clipBehavior: t < 0.5 ? a?.clipBehavior : b?.clipBehavior, + color: Color.lerp(a?.color, b?.color, t), + shadowColor: Color.lerp(a?.shadowColor, b?.shadowColor, t), + surfaceTintColor: Color.lerp(a?.surfaceTintColor, b?.surfaceTintColor, t), + elevation: lerpDouble(a?.elevation, b?.elevation, t), + margin: EdgeInsetsGeometry.lerp(a?.margin, b?.margin, t), + shape: ShapeBorder.lerp(a?.shape, b?.shape, t), + ); + } + @override int get hashCode => Object.hash( clipBehavior, @@ -150,14 +310,14 @@ class CardTheme with Diagnosticable { if (other.runtimeType != runtimeType) { return false; } - return other is CardTheme - && other.clipBehavior == clipBehavior - && other.color == color - && other.shadowColor == shadowColor - && other.surfaceTintColor == surfaceTintColor - && other.elevation == elevation - && other.margin == margin - && other.shape == shape; + return other is CardThemeData + && other.clipBehavior == clipBehavior + && other.color == color + && other.shadowColor == shadowColor + && other.surfaceTintColor == surfaceTintColor + && other.elevation == elevation + && other.margin == margin + && other.shape == shape; } @override diff --git a/packages/flutter/test/material/card_theme_test.dart b/packages/flutter/test/material/card_theme_test.dart index 17792d394a..f897cfa559 100644 --- a/packages/flutter/test/material/card_theme_test.dart +++ b/packages/flutter/test/material/card_theme_test.dart @@ -7,19 +7,79 @@ @Tags(['reduced-test-set']) library; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - test('CardTheme copyWith, ==, hashCode basics', () { - expect(const CardTheme(), const CardTheme().copyWith()); - expect(const CardTheme().hashCode, const CardTheme().copyWith().hashCode); + test('CardThemeData copyWith, ==, hashCode basics', () { + expect(const CardThemeData(), const CardThemeData().copyWith()); + expect(const CardThemeData().hashCode, const CardThemeData().copyWith().hashCode); }); - test('CardTheme lerp special cases', () { - expect(CardTheme.lerp(null, null, 0), const CardTheme()); - const CardTheme theme = CardTheme(); - expect(identical(CardTheme.lerp(theme, theme, 0.5), theme), true); + test('CardThemeData lerp special cases', () { + expect(CardThemeData.lerp(null, null, 0), const CardThemeData()); + const CardThemeData theme = CardThemeData(); + expect(identical(CardThemeData.lerp(theme, theme, 0.5), theme), true); + }); + + test('CardThemeData defaults', () { + const CardThemeData cardThemeData = CardThemeData(); + + expect(cardThemeData.clipBehavior, null); + expect(cardThemeData.color, null); + expect(cardThemeData.elevation, null); + expect(cardThemeData.margin, null); + expect(cardThemeData.shadowColor, null); + expect(cardThemeData.shape, null); + expect(cardThemeData.surfaceTintColor, null); + + const CardTheme cardTheme = CardTheme(data: CardThemeData(), child: SizedBox()); + expect(cardTheme.clipBehavior, null); + expect(cardTheme.color, null); + expect(cardTheme.elevation, null); + expect(cardTheme.margin, null); + expect(cardTheme.shadowColor, null); + expect(cardTheme.shape, null); + expect(cardTheme.surfaceTintColor, null); + }); + + testWidgets('Default CardThemeData debugFillProperties', (WidgetTester tester) async { + final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); + const CardThemeData().debugFillProperties(builder); + + final List description = builder.properties + .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) + .map((DiagnosticsNode node) => node.toString()) + .toList(); + + expect(description, []); + }); + + testWidgets('CardThemeData implements debugFillProperties', (WidgetTester tester) async { + final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); + const CardThemeData( + clipBehavior: Clip.antiAlias, + color: Colors.amber, + elevation: 10.5, + margin: EdgeInsets.all(20.5), + shadowColor: Colors.green, + surfaceTintColor: Colors.purple, + shape: BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(20.5))), + ).debugFillProperties(builder); + + final List description = builder.properties + .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) + .map((DiagnosticsNode node) => node.toString()) + .toList(); + + expect(description[0], 'clipBehavior: Clip.antiAlias'); + expect(description[1], 'color: MaterialColor(primary value: Color(0xffffc107))'); + expect(description[2], 'shadowColor: MaterialColor(primary value: Color(0xff4caf50))'); + expect(description[3], 'surfaceTintColor: MaterialColor(primary value: Color(0xff9c27b0))'); + expect(description[4], 'elevation: 10.5'); + expect(description[5], 'margin: EdgeInsets.all(20.5)'); + expect(description[6], 'shape: BeveledRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.circular(20.5))'); }); testWidgets('Material3 - Passing no CardTheme returns defaults', (WidgetTester tester) async {