Add support for Material 3 `Divider` and `VerticalDivider` (#112378)
diff --git a/dev/tools/gen_defaults/bin/gen_defaults.dart b/dev/tools/gen_defaults/bin/gen_defaults.dart
index ef1f32a..00c6cd7 100644
--- a/dev/tools/gen_defaults/bin/gen_defaults.dart
+++ b/dev/tools/gen_defaults/bin/gen_defaults.dart
@@ -24,6 +24,7 @@
import 'package:gen_defaults/card_template.dart';
import 'package:gen_defaults/checkbox_template.dart';
import 'package:gen_defaults/dialog_template.dart';
+import 'package:gen_defaults/divider_template.dart';
import 'package:gen_defaults/fab_template.dart';
import 'package:gen_defaults/filter_chip_template.dart';
import 'package:gen_defaults/icon_button_template.dart';
@@ -65,6 +66,7 @@
'date_picker_docked.json',
'date_picker_modal.json',
'dialog.json',
+ 'divider.json',
'dialog_fullscreen.json',
'elevation.json',
'fab_extended_primary.json',
@@ -121,6 +123,7 @@
CardTemplate('Card', '$materialLib/card.dart', tokens).updateFile();
CheckboxTemplate('Checkbox', '$materialLib/checkbox.dart', tokens).updateFile();
DialogTemplate('Dialog', '$materialLib/dialog.dart', tokens).updateFile();
+ DividerTemplate('Divider', '$materialLib/divider.dart', tokens).updateFile();
FABTemplate('FAB', '$materialLib/floating_action_button.dart', tokens).updateFile();
FilterChipTemplate('ChoiceChip', '$materialLib/choice_chip.dart', tokens).updateFile();
FilterChipTemplate('FilterChip', '$materialLib/filter_chip.dart', tokens).updateFile();
diff --git a/dev/tools/gen_defaults/lib/divider_template.dart b/dev/tools/gen_defaults/lib/divider_template.dart
new file mode 100644
index 0000000..2028dcb
--- /dev/null
+++ b/dev/tools/gen_defaults/lib/divider_template.dart
@@ -0,0 +1,24 @@
+// 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 DividerTemplate extends TokenTemplate {
+ const DividerTemplate(super.blockName, super.fileName, super.tokens, {
+ super.colorSchemePrefix = '_colors.',
+ super.textThemePrefix = '_textTheme.'
+ });
+
+ @override
+ String generate() => '''
+class _${blockName}DefaultsM3 extends DividerThemeData {
+ _${blockName}DefaultsM3(this.context) : super(thickness: ${tokens["md.comp.divider.thickness"]});
+
+ final BuildContext context;
+ late final ColorScheme _colors = Theme.of(context).colorScheme;
+
+ @override Color? get color => ${componentColor("md.comp.divider")};
+}
+''';
+}
diff --git a/examples/api/lib/material/divider/divider.0.dart b/examples/api/lib/material/divider/divider.0.dart
index 5912c1f..6672379 100644
--- a/examples/api/lib/material/divider/divider.0.dart
+++ b/examples/api/lib/material/divider/divider.0.dart
@@ -11,22 +11,19 @@
class MyApp extends StatelessWidget {
const MyApp({super.key});
- static const String _title = 'Flutter Code Sample';
-
@override
Widget build(BuildContext context) {
return MaterialApp(
- title: _title,
home: Scaffold(
- appBar: AppBar(title: const Text(_title)),
- body: const MyStatelessWidget(),
+ appBar: AppBar(title: const Text('Divider Sample')),
+ body: const DividerExample(),
),
);
}
}
-class MyStatelessWidget extends StatelessWidget {
- const MyStatelessWidget({super.key});
+class DividerExample extends StatelessWidget {
+ const DividerExample({super.key});
@override
Widget build(BuildContext context) {
diff --git a/examples/api/lib/material/divider/divider.1.dart b/examples/api/lib/material/divider/divider.1.dart
new file mode 100644
index 0000000..708e642
--- /dev/null
+++ b/examples/api/lib/material/divider/divider.1.dart
@@ -0,0 +1,52 @@
+// 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 [Divider].
+
+import 'package:flutter/material.dart';
+
+void main() => runApp(const MyApp());
+
+class MyApp extends StatelessWidget {
+ const MyApp({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ theme: ThemeData(colorSchemeSeed: const Color(0xff6750a4), useMaterial3: true),
+ home: Scaffold(
+ appBar: AppBar(title: const Text('Divider Sample')),
+ body: const DividerExample(),
+ ),
+ );
+ }
+}
+
+class DividerExample extends StatelessWidget {
+ const DividerExample({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Center(
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Column(
+ children: const <Widget>[
+ Expanded(
+ child: Card(
+ child: SizedBox.expand(),
+ ),
+ ),
+ Divider(),
+ Expanded(
+ child: Card(
+ child: SizedBox.expand(),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/examples/api/lib/material/divider/vertical_divider.0.dart b/examples/api/lib/material/divider/vertical_divider.0.dart
index 0a9a5cb..2bb5ad1 100644
--- a/examples/api/lib/material/divider/vertical_divider.0.dart
+++ b/examples/api/lib/material/divider/vertical_divider.0.dart
@@ -11,22 +11,19 @@
class MyApp extends StatelessWidget {
const MyApp({super.key});
- static const String _title = 'Flutter Code Sample';
-
@override
Widget build(BuildContext context) {
return MaterialApp(
- title: _title,
home: Scaffold(
- appBar: AppBar(title: const Text(_title)),
- body: const MyStatelessWidget(),
+ appBar: AppBar(title: const Text('VerticalDivider Sample')),
+ body: const DividerExample(),
),
);
}
}
-class MyStatelessWidget extends StatelessWidget {
- const MyStatelessWidget({super.key});
+class DividerExample extends StatelessWidget {
+ const DividerExample({super.key});
@override
Widget build(BuildContext context) {
diff --git a/examples/api/lib/material/divider/vertical_divider.1.dart b/examples/api/lib/material/divider/vertical_divider.1.dart
new file mode 100644
index 0000000..47a7b54
--- /dev/null
+++ b/examples/api/lib/material/divider/vertical_divider.1.dart
@@ -0,0 +1,52 @@
+// 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 [Divider].
+
+import 'package:flutter/material.dart';
+
+void main() => runApp(const MyApp());
+
+class MyApp extends StatelessWidget {
+ const MyApp({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ theme: ThemeData(colorSchemeSeed: const Color(0xff6750a4), useMaterial3: true),
+ home: Scaffold(
+ appBar: AppBar(title: const Text('Divider Sample')),
+ body: const DividerExample(),
+ ),
+ );
+ }
+}
+
+class DividerExample extends StatelessWidget {
+ const DividerExample({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Center(
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Row(
+ children: const <Widget>[
+ Expanded(
+ child: Card(
+ child: SizedBox.expand(),
+ ),
+ ),
+ VerticalDivider(),
+ Expanded(
+ child: Card(
+ child: SizedBox.expand(),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/examples/api/test/material/divider/divider.0_test.dart b/examples/api/test/material/divider/divider.0_test.dart
new file mode 100644
index 0000000..825f0cb
--- /dev/null
+++ b/examples/api/test/material/divider/divider.0_test.dart
@@ -0,0 +1,28 @@
+// 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/divider/divider.0.dart' as example;
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ testWidgets('Horizontal Divider', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ const MaterialApp(
+ home: Scaffold(
+ body: example.MyApp(),
+ ),
+ ),
+ );
+
+ expect(find.byType(Divider), findsOneWidget);
+
+ // Divider is positioned horizintally.
+ final Offset container = tester.getBottomLeft(find.byType(Container).first);
+ expect(container.dy, tester.getTopLeft(find.byType(Divider)).dy);
+
+ final Offset subheader = tester.getTopLeft(find.text('Subheader'));
+ expect(subheader.dy, tester.getBottomLeft(find.byType(Divider)).dy);
+ });
+}
diff --git a/examples/api/test/material/divider/divider.1_test.dart b/examples/api/test/material/divider/divider.1_test.dart
new file mode 100644
index 0000000..a0cedbb
--- /dev/null
+++ b/examples/api/test/material/divider/divider.1_test.dart
@@ -0,0 +1,28 @@
+// 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/divider/divider.1.dart' as example;
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ testWidgets('Horizontal Divider', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ const MaterialApp(
+ home: Scaffold(
+ body: example.MyApp(),
+ ),
+ ),
+ );
+
+ expect(find.byType(Divider), findsOneWidget);
+
+ // Divider is positioned horizontally.
+ Offset card = tester.getBottomLeft(find.byType(Card).first);
+ expect(card.dy, tester.getTopLeft(find.byType(Divider)).dy);
+
+ card = tester.getTopLeft(find.byType(Card).last);
+ expect(card.dy, tester.getBottomLeft(find.byType(Divider)).dy);
+ });
+}
diff --git a/examples/api/test/material/divider/vertical_divider.0_test.dart b/examples/api/test/material/divider/vertical_divider.0_test.dart
new file mode 100644
index 0000000..63c7513
--- /dev/null
+++ b/examples/api/test/material/divider/vertical_divider.0_test.dart
@@ -0,0 +1,28 @@
+// 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/divider/vertical_divider.0.dart' as example;
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ testWidgets('Vertical Divider', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ const MaterialApp(
+ home: Scaffold(
+ body: example.MyApp(),
+ ),
+ ),
+ );
+
+ expect(find.byType(VerticalDivider), findsOneWidget);
+
+ // Divider is positioned horizintally.
+ Offset expanded = tester.getTopRight(find.byType(Expanded).first);
+ expect(expanded.dx, tester.getTopLeft(find.byType(VerticalDivider)).dx);
+
+ expanded = tester.getTopLeft(find.byType(Expanded).last);
+ expect(expanded.dx, tester.getTopRight(find.byType(VerticalDivider)).dx);
+ });
+}
diff --git a/examples/api/test/material/divider/vertical_divider.1_test.dart b/examples/api/test/material/divider/vertical_divider.1_test.dart
new file mode 100644
index 0000000..8840fee
--- /dev/null
+++ b/examples/api/test/material/divider/vertical_divider.1_test.dart
@@ -0,0 +1,28 @@
+// 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/divider/vertical_divider.1.dart' as example;
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ testWidgets('Vertical Divider', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ const MaterialApp(
+ home: Scaffold(
+ body: example.MyApp(),
+ ),
+ ),
+ );
+
+ expect(find.byType(VerticalDivider), findsOneWidget);
+
+ // Divider is positioned vertically.
+ Offset card = tester.getTopRight(find.byType(Card).first);
+ expect(card.dx, tester.getTopLeft(find.byType(VerticalDivider)).dx);
+
+ card = tester.getTopLeft(find.byType(Card).last);
+ expect(card.dx, tester.getTopRight(find.byType(VerticalDivider)).dx);
+ });
+}
diff --git a/packages/flutter/lib/src/material/divider.dart b/packages/flutter/lib/src/material/divider.dart
index 44b3f66..c3f30f4 100644
--- a/packages/flutter/lib/src/material/divider.dart
+++ b/packages/flutter/lib/src/material/divider.dart
@@ -4,6 +4,7 @@
import 'package:flutter/widgets.dart';
+import 'color_scheme.dart';
import 'divider_theme.dart';
import 'theme.dart';
@@ -34,6 +35,13 @@
/// ** See code in examples/api/lib/material/divider/divider.0.dart **
/// {@end-tool}
///
+/// {@tool dartpad}
+/// This sample shows the creation of [Divider] widget, as described in:
+/// https://m3.material.io/components/divider/overview
+///
+/// ** See code in examples/api/lib/material/divider/divider.1.dart **
+/// {@end-tool}
+///
/// See also:
///
/// * [PopupMenuDivider], which is the equivalent but for popup menus.
@@ -154,11 +162,14 @@
@override
Widget build(BuildContext context) {
+ final ThemeData theme = Theme.of(context);
final DividerThemeData dividerTheme = DividerTheme.of(context);
+ final DividerThemeData defaults = theme.useMaterial3 ? _DividerDefaultsM3(context) : const _DividerDefaultsM2();
final double height = this.height ?? dividerTheme.space ?? 16.0;
- final double thickness = this.thickness ?? dividerTheme.thickness ?? 0.0;
+ final double thickness = this.thickness ?? dividerTheme.thickness ?? defaults.thickness!;
final double indent = this.indent ?? dividerTheme.indent ?? 0.0;
final double endIndent = this.endIndent ?? dividerTheme.endIndent ?? 0.0;
+ final Color? color = this.color ?? defaults.color;
return SizedBox(
height: height,
@@ -195,6 +206,13 @@
/// ** See code in examples/api/lib/material/divider/vertical_divider.0.dart **
/// {@end-tool}
///
+/// {@tool dartpad}
+/// This sample shows the creation of [VerticalDivider] widget, as described in:
+/// https://m3.material.io/components/divider/overview
+///
+/// ** See code in examples/api/lib/material/divider/vertical_divider.1.dart **
+/// {@end-tool}
+///
/// See also:
///
/// * [ListView.separated], which can be used to generate vertical dividers.
@@ -264,11 +282,14 @@
@override
Widget build(BuildContext context) {
+ final ThemeData theme = Theme.of(context);
final DividerThemeData dividerTheme = DividerTheme.of(context);
+ final DividerThemeData defaults = theme.useMaterial3 ? _DividerDefaultsM3(context) : const _DividerDefaultsM2();
final double width = this.width ?? dividerTheme.space ?? 16.0;
- final double thickness = this.thickness ?? dividerTheme.thickness ?? 0.0;
+ final double thickness = this.thickness ?? dividerTheme.thickness ?? defaults.thickness!;
final double indent = this.indent ?? dividerTheme.indent ?? 0.0;
final double endIndent = this.endIndent ?? dividerTheme.endIndent ?? 0.0;
+ final Color? color = this.color ?? defaults.color;
return SizedBox(
width: width,
@@ -286,3 +307,27 @@
);
}
}
+
+class _DividerDefaultsM2 extends DividerThemeData {
+ const _DividerDefaultsM2() : super(thickness: 0.0);
+}
+
+// BEGIN GENERATED TOKEN PROPERTIES - Divider
+
+// 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.
+
+// Token database version: v0_132
+
+class _DividerDefaultsM3 extends DividerThemeData {
+ _DividerDefaultsM3(this.context) : super(thickness: 1.0);
+
+ final BuildContext context;
+ late final ColorScheme _colors = Theme.of(context).colorScheme;
+
+ @override Color? get color => _colors.outlineVariant;
+}
+
+// END GENERATED TOKEN PROPERTIES - Divider
diff --git a/packages/flutter/test/material/divider_theme_test.dart b/packages/flutter/test/material/divider_theme_test.dart
index 4d34e28..909c9e4 100644
--- a/packages/flutter/test/material/divider_theme_test.dart
+++ b/packages/flutter/test/material/divider_theme_test.dart
@@ -59,8 +59,9 @@
group('Horizontal Divider', () {
testWidgets('Passing no DividerThemeData returns defaults', (WidgetTester tester) async {
- await tester.pumpWidget(const MaterialApp(
- home: Scaffold(
+ await tester.pumpWidget( MaterialApp(
+ theme: ThemeData(useMaterial3: true),
+ home: const Scaffold(
body: Divider(),
),
));
@@ -70,10 +71,10 @@
final Container container = tester.widget(find.byType(Container));
final BoxDecoration decoration = container.decoration! as BoxDecoration;
- expect(decoration.border!.bottom.width, 0.0);
+ expect(decoration.border!.bottom.width, 1.0);
final ThemeData theme = ThemeData();
- expect(decoration.border!.bottom.color, theme.dividerColor);
+ expect(decoration.border!.bottom.color, theme.colorScheme.outlineVariant);
final Rect dividerRect = tester.getRect(find.byType(Divider));
final Rect lineRect = tester.getRect(find.byType(DecoratedBox));
@@ -142,8 +143,9 @@
group('Vertical Divider', () {
testWidgets('Passing no DividerThemeData returns defaults', (WidgetTester tester) async {
- await tester.pumpWidget(const MaterialApp(
- home: Scaffold(
+ await tester.pumpWidget(MaterialApp(
+ theme: ThemeData(useMaterial3: true),
+ home: const Scaffold(
body: VerticalDivider(),
),
));
@@ -154,10 +156,10 @@
final Container container = tester.widget(find.byType(Container));
final BoxDecoration decoration = container.decoration! as BoxDecoration;
final Border border = decoration.border! as Border;
- expect(border.left.width, 0.0);
+ expect(border.left.width, 1.0);
final ThemeData theme = ThemeData();
- expect(border.left.color, theme.dividerColor);
+ expect(border.left.color, theme.colorScheme.outlineVariant);
final Rect dividerRect = tester.getRect(find.byType(VerticalDivider));
final Rect lineRect = tester.getRect(find.byType(DecoratedBox));
@@ -225,6 +227,62 @@
expect(lineRect.bottom, dividerRect.bottom - endIndent);
});
});
+
+ group('Material 2', () {
+ // Tests that are only relevant for Material 2. Once ThemeData.useMaterial3
+ // is turned on by default, these tests can be removed.
+
+ group('Horizontal Divider', () {
+ testWidgets('Passing no DividerThemeData returns defaults', (WidgetTester tester) async {
+ await tester.pumpWidget(const MaterialApp(
+ home: Scaffold(
+ body: Divider(),
+ ),
+ ));
+
+ final RenderBox box = tester.firstRenderObject(find.byType(Divider));
+ expect(box.size.height, 16.0);
+
+ final Container container = tester.widget(find.byType(Container));
+ final BoxDecoration decoration = container.decoration! as BoxDecoration;
+ expect(decoration.border!.bottom.width, 0.0);
+
+ final ThemeData theme = ThemeData();
+ expect(decoration.border!.bottom.color, theme.dividerColor);
+
+ final Rect dividerRect = tester.getRect(find.byType(Divider));
+ final Rect lineRect = tester.getRect(find.byType(DecoratedBox));
+ expect(lineRect.left, dividerRect.left);
+ expect(lineRect.right, dividerRect.right);
+ });
+ });
+
+ group('Vertical Divider', () {
+ testWidgets('Passing no DividerThemeData returns defaults', (WidgetTester tester) async {
+ await tester.pumpWidget(const MaterialApp(
+ home: Scaffold(
+ body: VerticalDivider(),
+ ),
+ ));
+
+ final RenderBox box = tester.firstRenderObject(find.byType(VerticalDivider));
+ expect(box.size.width, 16.0);
+
+ final Container container = tester.widget(find.byType(Container));
+ final BoxDecoration decoration = container.decoration! as BoxDecoration;
+ final Border border = decoration.border! as Border;
+ expect(border.left.width, 0.0);
+
+ final ThemeData theme = ThemeData();
+ expect(border.left.color, theme.dividerColor);
+
+ final Rect dividerRect = tester.getRect(find.byType(VerticalDivider));
+ final Rect lineRect = tester.getRect(find.byType(DecoratedBox));
+ expect(lineRect.top, dividerRect.top);
+ expect(lineRect.bottom, dividerRect.bottom);
+ });
+ });
+ });
}
DividerThemeData _dividerTheme() {