Normalize Card theme (#151914)

This PR is to make preparations to make `CardTheme` conform to Flutter's conventions for component themes:
* Added a `CardThemeData` class which defines overrides for the defaults for `Card` properties.
* Added 2 `CardTheme` constructor parameters: `CardThemeData? data` and `Widget? child`. This is now the preferred way to configure a `CardTheme`:
```dart
CardTheme(
  data: CardThemeData(color: xxx, elevation: xxx, ...),
  child: Card(...)
)
```

These two properties are made nullable to not break existing apps which has customized `ThemeData.cardTheme`.
* Changed the type of theme defaults from `CardTheme` to `CardThemeData`.

TODO: 
* Fix internal failures that may have breakages.
* Change the type of `ThemeData.cardTheme` from `CardTheme` to `CardThemeData`. 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
This commit is contained in:
Qun Cheng 2024-07-29 09:48:06 -07:00 committed by GitHub
parent 804cca62de
commit 2e7fa83f67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 273 additions and 53 deletions

View File

@ -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,

View File

@ -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,

View File

@ -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<CardTheme>();
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<Clip>('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<double>('elevation', elevation, defaultValue: null));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('margin', margin, defaultValue: null));
properties.add(DiagnosticsProperty<ShapeBorder>('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

View File

@ -7,19 +7,79 @@
@Tags(<String>['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<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(description, <String>[]);
});
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<String> 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 {