Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit e8eac0d

Browse files
authored
Update ExpansionTile to support Material 3 & add an example (#119712)
1 parent da36bd6 commit e8eac0d

File tree

6 files changed

+228
-23
lines changed

6 files changed

+228
-23
lines changed

dev/tools/gen_defaults/bin/gen_defaults.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import 'package:gen_defaults/date_picker_template.dart';
3131
import 'package:gen_defaults/dialog_template.dart';
3232
import 'package:gen_defaults/divider_template.dart';
3333
import 'package:gen_defaults/drawer_template.dart';
34+
import 'package:gen_defaults/expansion_tile_template.dart';
3435
import 'package:gen_defaults/fab_template.dart';
3536
import 'package:gen_defaults/filter_chip_template.dart';
3637
import 'package:gen_defaults/icon_button_template.dart';
@@ -153,6 +154,7 @@ Future<void> main(List<String> args) async {
153154
DialogTemplate('Dialog', '$materialLib/dialog.dart', tokens).updateFile();
154155
DividerTemplate('Divider', '$materialLib/divider.dart', tokens).updateFile();
155156
DrawerTemplate('Drawer', '$materialLib/drawer.dart', tokens).updateFile();
157+
ExpansionTileTemplate('ExpansionTile', '$materialLib/expansion_tile.dart', tokens).updateFile();
156158
FABTemplate('FAB', '$materialLib/floating_action_button.dart', tokens).updateFile();
157159
FilterChipTemplate('ChoiceChip', '$materialLib/choice_chip.dart', tokens).updateFile();
158160
FilterChipTemplate('FilterChip', '$materialLib/filter_chip.dart', tokens).updateFile();
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'template.dart';
6+
7+
class ExpansionTileTemplate extends TokenTemplate {
8+
const ExpansionTileTemplate(super.blockName, super.fileName, super.tokens, {
9+
super.colorSchemePrefix = '_colors.',
10+
});
11+
12+
@override
13+
String generate() => '''
14+
class _${blockName}DefaultsM3 extends ExpansionTileThemeData {
15+
_${blockName}DefaultsM3(this.context);
16+
17+
final BuildContext context;
18+
late final ThemeData _theme = Theme.of(context);
19+
late final ColorScheme _colors = _theme.colorScheme;
20+
21+
@override
22+
Color? get textColor => ${textStyle("md.comp.list.list-item.label-text")}!.color;
23+
24+
@override
25+
Color? get iconColor => ${componentColor('md.comp.list.list-item.selected.trailing-icon')};
26+
27+
@override
28+
Color? get collapsedTextColor => ${textStyle("md.comp.list.list-item.label-text")}!.color;
29+
30+
@override
31+
Color? get collapsedIconColor => ${componentColor('md.comp.list.list-item.unselected.trailing-icon')};
32+
}
33+
''';
34+
}

examples/api/lib/material/expansion_tile/expansion_tile.0.dart

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,31 @@
66

77
import 'package:flutter/material.dart';
88

9-
void main() => runApp(const MyApp());
9+
void main() => runApp(const ExpansionTileApp());
1010

11-
class MyApp extends StatelessWidget {
12-
const MyApp({super.key});
13-
14-
static const String _title = 'Flutter Code Sample';
11+
class ExpansionTileApp extends StatelessWidget {
12+
const ExpansionTileApp({super.key});
1513

1614
@override
1715
Widget build(BuildContext context) {
1816
return MaterialApp(
19-
title: _title,
17+
theme: ThemeData(useMaterial3: true),
2018
home: Scaffold(
21-
appBar: AppBar(title: const Text(_title)),
22-
body: const MyStatefulWidget(),
19+
appBar: AppBar(title: const Text('ExpansionTile Sample')),
20+
body: const ExpansionTileExample(),
2321
),
2422
);
2523
}
2624
}
2725

28-
class MyStatefulWidget extends StatefulWidget {
29-
const MyStatefulWidget({super.key});
26+
class ExpansionTileExample extends StatefulWidget {
27+
const ExpansionTileExample({super.key});
3028

3129
@override
32-
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
30+
State<ExpansionTileExample> createState() => _ExpansionTileExampleState();
3331
}
3432

35-
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
33+
class _ExpansionTileExampleState extends State<ExpansionTileExample> {
3634
bool _customTileExpanded = false;
3735

3836
@override
@@ -58,7 +56,9 @@ class _MyStatefulWidgetState extends State<MyStatefulWidget> {
5856
ListTile(title: Text('This is tile number 2')),
5957
],
6058
onExpansionChanged: (bool expanded) {
61-
setState(() => _customTileExpanded = expanded);
59+
setState(() {
60+
_customTileExpanded = expanded;
61+
});
6262
},
6363
),
6464
const ExpansionTile(
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/material.dart';
6+
import 'package:flutter_api_samples/material/expansion_tile/expansion_tile.0.dart' as example;
7+
import 'package:flutter_test/flutter_test.dart';
8+
9+
void main() {
10+
testWidgets('When expansion tiles are expanded tile numbers are revealed', (WidgetTester tester) async {
11+
const int totalTiles = 3;
12+
13+
await tester.pumpWidget(
14+
const example.ExpansionTileApp(),
15+
);
16+
17+
expect(find.byType(ExpansionTile), findsNWidgets(totalTiles));
18+
19+
const String tileOne = 'This is tile number 1';
20+
expect(find.text(tileOne), findsNothing);
21+
22+
await tester.tap(find.text('ExpansionTile 1'));
23+
await tester.pumpAndSettle();
24+
expect(find.text(tileOne), findsOneWidget);
25+
26+
const String tileTwo = 'This is tile number 2';
27+
expect(find.text(tileTwo), findsNothing);
28+
29+
await tester.tap(find.text('ExpansionTile 2'));
30+
await tester.pumpAndSettle();
31+
expect(find.text(tileTwo), findsOneWidget);
32+
33+
const String tileThree = 'This is tile number 3';
34+
expect(find.text(tileThree), findsNothing);
35+
36+
await tester.tap(find.text('ExpansionTile 3'));
37+
await tester.pumpAndSettle();
38+
expect(find.text(tileThree), findsOneWidget);
39+
});
40+
}

packages/flutter/lib/src/material/expansion_tile.dart

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ const Duration _kExpand = Duration(milliseconds: 200);
3434
/// to the [leading] and [trailing] properties of [ExpansionTile].
3535
///
3636
/// {@tool dartpad}
37-
/// This example demonstrates different configurations of ExpansionTile.
37+
/// This example demonstrates how the [ExpansionTile] icon's location and appearance
38+
/// can be customized.
3839
///
3940
/// ** See code in examples/api/lib/material/expansion_tile/expansion_tile.0.dart **
4041
/// {@end-tool}
@@ -216,7 +217,7 @@ class ExpansionTile extends StatefulWidget {
216217
/// Used to override to the [ListTileThemeData.iconColor].
217218
///
218219
/// If this property is null then [ExpansionTileThemeData.iconColor] is used. If that
219-
/// is also null then the value of [ListTileThemeData.iconColor] is used.
220+
/// is also null then the value of [ColorScheme.primary] is used.
220221
///
221222
/// See also:
222223
///
@@ -227,6 +228,15 @@ class ExpansionTile extends StatefulWidget {
227228
/// The icon color of tile's expansion arrow icon when the sublist is collapsed.
228229
///
229230
/// Used to override to the [ListTileThemeData.iconColor].
231+
///
232+
/// If this property is null then [ExpansionTileThemeData.collapsedIconColor] is used. If that
233+
/// is also null and [ThemeData.useMaterial3] is true, [ColorScheme.onSurface] is used. Otherwise,
234+
/// defaults to [ThemeData.unselectedWidgetColor] color.
235+
///
236+
/// See also:
237+
///
238+
/// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
239+
/// [ExpansionTileThemeData].
230240
final Color? collapsedIconColor;
231241

232242

@@ -235,7 +245,8 @@ class ExpansionTile extends StatefulWidget {
235245
/// Used to override to the [ListTileThemeData.textColor].
236246
///
237247
/// If this property is null then [ExpansionTileThemeData.textColor] is used. If that
238-
/// is also null then the value of [ListTileThemeData.textColor] is used.
248+
/// is also null then and [ThemeData.useMaterial3] is true, color of the [TextTheme.bodyLarge]
249+
/// will be used for the [title] and [subtitle]. Otherwise, defaults to [ColorScheme.primary] color.
239250
///
240251
/// See also:
241252
///
@@ -247,8 +258,10 @@ class ExpansionTile extends StatefulWidget {
247258
///
248259
/// Used to override to the [ListTileThemeData.textColor].
249260
///
250-
/// If this property is null then [ExpansionTileThemeData.collapsedTextColor] is used. If that
251-
/// is also null then the value of [ListTileThemeData.textColor] is used.
261+
/// If this property is null then [ExpansionTileThemeData.collapsedTextColor] is used.
262+
/// If that is also null and [ThemeData.useMaterial3] is true, color of the
263+
/// [TextTheme.bodyLarge] will be used for the [title] and [subtitle]. Otherwise,
264+
/// defaults to color of the [TextTheme.titleMedium].
252265
///
253266
/// See also:
254267
///
@@ -441,7 +454,9 @@ class _ExpansionTileState extends State<ExpansionTile> with SingleTickerProvider
441454
void didChangeDependencies() {
442455
final ThemeData theme = Theme.of(context);
443456
final ExpansionTileThemeData expansionTileTheme = ExpansionTileTheme.of(context);
444-
final ColorScheme colorScheme = theme.colorScheme;
457+
final ExpansionTileThemeData defaults = theme.useMaterial3
458+
? _ExpansionTileDefaultsM3(context)
459+
: _ExpansionTileDefaultsM2(context);
445460
_borderTween
446461
..begin = widget.collapsedShape
447462
?? expansionTileTheme.collapsedShape
@@ -458,13 +473,13 @@ class _ExpansionTileState extends State<ExpansionTile> with SingleTickerProvider
458473
_headerColorTween
459474
..begin = widget.collapsedTextColor
460475
?? expansionTileTheme.collapsedTextColor
461-
?? theme.textTheme.titleMedium!.color
462-
..end = widget.textColor ?? expansionTileTheme.textColor ?? colorScheme.primary;
476+
?? defaults.collapsedTextColor
477+
..end = widget.textColor ?? expansionTileTheme.textColor ?? defaults.textColor;
463478
_iconColorTween
464479
..begin = widget.collapsedIconColor
465480
?? expansionTileTheme.collapsedIconColor
466-
?? theme.unselectedWidgetColor
467-
..end = widget.iconColor ?? expansionTileTheme.iconColor ?? colorScheme.primary;
481+
?? defaults.collapsedIconColor
482+
..end = widget.iconColor ?? expansionTileTheme.iconColor ?? defaults.iconColor;
468483
_backgroundColorTween
469484
..begin = widget.collapsedBackgroundColor ?? expansionTileTheme.collapsedBackgroundColor
470485
..end = widget.backgroundColor ?? expansionTileTheme.backgroundColor;
@@ -498,3 +513,54 @@ class _ExpansionTileState extends State<ExpansionTile> with SingleTickerProvider
498513
);
499514
}
500515
}
516+
517+
class _ExpansionTileDefaultsM2 extends ExpansionTileThemeData {
518+
_ExpansionTileDefaultsM2(this.context);
519+
520+
final BuildContext context;
521+
late final ThemeData _theme = Theme.of(context);
522+
late final ColorScheme _colorScheme = _theme.colorScheme;
523+
524+
@override
525+
Color? get textColor => _colorScheme.primary;
526+
527+
@override
528+
Color? get iconColor => _colorScheme.primary;
529+
530+
@override
531+
Color? get collapsedTextColor => _theme.textTheme.titleMedium!.color;
532+
533+
@override
534+
Color? get collapsedIconColor => _theme.unselectedWidgetColor;
535+
}
536+
537+
// BEGIN GENERATED TOKEN PROPERTIES - ExpansionTile
538+
539+
// Do not edit by hand. The code between the "BEGIN GENERATED" and
540+
// "END GENERATED" comments are generated from data in the Material
541+
// Design token database by the script:
542+
// dev/tools/gen_defaults/bin/gen_defaults.dart.
543+
544+
// Token database version: v0_152
545+
546+
class _ExpansionTileDefaultsM3 extends ExpansionTileThemeData {
547+
_ExpansionTileDefaultsM3(this.context);
548+
549+
final BuildContext context;
550+
late final ThemeData _theme = Theme.of(context);
551+
late final ColorScheme _colors = _theme.colorScheme;
552+
553+
@override
554+
Color? get textColor => Theme.of(context).textTheme.bodyLarge!.color;
555+
556+
@override
557+
Color? get iconColor => _colors.primary;
558+
559+
@override
560+
Color? get collapsedTextColor => Theme.of(context).textTheme.bodyLarge!.color;
561+
562+
@override
563+
Color? get collapsedIconColor => _colors.onSurface;
564+
}
565+
566+
// END GENERATED TOKEN PROPERTIES - ExpansionTile

packages/flutter/test/material/expansion_tile_test.dart

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,35 @@ void main() {
522522
expect(shapeDecoration.color, backgroundColor);
523523
});
524524

525+
testWidgets('ExpansionTile default iconColor, textColor', (WidgetTester tester) async {
526+
final ThemeData theme = ThemeData(useMaterial3: true);
527+
528+
await tester.pumpWidget(MaterialApp(
529+
theme: theme,
530+
home: const Material(
531+
child: ExpansionTile(
532+
title: TestText('title'),
533+
trailing: TestIcon(),
534+
children: <Widget>[
535+
SizedBox(height: 100, width: 100),
536+
],
537+
),
538+
),
539+
));
540+
541+
Color getIconColor() => tester.state<TestIconState>(find.byType(TestIcon)).iconTheme.color!;
542+
Color getTextColor() => tester.state<TestTextState>(find.byType(TestText)).textStyle.color!;
543+
544+
expect(getIconColor(), theme.colorScheme.onSurface);
545+
expect(getTextColor(), theme.textTheme.bodyLarge!.color);
546+
547+
await tester.tap(find.text('title'));
548+
await tester.pumpAndSettle();
549+
550+
expect(getIconColor(), theme.colorScheme.primary);
551+
expect(getTextColor(), theme.textTheme.bodyLarge!.color);
552+
});
553+
525554
testWidgets('ExpansionTile iconColor, textColor', (WidgetTester tester) async {
526555
// Regression test for https://github.com/flutter/flutter/pull/78281
527556

@@ -666,4 +695,38 @@ void main() {
666695
expect(listTile.leading.runtimeType, Icon);
667696
expect(listTile.trailing, isNull);
668697
});
698+
699+
group('Material 2', () {
700+
// Tests that are only relevant for Material 2. Once ThemeData.useMaterial3
701+
// is turned on by default, these tests can be removed.
702+
703+
testWidgets('ExpansionTile default iconColor, textColor', (WidgetTester tester) async {
704+
final ThemeData theme = ThemeData(useMaterial3: false);
705+
706+
await tester.pumpWidget(MaterialApp(
707+
theme: theme,
708+
home: const Material(
709+
child: ExpansionTile(
710+
title: TestText('title'),
711+
trailing: TestIcon(),
712+
children: <Widget>[
713+
SizedBox(height: 100, width: 100),
714+
],
715+
),
716+
),
717+
));
718+
719+
Color getIconColor() => tester.state<TestIconState>(find.byType(TestIcon)).iconTheme.color!;
720+
Color getTextColor() => tester.state<TestTextState>(find.byType(TestText)).textStyle.color!;
721+
722+
expect(getIconColor(), theme.unselectedWidgetColor);
723+
expect(getTextColor(), theme.textTheme.titleMedium!.color);
724+
725+
await tester.tap(find.text('title'));
726+
await tester.pumpAndSettle();
727+
728+
expect(getIconColor(), theme.colorScheme.primary);
729+
expect(getTextColor(), theme.colorScheme.primary);
730+
});
731+
});
669732
}

0 commit comments

Comments
 (0)