Unverified Commit 4a0f261b authored by Qun Cheng's avatar Qun Cheng Committed by GitHub

Add `Card.filled` and `Card.outlined` factory methods (#136229)

Fixes #119401

This PR is to:
* add `Card.filled` and `Card.outlined` factory methods so that we can use tokens for these two types of cards to generate default theme instead of providing hard-corded values in example.
* update card.2.dart example.
* add test file for card.2.dart example.
* fix some mismatch caused by editing the auto-generated defaults by hand in navigation_bar.dart and navigation_drawer.dart.
parent b47e4c4a
......@@ -261,7 +261,7 @@ class SampleChecker {
}
}
// These tests are known to be missing. They should all eventually be
// These tests are known to be missing. They should all eventually be
// implemented, but until they are we allow them, so that we can catch any new
// examples that are added without tests.
//
......@@ -282,7 +282,6 @@ final Set<String> _knownMissingTests = <String>{
'examples/api/test/material/text_field/text_field.1_test.dart',
'examples/api/test/material/button_style/button_style.0_test.dart',
'examples/api/test/material/range_slider/range_slider.0_test.dart',
'examples/api/test/material/card/card.2_test.dart',
'examples/api/test/material/card/card.0_test.dart',
'examples/api/test/material/selection_container/selection_container_disabled.0_test.dart',
'examples/api/test/material/selection_container/selection_container.0_test.dart',
......
......@@ -112,7 +112,9 @@ Future<void> main(List<String> args) async {
ButtonTemplate('md.comp.filled-tonal-button', 'FilledTonalButton', '$materialLib/filled_button.dart', tokens).updateFile();
ButtonTemplate('md.comp.outlined-button', 'OutlinedButton', '$materialLib/outlined_button.dart', tokens).updateFile();
ButtonTemplate('md.comp.text-button', 'TextButton', '$materialLib/text_button.dart', tokens).updateFile();
CardTemplate('Card', '$materialLib/card.dart', tokens).updateFile();
CardTemplate('md.comp.elevated-card', 'Card', '$materialLib/card.dart', tokens).updateFile();
CardTemplate('md.comp.filled-card', 'FilledCard', '$materialLib/card.dart', tokens).updateFile();
CardTemplate('md.comp.outlined-card', 'OutlinedCard', '$materialLib/card.dart', tokens).updateFile();
CheckboxTemplate('Checkbox', '$materialLib/checkbox.dart', tokens).updateFile();
ColorSchemeTemplate(colorLightTokens, colorDarkTokens, 'ColorScheme', '$materialLib/theme_data.dart', tokens).updateFile();
DatePickerTemplate('DatePicker', '$materialLib/date_picker_theme.dart', tokens).updateFile();
......
......@@ -203,6 +203,10 @@ md.comp.filled-button.label-text.text-style,
md.comp.filled-button.pressed.container.elevation,
md.comp.filled-button.pressed.state-layer.color,
md.comp.filled-button.pressed.state-layer.opacity,
md.comp.filled-card.container.color,
md.comp.filled-card.container.elevation,
md.comp.filled-card.container.shadow-color,
md.comp.filled-card.container.shape,
md.comp.filled-icon-button.container.color,
md.comp.filled-icon-button.container.shape,
md.comp.filled-icon-button.container.size,
......@@ -459,6 +463,13 @@ md.comp.outlined-button.outline.color,
md.comp.outlined-button.outline.width,
md.comp.outlined-button.pressed.state-layer.color,
md.comp.outlined-button.pressed.state-layer.opacity,
md.comp.outlined-card.container.color,
md.comp.outlined-card.container.elevation,
md.comp.outlined-card.container.shadow-color,
md.comp.outlined-card.container.shape,
md.comp.outlined-card.container.surface-tint-layer.color,
md.comp.outlined-card.outline.color,
md.comp.outlined-card.outline.width,
md.comp.outlined-icon-button.container.shape,
md.comp.outlined-icon-button.container.size,
md.comp.outlined-icon-button.disabled.icon.color,
......
......@@ -5,32 +5,49 @@
import 'template.dart';
class CardTemplate extends TokenTemplate {
const CardTemplate(super.blockName, super.fileName, super.tokens, {
const CardTemplate(this.tokenGroup, super.blockName, super.fileName, super.tokens, {
super.colorSchemePrefix = '_colors.',
});
final String tokenGroup;
String _shape() {
final String cardShape = shape('$tokenGroup.container');
if (tokenAvailable('$tokenGroup.outline.color')) {
return '''
$cardShape.copyWith(
side: ${border('$tokenGroup.outline')}
)''';
} else {
return cardShape;
}
}
@override
String generate() => '''
class _${blockName}DefaultsM3 extends CardTheme {
_${blockName}DefaultsM3(this.context)
: super(
clipBehavior: Clip.none,
elevation: ${elevation("md.comp.elevated-card.container")},
elevation: ${elevation('$tokenGroup.container')},
margin: const EdgeInsets.all(4.0),
shape: ${shape("md.comp.elevated-card.container")},
);
final BuildContext context;
late final ColorScheme _colors = Theme.of(context).colorScheme;
@override
Color? get color => ${componentColor("md.comp.elevated-card.container")};
Color? get color => ${componentColor('$tokenGroup.container')};
@override
Color? get shadowColor => ${colorOrTransparent('$tokenGroup.container.shadow-color')};
@override
Color? get shadowColor => ${colorOrTransparent("md.comp.elevated-card.container.shadow-color")};
Color? get surfaceTintColor => ${colorOrTransparent('$tokenGroup.container.surface-tint-layer.color')};
@override
Color? get surfaceTintColor => ${colorOrTransparent("md.comp.elevated-card.container.surface-tint-layer.color")};
ShapeBorder? get shape =>${_shape()};
}
''';
}
......@@ -34,9 +34,11 @@ class _${blockName}DefaultsM3 extends NavigationBarThemeData {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
return IconThemeData(
size: ${getToken("md.comp.navigation-bar.icon.size")},
color: states.contains(MaterialState.selected)
? ${componentColor("md.comp.navigation-bar.active.icon")}
: ${componentColor("md.comp.navigation-bar.inactive.icon")},
color: states.contains(MaterialState.disabled)
? _colors.onSurfaceVariant.withOpacity(0.38)
: states.contains(MaterialState.selected)
? ${componentColor("md.comp.navigation-bar.active.icon")}
: ${componentColor("md.comp.navigation-bar.inactive.icon")},
);
});
}
......@@ -47,9 +49,12 @@ class _${blockName}DefaultsM3 extends NavigationBarThemeData {
@override MaterialStateProperty<TextStyle?>? get labelTextStyle {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
final TextStyle style = ${textStyle("md.comp.navigation-bar.label-text")}!;
return style.apply(color: states.contains(MaterialState.selected)
? ${componentColor("md.comp.navigation-bar.active.label-text")}
: ${componentColor("md.comp.navigation-bar.inactive.label-text")}
return style.apply(
color: states.contains(MaterialState.disabled)
? _colors.onSurfaceVariant.withOpacity(0.38)
: states.contains(MaterialState.selected)
? ${componentColor("md.comp.navigation-bar.active.label-text")}
: ${componentColor("md.comp.navigation-bar.inactive.label-text")}
);
});
}
......
......@@ -42,7 +42,9 @@ class _${blockName}DefaultsM3 extends NavigationDrawerThemeData {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
return IconThemeData(
size: ${getToken("md.comp.navigation-drawer.icon.size")},
color: states.contains(MaterialState.selected)
color: states.contains(MaterialState.disabled)
? _colors.onSurfaceVariant.withOpacity(0.38)
: states.contains(MaterialState.selected)
? ${componentColor("md.comp.navigation-drawer.active.icon")}
: ${componentColor("md.comp.navigation-drawer.inactive.icon")},
);
......@@ -54,7 +56,9 @@ class _${blockName}DefaultsM3 extends NavigationDrawerThemeData {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
final TextStyle style = ${textStyle("md.comp.navigation-drawer.label-text")}!;
return style.apply(
color: states.contains(MaterialState.selected)
color: states.contains(MaterialState.disabled)
? _colors.onSurfaceVariant.withOpacity(0.38)
: states.contains(MaterialState.selected)
? ${componentColor("md.comp.navigation-drawer.active.label-text")}
: ${componentColor("md.comp.navigation-drawer.inactive.label-text")},
);
......
......@@ -16,97 +16,33 @@ class CardExamplesApp extends StatelessWidget {
@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: const Column(
children: <Widget>[
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({super.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({super.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')),
body: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Card(child: _SampleCard(cardName: 'Elevated Card')),
Card.filled(child: _SampleCard(cardName: 'Filled Card')),
Card.outlined(child: _SampleCard(cardName: 'Outlined 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({super.key});
class _SampleCard extends StatelessWidget {
const _SampleCard({required this.cardName});
final String cardName;
@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')),
),
),
return SizedBox(
width: 300,
height: 100,
child: Center(child: Text(cardName)),
);
}
}
// 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 'package:flutter/material.dart';
import 'package:flutter_api_samples/material/card/card.2.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Card variants', (WidgetTester tester) async {
await tester.pumpWidget(const example.CardExamplesApp());
expect(find.byType(Card), findsNWidgets(3));
expect(find.widgetWithText(Card, 'Elevated Card'), findsOneWidget);
expect(find.widgetWithText(Card, 'Filled Card'), findsOneWidget);
expect(find.widgetWithText(Card, 'Outlined Card'), findsOneWidget);
Material getCardMaterial(WidgetTester tester, int cardIndex) {
return tester.widget<Material>(
find.descendant(
of: find.byType(Card).at(cardIndex),
matching: find.byType(Material),
),
);
}
final Material defaultCard = getCardMaterial(tester, 0);
expect(defaultCard.clipBehavior, Clip.none);
expect(defaultCard.elevation, 1.0);
expect(defaultCard.shape, const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(12.0)),
));
expect(defaultCard.color, const Color(0xfffffbfe));
expect(defaultCard.shadowColor, const Color(0xff000000));
expect(defaultCard.surfaceTintColor, const Color(0xff6750a4));
final Material filledCard = getCardMaterial(tester, 1);
expect(filledCard.clipBehavior, Clip.none);
expect(filledCard.elevation, 0.0);
expect(filledCard.shape, const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(12.0)),
));
expect(filledCard.color, const Color(0xffe7e0ec));
expect(filledCard.shadowColor, const Color(0xff000000));
expect(filledCard.surfaceTintColor, const Color(0x00000000));
final Material outlinedCard = getCardMaterial(tester, 2);
expect(outlinedCard.clipBehavior, Clip.none);
expect(outlinedCard.elevation, 0.0);
expect(outlinedCard.shape, const RoundedRectangleBorder(
side: BorderSide(color: Color(0xffcac4d0)),
borderRadius: BorderRadius.all(Radius.circular(12.0)),
));
expect(outlinedCard.color, const Color(0xfffffbfe));
expect(outlinedCard.shadowColor, const Color(0xff000000));
expect(outlinedCard.surfaceTintColor, const Color(0xff6750a4));
});
}
......@@ -6,9 +6,12 @@ import 'package:flutter/widgets.dart';
import 'card_theme.dart';
import 'color_scheme.dart';
import 'colors.dart';
import 'material.dart';
import 'theme.dart';
enum _CardVariant { elevated, filled, outlined }
/// A Material Design card: a panel with slightly rounded corners and an
/// elevation shadow.
///
......@@ -39,9 +42,9 @@ 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.
/// Material Design 3 introduced new types of cards. The default [Card] is the
/// elevated card. To create a filled card, use [Card.filled]; to create a outlined
/// card, use [Card.outlined].
/// {@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
......@@ -71,7 +74,46 @@ class Card extends StatelessWidget {
this.clipBehavior,
this.child,
this.semanticContainer = true,
}) : assert(elevation == null || elevation >= 0.0);
}) : assert(elevation == null || elevation >= 0.0),
_variant = _CardVariant.elevated;
/// Create a filled variant of Card.
///
/// Filled cards provide subtle separation from the background. This has less
/// emphasis than elevated(default) or outlined cards.
const Card.filled({
super.key,
this.color,
this.shadowColor,
this.surfaceTintColor,
this.elevation,
this.shape,
this.borderOnForeground = true,
this.margin,
this.clipBehavior,
this.child,
this.semanticContainer = true,
}) : assert(elevation == null || elevation >= 0.0),
_variant = _CardVariant.filled;
/// Create an outlined variant of Card.
///
/// Outlined cards have a visual boundary around the container. This can
/// provide greater emphasis than the other types.
const Card.outlined({
super.key,
this.color,
this.shadowColor,
this.surfaceTintColor,
this.elevation,
this.shape,
this.borderOnForeground = true,
this.margin,
this.clipBehavior,
this.child,
this.semanticContainer = true,
}) : assert(elevation == null || elevation >= 0.0),
_variant = _CardVariant.outlined;
/// The card's background color.
///
......@@ -164,10 +206,24 @@ class Card extends StatelessWidget {
/// {@macro flutter.widgets.ProxyWidget.child}
final Widget? child;
final _CardVariant _variant;
@override
Widget build(BuildContext context) {
final CardTheme cardTheme = CardTheme.of(context);
final CardTheme defaults = Theme.of(context).useMaterial3 ? _CardDefaultsM3(context) : _CardDefaultsM2(context);
final CardTheme defaults;
if (Theme.of(context).useMaterial3) {
switch (_variant) {
case _CardVariant.elevated:
defaults = _CardDefaultsM3(context);
case _CardVariant.filled:
defaults = _FilledCardDefaultsM3(context);
case _CardVariant.outlined:
defaults = _OutlinedCardDefaultsM3(context);
}
} else {
defaults = _CardDefaultsM2(context);
}
return Semantics(
container: semanticContainer,
......@@ -226,7 +282,6 @@ class _CardDefaultsM3 extends CardTheme {
clipBehavior: Clip.none,
elevation: 1.0,
margin: const EdgeInsets.all(4.0),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12.0))),
);
final BuildContext context;
......@@ -240,6 +295,78 @@ class _CardDefaultsM3 extends CardTheme {
@override
Color? get surfaceTintColor => _colors.surfaceTint;
@override
ShapeBorder? get shape =>const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12.0)));
}
// END GENERATED TOKEN PROPERTIES - Card
// BEGIN GENERATED TOKEN PROPERTIES - FilledCard
// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
class _FilledCardDefaultsM3 extends CardTheme {
_FilledCardDefaultsM3(this.context)
: super(
clipBehavior: Clip.none,
elevation: 0.0,
margin: const EdgeInsets.all(4.0),
);
final BuildContext context;
late final ColorScheme _colors = Theme.of(context).colorScheme;
@override
Color? get color => _colors.surfaceVariant;
@override
Color? get shadowColor => _colors.shadow;
@override
Color? get surfaceTintColor => Colors.transparent;
@override
ShapeBorder? get shape =>const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12.0)));
}
// END GENERATED TOKEN PROPERTIES - FilledCard
// BEGIN GENERATED TOKEN PROPERTIES - OutlinedCard
// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
class _OutlinedCardDefaultsM3 extends CardTheme {
_OutlinedCardDefaultsM3(this.context)
: super(
clipBehavior: Clip.none,
elevation: 0.0,
margin: const EdgeInsets.all(4.0),
);
final BuildContext context;
late final ColorScheme _colors = Theme.of(context).colorScheme;
@override
Color? get color => _colors.surface;
@override
Color? get shadowColor => _colors.shadow;
@override
Color? get surfaceTintColor => _colors.surfaceTint;
@override
ShapeBorder? get shape =>
const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12.0))).copyWith(
side: BorderSide(color: _colors.outlineVariant)
);
}
// END GENERATED TOKEN PROPERTIES - OutlinedCard
......@@ -1364,9 +1364,10 @@ class _NavigationBarDefaultsM3 extends NavigationBarThemeData {
@override MaterialStateProperty<TextStyle?>? get labelTextStyle {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
final TextStyle style = _textTheme.labelMedium!;
return style.apply(color: states.contains(MaterialState.disabled)
? _colors.onSurfaceVariant.withOpacity(0.38)
: states.contains(MaterialState.selected)
return style.apply(
color: states.contains(MaterialState.disabled)
? _colors.onSurfaceVariant.withOpacity(0.38)
: states.contains(MaterialState.selected)
? _colors.onSurface
: _colors.onSurfaceVariant
);
......
......@@ -9,6 +9,79 @@ import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
import '../widgets/semantics_tester.dart';
void main() {
testWidgetsWithLeakTracking('Material3 - Card defaults (Elevated card)', (WidgetTester tester) async {
final ThemeData theme = ThemeData();
final ColorScheme colors = theme.colorScheme;
await tester.pumpWidget(MaterialApp(
theme: theme,
home: const Scaffold(
body: Card(),
),
));
final Container container = _getCardContainer(tester);
final Material material = _getCardMaterial(tester);
expect(material.clipBehavior, Clip.none);
expect(material.elevation, 1.0);
expect(container.margin, const EdgeInsets.all(4.0));
expect(material.color, colors.surface);
expect(material.shadowColor, colors.shadow);
expect(material.surfaceTintColor, colors.surfaceTint); // Default primary color
expect(material.shape, const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(12.0)),
));
});
testWidgetsWithLeakTracking('Material3 - Card.filled defaults', (WidgetTester tester) async {
final ThemeData theme = ThemeData();
final ColorScheme colors = theme.colorScheme;
await tester.pumpWidget(MaterialApp(
theme: theme,
home: const Scaffold(
body: Card.filled(),
),
));
final Container container = _getCardContainer(tester);
final Material material = _getCardMaterial(tester);
expect(material.clipBehavior, Clip.none);
expect(material.elevation, 0.0);
expect(container.margin, const EdgeInsets.all(4.0));
expect(material.color, colors.surfaceVariant);
expect(material.shadowColor, colors.shadow);
expect(material.surfaceTintColor, Colors.transparent);
expect(material.shape, const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(12.0)),
));
});
testWidgetsWithLeakTracking('Material3 - Card.outlined defaults', (WidgetTester tester) async {
final ThemeData theme = ThemeData();
final ColorScheme colors = theme.colorScheme;
await tester.pumpWidget(MaterialApp(
theme: theme,
home: const Scaffold(
body: Card.outlined(),
),
));
final Container container = _getCardContainer(tester);
final Material material = _getCardMaterial(tester);
expect(material.clipBehavior, Clip.none);
expect(material.elevation, 0.0);
expect(container.margin, const EdgeInsets.all(4.0));
expect(material.color, colors.surface);
expect(material.shadowColor, colors.shadow);
expect(material.surfaceTintColor, colors.surfaceTint);
expect(material.shape, RoundedRectangleBorder(
side: BorderSide(color: colors.outlineVariant),
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
));
});
testWidgetsWithLeakTracking('Card can take semantic text from multiple children', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
......@@ -219,3 +292,21 @@ void main() {
expect(getCardMaterial(tester).shadowColor, Colors.red);
});
}
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),
),
);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment