Skip to content

Commit 219ff64

Browse files
authored
Reland "Update ExpansionTile to support Material 3 & add an example" (#121212)
1 parent cab761d commit 219ff64

File tree

6 files changed

+230
-25
lines changed

6 files changed

+230
-25
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 => ${componentColor('md.comp.list.list-item.label-text')};
23+
24+
@override
25+
Color? get iconColor => ${componentColor('md.comp.list.list-item.selected.trailing-icon')};
26+
27+
@override
28+
Color? get collapsedTextColor => ${componentColor('md.comp.list.list-item.label-text')};
29+
30+
@override
31+
Color? get collapsedIconColor => ${componentColor('md.comp.list.list-item.trailing-icon')};
32+
}
33+
''';
34+
}

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

Lines changed: 15 additions & 15 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
@@ -51,14 +49,16 @@ class _MyStatefulWidgetState extends State<MyStatefulWidget> {
5149
subtitle: const Text('Custom expansion arrow icon'),
5250
trailing: Icon(
5351
_customTileExpanded
54-
? Icons.arrow_drop_down_circle
55-
: Icons.arrow_drop_down,
52+
? Icons.arrow_drop_down_circle
53+
: Icons.arrow_drop_down,
5654
),
5755
children: const <Widget>[
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}
@@ -218,7 +219,7 @@ class ExpansionTile extends StatefulWidget {
218219
/// Used to override to the [ListTileThemeData.iconColor].
219220
///
220221
/// If this property is null then [ExpansionTileThemeData.iconColor] is used. If that
221-
/// is also null then the value of [ListTileThemeData.iconColor] is used.
222+
/// is also null then the value of [ColorScheme.primary] is used.
222223
///
223224
/// See also:
224225
///
@@ -229,6 +230,15 @@ class ExpansionTile extends StatefulWidget {
229230
/// The icon color of tile's expansion arrow icon when the sublist is collapsed.
230231
///
231232
/// Used to override to the [ListTileThemeData.iconColor].
233+
///
234+
/// If this property is null then [ExpansionTileThemeData.collapsedIconColor] is used. If that
235+
/// is also null and [ThemeData.useMaterial3] is true, [ColorScheme.onSurface] is used. Otherwise,
236+
/// defaults to [ThemeData.unselectedWidgetColor] color.
237+
///
238+
/// See also:
239+
///
240+
/// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
241+
/// [ExpansionTileThemeData].
232242
final Color? collapsedIconColor;
233243

234244

@@ -237,7 +247,8 @@ class ExpansionTile extends StatefulWidget {
237247
/// Used to override to the [ListTileThemeData.textColor].
238248
///
239249
/// If this property is null then [ExpansionTileThemeData.textColor] is used. If that
240-
/// is also null then the value of [ListTileThemeData.textColor] is used.
250+
/// is also null then and [ThemeData.useMaterial3] is true, color of the [TextTheme.bodyLarge]
251+
/// will be used for the [title] and [subtitle]. Otherwise, defaults to [ColorScheme.primary] color.
241252
///
242253
/// See also:
243254
///
@@ -249,8 +260,10 @@ class ExpansionTile extends StatefulWidget {
249260
///
250261
/// Used to override to the [ListTileThemeData.textColor].
251262
///
252-
/// If this property is null then [ExpansionTileThemeData.collapsedTextColor] is used. If that
253-
/// is also null then the value of [ListTileThemeData.textColor] is used.
263+
/// If this property is null then [ExpansionTileThemeData.collapsedTextColor] is used.
264+
/// If that is also null and [ThemeData.useMaterial3] is true, color of the
265+
/// [TextTheme.bodyLarge] will be used for the [title] and [subtitle]. Otherwise,
266+
/// defaults to color of the [TextTheme.titleMedium].
254267
///
255268
/// See also:
256269
///
@@ -443,7 +456,9 @@ class _ExpansionTileState extends State<ExpansionTile> with SingleTickerProvider
443456
void didChangeDependencies() {
444457
final ThemeData theme = Theme.of(context);
445458
final ExpansionTileThemeData expansionTileTheme = ExpansionTileTheme.of(context);
446-
final ColorScheme colorScheme = theme.colorScheme;
459+
final ExpansionTileThemeData defaults = theme.useMaterial3
460+
? _ExpansionTileDefaultsM3(context)
461+
: _ExpansionTileDefaultsM2(context);
447462
_borderTween
448463
..begin = widget.collapsedShape
449464
?? expansionTileTheme.collapsedShape
@@ -460,13 +475,13 @@ class _ExpansionTileState extends State<ExpansionTile> with SingleTickerProvider
460475
_headerColorTween
461476
..begin = widget.collapsedTextColor
462477
?? expansionTileTheme.collapsedTextColor
463-
?? theme.textTheme.titleMedium!.color
464-
..end = widget.textColor ?? expansionTileTheme.textColor ?? colorScheme.primary;
478+
?? defaults.collapsedTextColor
479+
..end = widget.textColor ?? expansionTileTheme.textColor ?? defaults.textColor;
465480
_iconColorTween
466481
..begin = widget.collapsedIconColor
467482
?? expansionTileTheme.collapsedIconColor
468-
?? theme.unselectedWidgetColor
469-
..end = widget.iconColor ?? expansionTileTheme.iconColor ?? colorScheme.primary;
483+
?? defaults.collapsedIconColor
484+
..end = widget.iconColor ?? expansionTileTheme.iconColor ?? defaults.iconColor;
470485
_backgroundColorTween
471486
..begin = widget.collapsedBackgroundColor ?? expansionTileTheme.collapsedBackgroundColor
472487
..end = widget.backgroundColor ?? expansionTileTheme.backgroundColor;
@@ -500,3 +515,54 @@ class _ExpansionTileState extends State<ExpansionTile> with SingleTickerProvider
500515
);
501516
}
502517
}
518+
519+
class _ExpansionTileDefaultsM2 extends ExpansionTileThemeData {
520+
_ExpansionTileDefaultsM2(this.context);
521+
522+
final BuildContext context;
523+
late final ThemeData _theme = Theme.of(context);
524+
late final ColorScheme _colorScheme = _theme.colorScheme;
525+
526+
@override
527+
Color? get textColor => _colorScheme.primary;
528+
529+
@override
530+
Color? get iconColor => _colorScheme.primary;
531+
532+
@override
533+
Color? get collapsedTextColor => _theme.textTheme.titleMedium!.color;
534+
535+
@override
536+
Color? get collapsedIconColor => _theme.unselectedWidgetColor;
537+
}
538+
539+
// BEGIN GENERATED TOKEN PROPERTIES - ExpansionTile
540+
541+
// Do not edit by hand. The code between the "BEGIN GENERATED" and
542+
// "END GENERATED" comments are generated from data in the Material
543+
// Design token database by the script:
544+
// dev/tools/gen_defaults/bin/gen_defaults.dart.
545+
546+
// Token database version: v0_158
547+
548+
class _ExpansionTileDefaultsM3 extends ExpansionTileThemeData {
549+
_ExpansionTileDefaultsM3(this.context);
550+
551+
final BuildContext context;
552+
late final ThemeData _theme = Theme.of(context);
553+
late final ColorScheme _colors = _theme.colorScheme;
554+
555+
@override
556+
Color? get textColor => _colors.onSurface;
557+
558+
@override
559+
Color? get iconColor => _colors.primary;
560+
561+
@override
562+
Color? get collapsedTextColor => _colors.onSurface;
563+
564+
@override
565+
Color? get collapsedIconColor => _colors.onSurfaceVariant;
566+
}
567+
568+
// 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.onSurfaceVariant);
545+
expect(getTextColor(), theme.colorScheme.onSurface);
546+
547+
await tester.tap(find.text('title'));
548+
await tester.pumpAndSettle();
549+
550+
expect(getIconColor(), theme.colorScheme.primary);
551+
expect(getTextColor(), theme.colorScheme.onSurface);
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)