[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),
+    ),
+  );
+}