diff --git a/dev/tools/gen_defaults/bin/gen_defaults.dart b/dev/tools/gen_defaults/bin/gen_defaults.dart index 5c8f2043dc..caed5739ef 100644 --- a/dev/tools/gen_defaults/bin/gen_defaults.dart +++ b/dev/tools/gen_defaults/bin/gen_defaults.dart @@ -17,6 +17,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:gen_defaults/card_template.dart'; import 'package:gen_defaults/dialog_template.dart'; import 'package:gen_defaults/fab_template.dart'; import 'package:gen_defaults/navigation_bar_template.dart'; @@ -71,6 +72,7 @@ Future main(List args) async { tokens['colorsLight'] = _readTokenFile('color_light.json'); tokens['colorsDark'] = _readTokenFile('color_dark.json'); + CardTemplate('$materialLib/card.dart', tokens).updateFile(); DialogTemplate('$materialLib/dialog.dart', tokens).updateFile(); FABTemplate('$materialLib/floating_action_button.dart', tokens).updateFile(); NavigationBarTemplate('$materialLib/navigation_bar.dart', tokens).updateFile(); diff --git a/dev/tools/gen_defaults/lib/card_template.dart b/dev/tools/gen_defaults/lib/card_template.dart new file mode 100644 index 0000000000..09f2284d85 --- /dev/null +++ b/dev/tools/gen_defaults/lib/card_template.dart @@ -0,0 +1,34 @@ +// Copyright 2014 The Flutter 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 'template.dart'; + +class CardTemplate extends TokenTemplate { + const CardTemplate(String fileName, Map tokens) : super(fileName, tokens); + + @override + String generate() => ''' +// Generated version ${tokens["version"]} +class _TokenDefaultsM3 extends CardTheme { + const _TokenDefaultsM3(this.context) + : super( + clipBehavior: Clip.none, + elevation: ${elevation("md.comp.elevated-card.container")}, + margin: const EdgeInsets.all(4.0), + shape: ${shape("md.comp.elevated-card.container")}, + ); + + final BuildContext context; + + @override + Color? get color => Theme.of(context).colorScheme.${color("md.comp.elevated-card.container")}; + + @override + Color? get shadowColor => Theme.of(context).colorScheme.${tokens["md.comp.elevated-card.container.shadow-color"]}; + + @override + Color? get surfaceTintColor => Theme.of(context).colorScheme.${tokens["md.comp.elevated-card.container.surface-tint-layer.color"]}; +} +'''; +} diff --git a/examples/api/lib/material/card/card.2.dart b/examples/api/lib/material/card/card.2.dart new file mode 100644 index 0000000000..174d191705 --- /dev/null +++ b/examples/api/lib/material/card/card.2.dart @@ -0,0 +1,110 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Flutter code sample for Card + +import 'package:flutter/material.dart'; + +void main() { runApp(const CardExamplesApp()); } + +class CardExamplesApp extends StatelessWidget { + const CardExamplesApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + theme: ThemeData(colorSchemeSeed: const Color(0xff6750a4), useMaterial3: true), + home: Scaffold( + appBar: AppBar(title: const Text('Card Examples')), + body: Column( + children: const [ + Spacer(), + ElevatedCardExample(), + FilledCardExample(), + OutlinedCardExample(), + Spacer(), + ], + ), + ), + ); + } +} + +/// An example of the elevated card type. +/// +/// The default settings for [Card] will provide an elevated +/// card matching the spec: +/// +/// https://m3.material.io/components/cards/specs#a012d40d-7a5c-4b07-8740-491dec79d58b +class ElevatedCardExample extends StatelessWidget { + const ElevatedCardExample({ Key? key }) : super(key: key); + + @override + Widget build(BuildContext context) { + return const Center( + child: Card( + child: SizedBox( + width: 300, + height: 100, + child: Center(child: Text('Elevated Card')), + ), + ), + ); + } +} + +/// An example of the filled card type. +/// +/// To make a [Card] match the filled type, the default elevation and color +/// need to be changed to the values from the spec: +/// +/// https://m3.material.io/components/cards/specs#0f55bf62-edf2-4619-b00d-b9ed462f2c5a +class FilledCardExample extends StatelessWidget { + const FilledCardExample({ Key? key }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Center( + child: Card( + elevation: 0, + color: Theme.of(context).colorScheme.surfaceVariant, + child: const SizedBox( + width: 300, + height: 100, + child: Center(child: Text('Filled Card')), + ), + ), + ); + } +} + +/// An example of the outlined card type. +/// +/// To make a [Card] match the outlined type, the default elevation and shape +/// need to be changed to the values from the spec: +/// +/// https://m3.material.io/components/cards/specs#0f55bf62-edf2-4619-b00d-b9ed462f2c5a +class OutlinedCardExample extends StatelessWidget { + const OutlinedCardExample({ Key? key }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Center( + child: Card( + elevation: 0, + shape: RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).colorScheme.outline, + ), + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), + child: const SizedBox( + width: 300, + height: 100, + child: Center(child: Text('Outlined Card')), + ), + ), + ); + } +} diff --git a/packages/flutter/lib/src/material/card.dart b/packages/flutter/lib/src/material/card.dart index 81f2a3c31a..62afb9f775 100644 --- a/packages/flutter/lib/src/material/card.dart +++ b/packages/flutter/lib/src/material/card.dart @@ -38,6 +38,16 @@ import 'theme.dart'; /// ** See code in examples/api/lib/material/card/card.1.dart ** /// {@end-tool} /// +/// Material Design 3 introduced new types of cards. These can +/// be produced by configuring the [Card] widget's properties. +/// [Card] widget. +/// {@tool dartpad} +/// This sample shows creation of [Card] widgets for elevated, filled and +/// outlined types, as described in: https://m3.material.io/components/cards/overview +/// +/// ** See code in examples/api/lib/material/card/card.2.dart ** +/// {@end-tool} +/// /// See also: /// /// * [ListTile], to display icons and text in a card. @@ -52,6 +62,7 @@ class Card extends StatelessWidget { Key? key, this.color, this.shadowColor, + this.surfaceTintColor, this.elevation, this.shape, this.borderOnForeground = true, @@ -78,6 +89,18 @@ class Card extends StatelessWidget { /// (default black) is used. final Color? shadowColor; + /// The color used as an overlay on [color] to indicate elevation. + /// + /// If this is null, no overlay will be applied. Otherwise the this + /// color will be composited on top of [color] with an opacity related + /// to [elevation] and used to paint the background of the card. + /// + /// The default is null. + /// + /// See [Material.surfaceTintColor] for more details on how this + /// overlay is applied. + final Color? surfaceTintColor; + /// The z-coordinate at which to place this card. This controls the size of /// the shadow below the card. /// @@ -135,27 +158,24 @@ class Card extends StatelessWidget { /// {@macro flutter.widgets.ProxyWidget.child} final Widget? child; - static const double _defaultElevation = 1.0; - @override Widget build(BuildContext context) { - final ThemeData theme = Theme.of(context); final CardTheme cardTheme = CardTheme.of(context); + final CardTheme defaults = Theme.of(context).useMaterial3 ? _TokenDefaultsM3(context) : _DefaultsM2(context); return Semantics( container: semanticContainer, child: Container( - margin: margin ?? cardTheme.margin ?? const EdgeInsets.all(4.0), + margin: margin ?? cardTheme.margin ?? defaults.margin!, child: Material( type: MaterialType.card, - shadowColor: shadowColor ?? cardTheme.shadowColor ?? theme.shadowColor, - color: color ?? cardTheme.color ?? theme.cardColor, - elevation: elevation ?? cardTheme.elevation ?? _defaultElevation, - shape: shape ?? cardTheme.shape ?? const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(4.0)), - ), + color: color ?? cardTheme.color ?? defaults.color, + shadowColor: shadowColor ?? cardTheme.shadowColor ?? defaults.shadowColor, + surfaceTintColor: surfaceTintColor ?? cardTheme.surfaceTintColor ?? defaults.surfaceTintColor, + elevation: elevation ?? cardTheme.elevation ?? defaults.elevation!, + shape: shape ?? cardTheme.shape ?? defaults.shape, borderOnForeground: borderOnForeground, - clipBehavior: clipBehavior ?? cardTheme.clipBehavior ?? Clip.none, + clipBehavior: clipBehavior ?? cardTheme.clipBehavior ?? defaults.clipBehavior!, child: Semantics( explicitChildNodes: !semanticContainer, child: child, @@ -165,3 +185,53 @@ class Card extends StatelessWidget { ); } } + +class _DefaultsM2 extends CardTheme { + const _DefaultsM2(this.context) + : super( + clipBehavior: Clip.none, + elevation: 1.0, + margin: const EdgeInsets.all(4.0), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(4.0)), + ) + ); + + final BuildContext context; + + @override + Color? get color => Theme.of(context).cardColor; + + @override + Color? get shadowColor => Theme.of(context).shadowColor; +} + +// BEGIN GENERATED TOKEN PROPERTIES + +// Generated code to the end of this file. Do not edit by hand. +// These defaults are generated from the Material Design Token +// database by the script dev/tools/gen_defaults/bin/gen_defaults.dart. + +// Generated version v0_90 +class _TokenDefaultsM3 extends CardTheme { + const _TokenDefaultsM3(this.context) + : super( + clipBehavior: Clip.none, + elevation: 1.0, + margin: const EdgeInsets.all(4.0), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.only(topLeft: Radius.circular(12.0), topRight: Radius.circular(12.0), bottomLeft: Radius.circular(12.0), bottomRight: Radius.circular(12.0))), + ); + + final BuildContext context; + + @override + Color? get color => Theme.of(context).colorScheme.surface; + + @override + Color? get shadowColor => Theme.of(context).colorScheme.shadow; + + @override + Color? get surfaceTintColor => Theme.of(context).colorScheme.surfaceTint; +} + +// END GENERATED TOKEN PROPERTIES diff --git a/packages/flutter/lib/src/material/card_theme.dart b/packages/flutter/lib/src/material/card_theme.dart index 04cc23f0e5..8862a8e5f5 100644 --- a/packages/flutter/lib/src/material/card_theme.dart +++ b/packages/flutter/lib/src/material/card_theme.dart @@ -36,6 +36,7 @@ class CardTheme with Diagnosticable { this.clipBehavior, this.color, this.shadowColor, + this.surfaceTintColor, this.elevation, this.margin, this.shape, @@ -56,6 +57,13 @@ class CardTheme with Diagnosticable { /// If null, [Card] defaults to fully opaque black. final Color? shadowColor; + /// Default value for [Card.surfaceTintColor]. + /// + /// If null, [Card] will not display an overlay color. + /// + /// See [Material.surfaceTintColor] for more details. + final Color? surfaceTintColor; + /// Default value for [Card.elevation]. /// /// If null, [Card] uses a default of 1.0. @@ -79,6 +87,7 @@ class CardTheme with Diagnosticable { Clip? clipBehavior, Color? color, Color? shadowColor, + Color? surfaceTintColor, double? elevation, EdgeInsetsGeometry? margin, ShapeBorder? shape, @@ -87,6 +96,7 @@ class CardTheme with Diagnosticable { 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, @@ -109,6 +119,7 @@ class CardTheme with Diagnosticable { 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), @@ -120,6 +131,7 @@ class CardTheme with Diagnosticable { clipBehavior, color, shadowColor, + surfaceTintColor, elevation, margin, shape, @@ -135,6 +147,7 @@ class CardTheme with Diagnosticable { && other.clipBehavior == clipBehavior && other.color == color && other.shadowColor == shadowColor + && other.surfaceTintColor == surfaceTintColor && other.elevation == elevation && other.margin == margin && other.shape == shape; @@ -146,6 +159,7 @@ class CardTheme with Diagnosticable { 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)); diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart index e75f57f250..2cbf5aa906 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -1249,6 +1249,7 @@ class ThemeData with Diagnosticable { /// Components that have been migrated to Material 3 are: /// /// * [AlertDialog] + /// * [Card] /// * [Dialog] /// * [FloatingActionButton] /// * [Material] diff --git a/packages/flutter/test/material/card_theme_test.dart b/packages/flutter/test/material/card_theme_test.dart index 9f29099340..4103884d17 100644 --- a/packages/flutter/test/material/card_theme_test.dart +++ b/packages/flutter/test/material/card_theme_test.dart @@ -16,8 +16,9 @@ void main() { }); testWidgets('Passing no CardTheme returns defaults', (WidgetTester tester) async { - await tester.pumpWidget(const MaterialApp( - home: Scaffold( + await tester.pumpWidget(MaterialApp( + theme: ThemeData(useMaterial3: true), + home: const Scaffold( body: Card(), ), )); @@ -27,10 +28,12 @@ void main() { expect(material.clipBehavior, Clip.none); expect(material.color, Colors.white); + expect(material.shadowColor, Colors.black); + expect(material.surfaceTintColor, Colors.blue); // Default primary color 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)), + borderRadius: BorderRadius.all(Radius.circular(12.0)), )); }); @@ -50,6 +53,7 @@ void main() { expect(material.clipBehavior, cardTheme.clipBehavior); expect(material.color, cardTheme.color); expect(material.shadowColor, cardTheme.shadowColor); + expect(material.surfaceTintColor, cardTheme.surfaceTintColor); expect(material.elevation, cardTheme.elevation); expect(container.margin, cardTheme.margin); expect(material.shape, cardTheme.shape); @@ -129,7 +133,7 @@ void main() { final Key painterKey = UniqueKey(); await tester.pumpWidget(MaterialApp( - theme: ThemeData(cardTheme: cardTheme), + theme: ThemeData(cardTheme: cardTheme, useMaterial3: true), home: Scaffold( body: RepaintBoundary( key: painterKey, @@ -147,6 +151,62 @@ void main() { matchesGoldenFile('card_theme.custom_shape.png'), ); }); + + group('Material 2', () { + // Tests that are only relevant for Material 2. Once ThemeData.useMaterial3 + // is turned on by default, these tests can be removed. + + testWidgets('Passing no CardTheme returns defaults - M2', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + theme: ThemeData(useMaterial3: false), + home: const 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.shadowColor, Colors.black); + expect(material.surfaceTintColor, null); + 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('CardTheme customizes shape - M2', (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, useMaterial3: false), + 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_m2.png'), + ); + }); + }); } CardTheme _cardTheme() { @@ -154,6 +214,7 @@ CardTheme _cardTheme() { clipBehavior: Clip.antiAlias, color: Colors.green, shadowColor: Colors.red, + surfaceTintColor: Colors.purple, elevation: 6.0, margin: EdgeInsets.all(7.0), shape: RoundedRectangleBorder(