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:
parent
804cca62de
commit
2e7fa83f67
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user