[Material] Theme data type for cards (#26796)
This change adds a CardTheme and tests for it.
Golden is here:
https://github.com/flutter/goldens/blob/ec26eeebb44068599c100d3f0c2db7102b047737/packages/flutter/test/material/card_theme.custom_shape.png
From commit:
https://github.com/flutter/goldens/commit/ec26eeebb44068599c100d3f0c2db7102b047737#diff-7564b206413654283ddc5cb59ecd64d4
diff --git a/bin/internal/goldens.version b/bin/internal/goldens.version
index 7646d27..2c224cf 100644
--- a/bin/internal/goldens.version
+++ b/bin/internal/goldens.version
@@ -1 +1 @@
-24e9ab38cea4fd85f11ee5a3a491576037294bb9
+6fc7ec65d51116c3f83acb5251e57e779af2ebbb
diff --git a/packages/flutter/lib/material.dart b/packages/flutter/lib/material.dart
index 2277e97..4968e19 100644
--- a/packages/flutter/lib/material.dart
+++ b/packages/flutter/lib/material.dart
@@ -29,6 +29,7 @@
export 'src/material/button_bar.dart';
export 'src/material/button_theme.dart';
export 'src/material/card.dart';
+export 'src/material/card_theme.dart';
export 'src/material/checkbox.dart';
export 'src/material/checkbox_list_tile.dart';
export 'src/material/chip.dart';
diff --git a/packages/flutter/lib/src/material/card.dart b/packages/flutter/lib/src/material/card.dart
index f20a780..4f1495f 100644
--- a/packages/flutter/lib/src/material/card.dart
+++ b/packages/flutter/lib/src/material/card.dart
@@ -4,6 +4,7 @@
import 'package:flutter/widgets.dart';
+import 'card_theme.dart';
import 'material.dart';
import 'theme.dart';
@@ -65,25 +66,25 @@
class Card extends StatelessWidget {
/// Creates a material design card.
///
- /// The [clipBehavior] and [elevation] arguments must not be null.
- /// Additionally, the [elevation] must be non-negative.
+ /// The [elevation] must be null or non-negative.
const Card({
Key key,
this.color,
- this.elevation = 1.0,
+ this.elevation,
this.shape,
- this.margin = const EdgeInsets.all(4.0),
- this.clipBehavior = Clip.none,
+ this.margin,
+ this.clipBehavior,
this.child,
this.semanticContainer = true,
- }) : assert(elevation != null && elevation >= 0.0),
+ }) : assert(elevation == null || elevation >= 0.0),
super(key: key);
/// The card's background color.
///
/// Defines the card's [Material.color].
///
- /// The default color is defined by the ambient [Theme]: [ThemeData.cardColor].
+ /// If this property is null then [ThemeData.cardTheme.color] is used,
+ /// if that's null then [ThemeData.cardColor] is used.
final Color color;
/// The z-coordinate at which to place this card. This controls the size of
@@ -91,25 +92,30 @@
///
/// Defines the card's [Material.elevation].
///
- /// The default elevation is 1.0. The value is always non-negative.
+ /// If this property is null then [ThemeData.cardTheme.elevation] is used,
+ /// if that's null, the default value is 1.0.
final double elevation;
/// The shape of the card's [Material].
///
/// Defines the card's [Material.shape].
///
- /// The default shape is a [RoundedRectangleBorder] with a circular corner
- /// radius of 4.0.
+ /// If this property is null then [ThemeData.cardTheme.shape] is used.
+ /// If that's null then the shape will be a [RoundedRectangleBorder] with a
+ /// circular corner radius of 4.0.
final ShapeBorder shape;
/// {@macro flutter.widgets.Clip}
+ /// If this property is null then [ThemeData.cardTheme.clipBehavior] is used.
+ /// If that's null then the behavior will be [Clip.none].
final Clip clipBehavior;
/// The empty space that surrounds the card.
///
/// Defines the card's outer [Container.margin].
///
- /// The default margin is 4.0 logical pixels on all sides:
+ /// If this property is null then [ThemeData.cardTheme.margin] is used,
+ /// if that's null, the default margin is 4.0 logical pixels on all sides:
/// `EdgeInsets.all(4.0)`.
final EdgeInsetsGeometry margin;
@@ -131,20 +137,25 @@
/// {@macro flutter.widgets.child}
final Widget child;
+ static const double _defaultElevation = 1.0;
+ static const Clip _defaultClipBehavior = Clip.none;
+
@override
Widget build(BuildContext context) {
+ final CardTheme cardTheme = CardTheme.of(context);
+
return Semantics(
container: semanticContainer,
child: Container(
- margin: margin ?? const EdgeInsets.all(4.0),
+ margin: margin ?? cardTheme.margin ?? const EdgeInsets.all(4.0),
child: Material(
type: MaterialType.card,
- color: color ?? Theme.of(context).cardColor,
- elevation: elevation,
- shape: shape ?? const RoundedRectangleBorder(
+ color: color ?? cardTheme.color ?? Theme.of(context).cardColor,
+ elevation: elevation ?? cardTheme.elevation ?? _defaultElevation,
+ shape: shape ?? cardTheme.shape ?? const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(4.0)),
),
- clipBehavior: clipBehavior,
+ clipBehavior: clipBehavior ?? cardTheme.clipBehavior ?? _defaultClipBehavior,
child: Semantics(
explicitChildNodes: !semanticContainer,
child: child,
diff --git a/packages/flutter/lib/src/material/card_theme.dart b/packages/flutter/lib/src/material/card_theme.dart
new file mode 100644
index 0000000..266017c
--- /dev/null
+++ b/packages/flutter/lib/src/material/card_theme.dart
@@ -0,0 +1,142 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// 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';
+
+import 'theme.dart';
+
+/// Defines default property values for descendant [Card] widgets.
+///
+/// Descendant widgets obtain the current [CardTheme] object using
+/// `CardTheme.of(context)`. Instances of [CardTheme] can be
+/// customized with [CardTheme.copyWith].
+///
+/// Typically a [CardTheme] is specified as part of the overall [Theme]
+/// with [ThemeData.cardTheme].
+///
+/// All [CardTheme] 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 also:
+///
+/// * [ThemeData], which describes the overall theme information for the
+/// application.
+class CardTheme extends 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.elevation,
+ this.margin,
+ this.shape,
+ }) : assert(elevation == null || elevation >= 0.0);
+
+ /// Default value for [Card.clipBehavior].
+ ///
+ /// If null, [Card] uses [Clip.none].
+ final Clip clipBehavior;
+
+ /// Default value for [Card.color].
+ ///
+ /// If null, [Card] uses [ThemeData.cardColor].
+ final Color color;
+
+ /// Default value for [Card.elevation].
+ ///
+ /// If null, [Card] uses a default of 1.0.
+ final double elevation;
+
+ /// 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;
+
+ /// Default value for [Card.shape].
+ ///
+ /// If null, [Card] then uses a [RoundedRectangleBorder] with a circular
+ /// corner radius of 4.0.
+ final ShapeBorder shape;
+
+ /// Creates a copy of this object with the given fields replaced with the
+ /// new values.
+ CardTheme copyWith({
+ Clip clipBehavior,
+ Color color,
+ double elevation,
+ EdgeInsetsGeometry margin,
+ ShapeBorder shape,
+ }) {
+ return CardTheme(
+ clipBehavior: clipBehavior ?? this.clipBehavior,
+ color: color ?? this.color,
+ elevation: elevation ?? this.elevation,
+ margin: margin ?? this.margin,
+ shape: shape ?? this.shape,
+ );
+ }
+
+ /// The [ThemeData.cardTheme] property of the ambient [Theme].
+ static CardTheme of(BuildContext context) {
+ return Theme.of(context).cardTheme;
+ }
+
+ /// Linearly interpolate between two Card themes.
+ ///
+ /// The argument `t` must not be null.
+ ///
+ /// {@macro dart.ui.shadow.lerp}
+ static CardTheme lerp(CardTheme a, CardTheme b, double t) {
+ assert(t != null);
+ return CardTheme(
+ clipBehavior: t < 0.5 ? a?.clipBehavior : b?.clipBehavior,
+ color: Color.lerp(a?.color, b?.color, 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 {
+ return hashValues(
+ clipBehavior,
+ color,
+ elevation,
+ margin,
+ shape,
+ );
+ }
+
+ @override
+ bool operator ==(dynamic other) {
+ if (identical(this, other))
+ return true;
+ if (other.runtimeType != runtimeType)
+ return false;
+ final CardTheme typedOther = other;
+ return typedOther.clipBehavior == clipBehavior
+ && typedOther.color == color
+ && typedOther.elevation == elevation
+ && typedOther.margin == margin
+ && typedOther.shape == shape;
+ }
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties.add(DiagnosticsProperty<Clip>('clipBehavior', clipBehavior, defaultValue: null));
+ properties.add(DiagnosticsProperty<Color>('color', color, 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));
+ }
+}
diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart
index 7704a45..0a09e24 100644
--- a/packages/flutter/lib/src/material/theme_data.dart
+++ b/packages/flutter/lib/src/material/theme_data.dart
@@ -12,6 +12,7 @@
import 'app_bar_theme.dart';
import 'bottom_app_bar_theme.dart';
import 'button_theme.dart';
+import 'card_theme.dart';
import 'chip_theme.dart';
import 'color_scheme.dart';
import 'colors.dart';
@@ -149,6 +150,7 @@
IconThemeData accentIconTheme,
SliderThemeData sliderTheme,
TabBarTheme tabBarTheme,
+ CardTheme cardTheme,
ChipThemeData chipTheme,
TargetPlatform platform,
MaterialTapTargetSize materialTapTargetSize,
@@ -248,6 +250,7 @@
tabBarTheme ??= const TabBarTheme();
appBarTheme ??= const AppBarTheme();
bottomAppBarTheme ??= const BottomAppBarTheme();
+ cardTheme ??= const CardTheme();
chipTheme ??= ChipThemeData.fromDefaults(
secondaryColor: primaryColor,
brightness: brightness,
@@ -296,6 +299,7 @@
accentIconTheme: accentIconTheme,
sliderTheme: sliderTheme,
tabBarTheme: tabBarTheme,
+ cardTheme: cardTheme,
chipTheme: chipTheme,
platform: platform,
materialTapTargetSize: materialTapTargetSize,
@@ -359,6 +363,7 @@
@required this.accentIconTheme,
@required this.sliderTheme,
@required this.tabBarTheme,
+ @required this.cardTheme,
@required this.chipTheme,
@required this.platform,
@required this.materialTapTargetSize,
@@ -407,6 +412,7 @@
assert(accentIconTheme != null),
assert(sliderTheme != null),
assert(tabBarTheme != null),
+ assert(cardTheme != null),
assert(chipTheme != null),
assert(platform != null),
assert(materialTapTargetSize != null),
@@ -604,6 +610,11 @@
/// A theme for customizing the size, shape, and color of the tab bar indicator.
final TabBarTheme tabBarTheme;
+ /// The colors and styles used to render [Card].
+ ///
+ /// This is the value returned from [CardTheme.of].
+ final CardTheme cardTheme;
+
/// The colors and styles used to render [Chip], [
///
/// This is the value returned from [ChipTheme.of].
@@ -708,6 +719,7 @@
IconThemeData accentIconTheme,
SliderThemeData sliderTheme,
TabBarTheme tabBarTheme,
+ CardTheme cardTheme,
ChipThemeData chipTheme,
TargetPlatform platform,
MaterialTapTargetSize materialTapTargetSize,
@@ -760,6 +772,7 @@
accentIconTheme: accentIconTheme ?? this.accentIconTheme,
sliderTheme: sliderTheme ?? this.sliderTheme,
tabBarTheme: tabBarTheme ?? this.tabBarTheme,
+ cardTheme: cardTheme ?? this.cardTheme,
chipTheme: chipTheme ?? this.chipTheme,
platform: platform ?? this.platform,
materialTapTargetSize: materialTapTargetSize ?? this.materialTapTargetSize,
@@ -890,6 +903,7 @@
accentIconTheme: IconThemeData.lerp(a.accentIconTheme, b.accentIconTheme, t),
sliderTheme: SliderThemeData.lerp(a.sliderTheme, b.sliderTheme, t),
tabBarTheme: TabBarTheme.lerp(a.tabBarTheme, b.tabBarTheme, t),
+ cardTheme: CardTheme.lerp(a.cardTheme, b.cardTheme, t),
chipTheme: ChipThemeData.lerp(a.chipTheme, b.chipTheme, t),
platform: t < 0.5 ? a.platform : b.platform,
materialTapTargetSize: t < 0.5 ? a.materialTapTargetSize : b.materialTapTargetSize,
@@ -950,6 +964,7 @@
(otherData.accentIconTheme == accentIconTheme) &&
(otherData.sliderTheme == sliderTheme) &&
(otherData.tabBarTheme == tabBarTheme) &&
+ (otherData.cardTheme == cardTheme) &&
(otherData.chipTheme == chipTheme) &&
(otherData.platform == platform) &&
(otherData.materialTapTargetSize == materialTapTargetSize) &&
@@ -1010,6 +1025,7 @@
sliderTheme,
hashValues(
tabBarTheme,
+ cardTheme,
chipTheme,
platform,
materialTapTargetSize,
@@ -1066,6 +1082,7 @@
properties.add(DiagnosticsProperty<IconThemeData>('accentIconTheme', accentIconTheme));
properties.add(DiagnosticsProperty<SliderThemeData>('sliderTheme', sliderTheme));
properties.add(DiagnosticsProperty<TabBarTheme>('tabBarTheme', tabBarTheme));
+ properties.add(DiagnosticsProperty<CardTheme>('cardTheme', cardTheme));
properties.add(DiagnosticsProperty<ChipThemeData>('chipTheme', chipTheme));
properties.add(DiagnosticsProperty<MaterialTapTargetSize>('materialTapTargetSize', materialTapTargetSize));
properties.add(DiagnosticsProperty<PageTransitionsTheme>('pageTransitionsTheme', pageTransitionsTheme));
diff --git a/packages/flutter/test/material/card_theme_test.dart b/packages/flutter/test/material/card_theme_test.dart
new file mode 100644
index 0000000..fa943a9
--- /dev/null
+++ b/packages/flutter/test/material/card_theme_test.dart
@@ -0,0 +1,182 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io' show Platform;
+
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.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);
+ });
+
+ testWidgets('Passing no CardTheme returns defaults', (WidgetTester tester) async {
+ await tester.pumpWidget(const MaterialApp(
+ home: Scaffold(
+ body: Card()
+ ),
+ ));
+
+ final Container container = _getCardContainer(tester);
+ final Material material = _getCardMaterial(tester);
+
+ expect(material.clipBehavior, Clip.none);
+ expect(material.color, Colors.white);
+ expect(material.elevation, 1.0);
+ expect(container.margin, const EdgeInsets.all(4.0));
+ expect(material.shape, const RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(Radius.circular(4.0)),
+ ));
+ });
+
+ testWidgets('Card uses values from CardTheme', (WidgetTester tester) async {
+ final CardTheme cardTheme = _cardTheme();
+
+ await tester.pumpWidget(MaterialApp(
+ theme: ThemeData(cardTheme: cardTheme),
+ home: const Scaffold(
+ body: Card()
+ ),
+ ));
+
+ final Container container = _getCardContainer(tester);
+ final Material material = _getCardMaterial(tester);
+
+ expect(material.clipBehavior, cardTheme.clipBehavior);
+ expect(material.color, cardTheme.color);
+ expect(material.elevation, cardTheme.elevation);
+ expect(container.margin, cardTheme.margin);
+ expect(material.shape, cardTheme.shape);
+ });
+
+ testWidgets('Card widget properties take priority over theme', (WidgetTester tester) async {
+ const Clip clip = Clip.hardEdge;
+ const Color color = Colors.orange;
+ const double elevation = 7.0;
+ const EdgeInsets margin = EdgeInsets.all(3.0);
+ const ShapeBorder shape = RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(Radius.circular(9.0)),
+ );
+
+ await tester.pumpWidget(MaterialApp(
+ theme: _themeData().copyWith(cardTheme: _cardTheme()),
+ home: const Scaffold(
+ body: Card(
+ clipBehavior: clip,
+ color: color,
+ elevation: elevation,
+ margin: margin,
+ shape: shape,
+ )
+ ),
+ ));
+
+ final Container container = _getCardContainer(tester);
+ final Material material = _getCardMaterial(tester);
+
+ expect(material.clipBehavior, clip);
+ expect(material.color, color);
+ expect(material.elevation, elevation);
+ expect(container.margin, margin);
+ expect(material.shape, shape);
+ });
+
+ testWidgets('CardTheme properties take priority over ThemeData properties', (WidgetTester tester) async {
+ final CardTheme cardTheme = _cardTheme();
+ final ThemeData themeData = _themeData().copyWith(cardTheme: cardTheme);
+
+ await tester.pumpWidget(MaterialApp(
+ theme: themeData,
+ home: const Scaffold(
+ body: Card()
+ ),
+ ));
+
+ final Material material = _getCardMaterial(tester);
+ expect(material.color, cardTheme.color);
+ });
+
+ testWidgets('ThemeData properties are used when no CardTheme is set', (WidgetTester tester) async {
+ final ThemeData themeData = _themeData();
+
+ await tester.pumpWidget(MaterialApp(
+ theme: themeData,
+ home: const Scaffold(
+ body: Card()
+ ),
+ ));
+
+ final Material material = _getCardMaterial(tester);
+ expect(material.color, themeData.cardColor);
+ });
+
+ testWidgets('CardTheme customizes shape', (WidgetTester tester) async {
+ const CardTheme cardTheme = CardTheme(
+ color: Colors.white,
+ shape: BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(7))),
+ elevation: 1.0,
+ );
+
+ final Key painterKey = UniqueKey();
+
+ await tester.pumpWidget(MaterialApp(
+ theme: ThemeData(cardTheme: cardTheme),
+ home: Scaffold(
+ body: RepaintBoundary(
+ key: painterKey,
+ child: Center(
+ child: Card(
+ child: SizedBox.fromSize(size: const Size(200, 300),),
+ )
+ )
+ )
+ ),
+ ));
+
+ await expectLater(
+ find.byKey(painterKey),
+ matchesGoldenFile('card_theme.custom_shape.png'),
+ skip: !Platform.isLinux,
+ );
+ });
+}
+
+CardTheme _cardTheme() {
+ return const CardTheme(
+ clipBehavior: Clip.antiAlias,
+ color: Colors.green,
+ elevation: 6.0,
+ margin: EdgeInsets.all(7.0),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(Radius.circular(5.0)),
+ )
+ );
+}
+
+ThemeData _themeData() {
+ return ThemeData(
+ cardColor: Colors.pink,
+ );
+}
+
+Material _getCardMaterial(WidgetTester tester) {
+ return tester.widget<Material>(
+ find.descendant(
+ of: find.byType(Card),
+ matching: find.byType(Material),
+ ),
+ );
+}
+
+Container _getCardContainer(WidgetTester tester) {
+ return tester.widget<Container>(
+ find.descendant(
+ of: find.byType(Card),
+ matching: find.byType(Container),
+ ),
+ );
+}