From e57b7f4ea8ab8b348810d0a76f7bcf4aeabbe6d2 Mon Sep 17 00:00:00 2001 From: Taha Tesser Date: Fri, 9 Dec 2022 22:05:12 +0200 Subject: [PATCH] Add Material 3 support for `ListTile` - Part 1 (#116194) * Add Material 3 support for `ListTile` - Part 1 * minor refactor * Add `useMaterial3: false` to M2 tests --- dev/tools/gen_defaults/bin/gen_defaults.dart | 2 + .../gen_defaults/lib/list_tile_template.dart | 37 ++ .../flutter/lib/src/material/list_tile.dart | 308 ++++++++++----- .../lib/src/material/list_tile_theme.dart | 30 ++ .../flutter/test/material/list_tile_test.dart | 360 +++++++++++------- .../test/material/list_tile_theme_test.dart | 240 +++++++++++- 6 files changed, 747 insertions(+), 230 deletions(-) create mode 100644 dev/tools/gen_defaults/lib/list_tile_template.dart diff --git a/dev/tools/gen_defaults/bin/gen_defaults.dart b/dev/tools/gen_defaults/bin/gen_defaults.dart index f03fa509d62c..d2cf5b5fbcf8 100644 --- a/dev/tools/gen_defaults/bin/gen_defaults.dart +++ b/dev/tools/gen_defaults/bin/gen_defaults.dart @@ -35,6 +35,7 @@ import 'package:gen_defaults/filter_chip_template.dart'; import 'package:gen_defaults/icon_button_template.dart'; import 'package:gen_defaults/input_chip_template.dart'; import 'package:gen_defaults/input_decorator_template.dart'; +import 'package:gen_defaults/list_tile_template.dart'; import 'package:gen_defaults/menu_template.dart'; import 'package:gen_defaults/navigation_bar_template.dart'; import 'package:gen_defaults/navigation_drawer_template.dart'; @@ -154,6 +155,7 @@ Future main(List args) async { FilterChipTemplate('FilterChip', '$materialLib/filter_chip.dart', tokens).updateFile(); IconButtonTemplate('IconButton', '$materialLib/icon_button.dart', tokens).updateFile(); InputChipTemplate('InputChip', '$materialLib/input_chip.dart', tokens).updateFile(); + ListTileTemplate('LisTile', '$materialLib/list_tile.dart', tokens).updateFile(); InputDecoratorTemplate('InputDecorator', '$materialLib/input_decorator.dart', tokens).updateFile(); MenuTemplate('Menu', '$materialLib/menu_anchor.dart', tokens).updateFile(); NavigationBarTemplate('NavigationBar', '$materialLib/navigation_bar.dart', tokens).updateFile(); diff --git a/dev/tools/gen_defaults/lib/list_tile_template.dart b/dev/tools/gen_defaults/lib/list_tile_template.dart new file mode 100644 index 000000000000..d2dc15e241d6 --- /dev/null +++ b/dev/tools/gen_defaults/lib/list_tile_template.dart @@ -0,0 +1,37 @@ +// 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 ListTileTemplate extends TokenTemplate { + const ListTileTemplate(super.blockName, super.fileName, super.tokens); + + @override + String generate() => ''' +class _${blockName}DefaultsM3 extends ListTileThemeData { + const _${blockName}DefaultsM3(this.context) + : super(shape: ${shape("md.comp.list.list-item.container")}); + + final BuildContext context; + + @override + Color? get tileColor => ${componentColor("md.comp.list.list-item.container")}; + + @override + TextStyle? get titleTextStyle => ${textStyle("md.comp.list.list-item.label-text")}; + + @override + TextStyle? get subtitleTextStyle => ${textStyle("md.comp.list.list-item.supporting-text")}; + + @override + TextStyle? get leadingAndTrailingTextStyle => ${textStyle("md.comp.list.list-item.trailing-supporting-text")}; + + @override + Color? get selectedColor => ${componentColor('md.comp.list.list-item.selected.trailing-icon')}; + + @override + Color? get iconColor => ${componentColor('md.comp.list.list-item.unselected.trailing-icon')}; +} +'''; +} diff --git a/packages/flutter/lib/src/material/list_tile.dart b/packages/flutter/lib/src/material/list_tile.dart index a0d79fb324c8..2deb81431c34 100644 --- a/packages/flutter/lib/src/material/list_tile.dart +++ b/packages/flutter/lib/src/material/list_tile.dart @@ -15,6 +15,7 @@ import 'ink_decoration.dart'; import 'ink_well.dart'; import 'list_tile_theme.dart'; import 'material_state.dart'; +import 'text_theme.dart'; import 'theme.dart'; import 'theme_data.dart'; @@ -278,6 +279,9 @@ class ListTile extends StatelessWidget { this.selectedColor, this.iconColor, this.textColor, + this.titleTextStyle, + this.subtitleTextStyle, + this.leadingAndTrailingTextStyle, this.contentPadding, this.enabled = true, this.onTap, @@ -364,6 +368,10 @@ class ListTile extends StatelessWidget { /// If this property is null then its value is based on [ListTileTheme.dense]. /// /// Dense list tiles default to a smaller height. + /// + /// When [ThemeData.useMaterial3] is true, ListTile doesn't support [dense] property. + /// If [dense] or [ListTileTheme.dense] and [ThemeData.useMaterial3] are true, + /// ListTile will throw an assertion error. final bool? dense; /// Defines how compact the list tile's layout will be. @@ -421,6 +429,28 @@ class ListTile extends StatelessWidget { /// [ListTileThemeData]. final Color? textColor; + /// The text style for ListTile's [title]. + /// + /// If this property is null, then [ListTileThemeData.titleTextStyle] is used. + /// If that is also null and [ThemeData.useMaterial3] is true, [TextTheme.bodyLarge] + /// will be used. Otherwise, If ListTile style is [ListTileStyle.list], + /// [TextTheme.titleMedium] will be used and if ListTile style is [ListTileStyle.drawer], + /// [TextTheme.bodyLarge] will be used. + final TextStyle? titleTextStyle; + + /// The text style for ListTile's [subtitle]. + /// + /// If this property is null, then [ListTileThemeData.subtitleTextStyle] is used. + /// If that is also null, [TextTheme.bodyMedium] will be used. + final TextStyle? subtitleTextStyle; + + /// The text style for ListTile's [leading] and [trailing]. + /// + /// If this property is null, then [ListTileThemeData.leadingAndTrailingTextStyle] is used. + /// If that is also null and [ThemeData.useMaterial3] is true, [TextTheme.labelSmall] + /// will be used, otherwise [TextTheme.bodyMedium] will be used. + final TextStyle? leadingAndTrailingTextStyle; + /// Defines the font used for the [title]. /// /// If this property is null then [ListTileThemeData.style] is used. If that @@ -588,91 +618,20 @@ class ListTile extends StatelessWidget { ]; } - Color? _iconColor(ThemeData theme, ListTileThemeData tileTheme) { - if (!enabled) { - return theme.disabledColor; - } - - if (selected) { - return selectedColor ?? tileTheme.selectedColor ?? theme.listTileTheme.selectedColor ?? theme.colorScheme.primary; - } - - final Color? color = iconColor - ?? tileTheme.iconColor - ?? theme.listTileTheme.iconColor - // If [ThemeData.useMaterial3] is set to true the disabled icon color - // will be set to Theme.colorScheme.onSurface(0.38), if false, defaults to null, - // as described in: https://m3.material.io/components/icon-buttons/specs. - ?? (theme.useMaterial3 ? theme.colorScheme.onSurface.withOpacity(0.38) : null); - if (color != null) { - return color; - } - - switch (theme.brightness) { - case Brightness.light: - // For the sake of backwards compatibility, the default for unselected - // tiles is Colors.black45 rather than colorScheme.onSurface.withAlpha(0x73). - return Colors.black45; - case Brightness.dark: - return null; // null - use current icon theme color - } - } - - Color? _textColor(ThemeData theme, ListTileThemeData tileTheme, Color? defaultColor) { - if (!enabled) { - return theme.disabledColor; - } - - if (selected) { - return selectedColor ?? tileTheme.selectedColor ?? theme.listTileTheme.selectedColor ?? theme.colorScheme.primary; - } - - return textColor ?? tileTheme.textColor ?? theme.listTileTheme.textColor ?? defaultColor; - } - bool _isDenseLayout(ThemeData theme, ListTileThemeData tileTheme) { - return dense ?? tileTheme.dense ?? theme.listTileTheme.dense ?? false; - } - - TextStyle _titleTextStyle(ThemeData theme, ListTileThemeData tileTheme) { - final TextStyle textStyle; - switch(style ?? tileTheme.style ?? theme.listTileTheme.style ?? ListTileStyle.list) { - case ListTileStyle.drawer: - textStyle = theme.useMaterial3 ? theme.textTheme.bodyMedium! : theme.textTheme.bodyLarge!; - break; - case ListTileStyle.list: - textStyle = theme.useMaterial3 ? theme.textTheme.titleMedium! : theme.textTheme.titleMedium!; - break; + final bool isDense = dense ?? tileTheme.dense ?? theme.listTileTheme.dense ?? false; + if (theme.useMaterial3) { + assert(!isDense, 'ListTile.dense cannot be true while Theme.useMaterial3 is true.'); } - final Color? color = _textColor(theme, tileTheme, textStyle.color); - return _isDenseLayout(theme, tileTheme) - ? textStyle.copyWith(fontSize: 13.0, color: color) - : textStyle.copyWith(color: color); - } - - TextStyle _subtitleTextStyle(ThemeData theme, ListTileThemeData tileTheme) { - final TextStyle textStyle = theme.useMaterial3 ? theme.textTheme.bodyMedium! : theme.textTheme.bodyMedium!; - final Color? color = _textColor( - theme, - tileTheme, - theme.useMaterial3 ? theme.textTheme.bodySmall!.color : theme.textTheme.bodySmall!.color, - ); - return _isDenseLayout(theme, tileTheme) - ? textStyle.copyWith(color: color, fontSize: 12.0) - : textStyle.copyWith(color: color); - } - - TextStyle _trailingAndLeadingTextStyle(ThemeData theme, ListTileThemeData tileTheme) { - final TextStyle textStyle = theme.useMaterial3 ? theme.textTheme.bodyMedium! : theme.textTheme.bodyMedium!; - final Color? color = _textColor(theme, tileTheme, textStyle.color); - return textStyle.copyWith(color: color); + return isDense; } - Color _tileBackgroundColor(ThemeData theme, ListTileThemeData tileTheme) { + // TODO(TahaTesser): Refactor this to support list tile states. + Color _tileBackgroundColor(ThemeData theme, ListTileThemeData tileTheme, ListTileThemeData defaults) { final Color? color = selected ? selectedTileColor ?? tileTheme.selectedTileColor ?? theme.listTileTheme.selectedTileColor : tileColor ?? tileTheme.tileColor ?? theme.listTileTheme.tileColor; - return color ?? Colors.transparent; + return color ?? defaults.tileColor!; } @override @@ -680,23 +639,63 @@ class ListTile extends StatelessWidget { assert(debugCheckHasMaterial(context)); final ThemeData theme = Theme.of(context); final ListTileThemeData tileTheme = ListTileTheme.of(context); - final IconThemeData iconThemeData = IconThemeData(color: _iconColor(theme, tileTheme)); + final ListTileStyle listTileStyle = style + ?? tileTheme.style + ?? theme.listTileTheme.style + ?? ListTileStyle.list; + final ListTileThemeData defaults = theme.useMaterial3 + ? _LisTileDefaultsM3(context) + : _LisTileDefaultsM2(context, listTileStyle); + final Set states = { + if (!enabled) MaterialState.disabled, + if (selected) MaterialState.selected, + }; - TextStyle? leadingAndTrailingTextStyle; + Color? resolveColor(Color? explicitColor, Color? selectedColor, Color? enabledColor, [Color? disabledColor]) { + return _IndividualOverrides( + explicitColor: explicitColor, + selectedColor: selectedColor, + enabledColor: enabledColor, + disabledColor: disabledColor, + ).resolve(states); + } + + final Color? effectiveIconColor = resolveColor(iconColor, selectedColor, iconColor) + ?? resolveColor(tileTheme.iconColor, tileTheme.selectedColor, tileTheme.iconColor) + ?? resolveColor(theme.listTileTheme.iconColor, theme.listTileTheme.selectedColor, theme.listTileTheme.iconColor) + ?? resolveColor(defaults.iconColor, defaults.selectedColor, defaults.iconColor, theme.disabledColor); + final Color? effectiveColor = resolveColor(textColor, selectedColor, textColor) + ?? resolveColor(tileTheme.textColor, tileTheme.selectedColor, tileTheme.textColor) + ?? resolveColor(theme.listTileTheme.textColor, theme.listTileTheme.selectedColor, theme.listTileTheme.textColor) + ?? resolveColor(defaults.textColor, defaults.selectedColor, defaults.textColor, theme.disabledColor); + final IconThemeData iconThemeData = IconThemeData(color: effectiveIconColor); + + TextStyle? leadingAndTrailingStyle; if (leading != null || trailing != null) { - leadingAndTrailingTextStyle = _trailingAndLeadingTextStyle(theme, tileTheme); + leadingAndTrailingStyle = leadingAndTrailingTextStyle + ?? tileTheme.leadingAndTrailingTextStyle + ?? defaults.leadingAndTrailingTextStyle!; + final Color? leadingAndTrailingTextColor = effectiveColor; + leadingAndTrailingStyle = leadingAndTrailingStyle.copyWith(color: leadingAndTrailingTextColor); } Widget? leadingIcon; if (leading != null) { leadingIcon = AnimatedDefaultTextStyle( - style: leadingAndTrailingTextStyle!, + style: leadingAndTrailingStyle!, duration: kThemeChangeDuration, child: leading!, ); } - final TextStyle titleStyle = _titleTextStyle(theme, tileTheme); + TextStyle titleStyle = titleTextStyle + ?? tileTheme.titleTextStyle + ?? defaults.titleTextStyle!; + final Color? titleColor = effectiveColor; + titleStyle = titleStyle.copyWith( + color: titleColor, + fontSize: _isDenseLayout(theme, tileTheme) ? 13.0 : null, + ); final Widget titleText = AnimatedDefaultTextStyle( style: titleStyle, duration: kThemeChangeDuration, @@ -706,7 +705,14 @@ class ListTile extends StatelessWidget { Widget? subtitleText; TextStyle? subtitleStyle; if (subtitle != null) { - subtitleStyle = _subtitleTextStyle(theme, tileTheme); + subtitleStyle = subtitleTextStyle + ?? tileTheme.subtitleTextStyle + ?? defaults.subtitleTextStyle!; + final Color? subtitleColor = effectiveColor ?? theme.textTheme.bodySmall!.color; + subtitleStyle = subtitleStyle.copyWith( + color: subtitleColor, + fontSize: _isDenseLayout(theme, tileTheme) ? 12.0 : null, + ); subtitleText = AnimatedDefaultTextStyle( style: subtitleStyle, duration: kThemeChangeDuration, @@ -717,7 +723,7 @@ class ListTile extends StatelessWidget { Widget? trailingIcon; if (trailing != null) { trailingIcon = AnimatedDefaultTextStyle( - style: leadingAndTrailingTextStyle!, + style: leadingAndTrailingStyle!, duration: kThemeChangeDuration, child: trailing!, ); @@ -728,15 +734,13 @@ class ListTile extends StatelessWidget { final EdgeInsets resolvedContentPadding = contentPadding?.resolve(textDirection) ?? tileTheme.contentPadding?.resolve(textDirection) ?? defaultContentPadding; - - final Set states = { + // Show basic cursor when ListTile isn't enabled or gesture callbacks are null. + final Set mouseStates = { if (!enabled || (onTap == null && onLongPress == null)) MaterialState.disabled, - if (selected) MaterialState.selected, }; - - final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs(mouseCursor, states) - ?? tileTheme.mouseCursor?.resolve(states) - ?? MaterialStateMouseCursor.clickable.resolve(states); + final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs(mouseCursor, mouseStates) + ?? tileTheme.mouseCursor?.resolve(mouseStates) + ?? MaterialStateMouseCursor.clickable.resolve(mouseStates); return InkWell( customBorder: shape ?? tileTheme.shape, @@ -757,7 +761,7 @@ class ListTile extends StatelessWidget { child: Ink( decoration: ShapeDecoration( shape: shape ?? tileTheme.shape ?? const Border(), - color: _tileBackgroundColor(theme, tileTheme), + color: _tileBackgroundColor(theme, tileTheme, defaults), ), child: SafeArea( top: false, @@ -774,8 +778,8 @@ class ListTile extends StatelessWidget { visualDensity: visualDensity ?? tileTheme.visualDensity ?? theme.visualDensity, isThreeLine: isThreeLine, textDirection: textDirection, - titleBaselineType: titleStyle.textBaseline!, - subtitleBaselineType: subtitleStyle?.textBaseline, + titleBaselineType: titleStyle.textBaseline ?? defaults.titleTextStyle!.textBaseline!, + subtitleBaselineType: subtitleStyle?.textBaseline ?? defaults.subtitleTextStyle!.textBaseline!, horizontalTitleGap: horizontalTitleGap ?? tileTheme.horizontalTitleGap ?? 16, minVerticalPadding: minVerticalPadding ?? tileTheme.minVerticalPadding ?? 4, minLeadingWidth: minLeadingWidth ?? tileTheme.minLeadingWidth ?? 40, @@ -821,6 +825,36 @@ class ListTile extends StatelessWidget { } } +class _IndividualOverrides extends MaterialStateProperty { + _IndividualOverrides({ + this.explicitColor, + this.enabledColor, + this.selectedColor, + this.disabledColor, + }); + + final Color? explicitColor; + final Color? enabledColor; + final Color? selectedColor; + final Color? disabledColor; + + @override + Color? resolve(Set states) { + if (explicitColor is MaterialStateColor) { + return MaterialStateProperty.resolveAs(explicitColor, states); + } + + if (states.contains(MaterialState.disabled)) { + return disabledColor; + } + if (states.contains(MaterialState.selected)) { + return selectedColor; + } + + return enabledColor; + } +} + // Identifies the children of a _ListTileElement. enum _ListTileSlot { leading, @@ -1343,3 +1377,87 @@ class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_ return false; } } + +class _LisTileDefaultsM2 extends ListTileThemeData { + _LisTileDefaultsM2(this.context, ListTileStyle style) + : _themeData = Theme.of(context), + _textTheme = Theme.of(context).textTheme, + super( + shape: const Border(), + style: style, + ); + + final BuildContext context; + final ThemeData _themeData; + final TextTheme _textTheme; + + @override + Color? get tileColor => Colors.transparent; + + @override + TextStyle? get titleTextStyle { + switch (style!) { + case ListTileStyle.drawer: + return _textTheme.bodyLarge; + case ListTileStyle.list: + return _textTheme.titleMedium; + } + } + + @override + TextStyle? get subtitleTextStyle => _textTheme.bodyMedium; + + @override + TextStyle? get leadingAndTrailingTextStyle => _textTheme.bodyMedium; + + @override + Color? get selectedColor => _themeData.colorScheme.primary; + + @override + Color? get iconColor { + switch (_themeData.brightness) { + case Brightness.light: + // For the sake of backwards compatibility, the default for unselected + // tiles is Colors.black45 rather than colorScheme.onSurface.withAlpha(0x73). + return Colors.black45; + case Brightness.dark: + return null; // null, Use current icon theme color + } + } +} + +// BEGIN GENERATED TOKEN PROPERTIES - LisTile + +// 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_143 + +class _LisTileDefaultsM3 extends ListTileThemeData { + const _LisTileDefaultsM3(this.context) + : super(shape: const RoundedRectangleBorder()); + + final BuildContext context; + + @override + Color? get tileColor => Theme.of(context).colorScheme.surface; + + @override + TextStyle? get titleTextStyle => Theme.of(context).textTheme.bodyLarge; + + @override + TextStyle? get subtitleTextStyle => Theme.of(context).textTheme.bodyMedium; + + @override + TextStyle? get leadingAndTrailingTextStyle => Theme.of(context).textTheme.labelSmall; + + @override + Color? get selectedColor => Theme.of(context).colorScheme.primary; + + @override + Color? get iconColor => Theme.of(context).colorScheme.onSurface; +} + +// END GENERATED TOKEN PROPERTIES - LisTile diff --git a/packages/flutter/lib/src/material/list_tile_theme.dart b/packages/flutter/lib/src/material/list_tile_theme.dart index 501a608dd511..b5e421708b81 100644 --- a/packages/flutter/lib/src/material/list_tile_theme.dart +++ b/packages/flutter/lib/src/material/list_tile_theme.dart @@ -51,6 +51,9 @@ class ListTileThemeData with Diagnosticable { this.selectedColor, this.iconColor, this.textColor, + this.titleTextStyle, + this.subtitleTextStyle, + this.leadingAndTrailingTextStyle, this.contentPadding, this.tileColor, this.selectedTileColor, @@ -80,6 +83,15 @@ class ListTileThemeData with Diagnosticable { /// Overrides the default value of [ListTile.textColor]. final Color? textColor; + /// Overrides the default value of [ListTile.titleTextStyle]. + final TextStyle? titleTextStyle; + + /// Overrides the default value of [ListTile.subtitleTextStyle]. + final TextStyle? subtitleTextStyle; + + /// Overrides the default value of [ListTile.leadingAndTrailingTextStyle]. + final TextStyle? leadingAndTrailingTextStyle; + /// Overrides the default value of [ListTile.contentPadding]. final EdgeInsetsGeometry? contentPadding; @@ -116,6 +128,9 @@ class ListTileThemeData with Diagnosticable { Color? selectedColor, Color? iconColor, Color? textColor, + TextStyle? titleTextStyle, + TextStyle? subtitleTextStyle, + TextStyle? leadingAndTrailingTextStyle, EdgeInsetsGeometry? contentPadding, Color? tileColor, Color? selectedTileColor, @@ -134,6 +149,9 @@ class ListTileThemeData with Diagnosticable { selectedColor: selectedColor ?? this.selectedColor, iconColor: iconColor ?? this.iconColor, textColor: textColor ?? this.textColor, + titleTextStyle: titleTextStyle ?? this.titleTextStyle, + subtitleTextStyle: titleTextStyle ?? this.subtitleTextStyle, + leadingAndTrailingTextStyle: titleTextStyle ?? this.leadingAndTrailingTextStyle, contentPadding: contentPadding ?? this.contentPadding, tileColor: tileColor ?? this.tileColor, selectedTileColor: selectedTileColor ?? this.selectedTileColor, @@ -159,6 +177,9 @@ class ListTileThemeData with Diagnosticable { selectedColor: Color.lerp(a?.selectedColor, b?.selectedColor, t), iconColor: Color.lerp(a?.iconColor, b?.iconColor, t), textColor: Color.lerp(a?.textColor, b?.textColor, t), + titleTextStyle: TextStyle.lerp(a?.titleTextStyle, b?.titleTextStyle, t), + subtitleTextStyle: TextStyle.lerp(a?.subtitleTextStyle, b?.subtitleTextStyle, t), + leadingAndTrailingTextStyle: TextStyle.lerp(a?.leadingAndTrailingTextStyle, b?.leadingAndTrailingTextStyle, t), contentPadding: EdgeInsetsGeometry.lerp(a?.contentPadding, b?.contentPadding, t), tileColor: Color.lerp(a?.tileColor, b?.tileColor, t), selectedTileColor: Color.lerp(a?.selectedTileColor, b?.selectedTileColor, t), @@ -179,6 +200,9 @@ class ListTileThemeData with Diagnosticable { selectedColor, iconColor, textColor, + titleTextStyle, + subtitleTextStyle, + leadingAndTrailingTextStyle, contentPadding, tileColor, selectedTileColor, @@ -204,6 +228,9 @@ class ListTileThemeData with Diagnosticable { && other.style == style && other.selectedColor == selectedColor && other.iconColor == iconColor + && other.titleTextStyle == titleTextStyle + && other.subtitleTextStyle == subtitleTextStyle + && other.leadingAndTrailingTextStyle == leadingAndTrailingTextStyle && other.textColor == textColor && other.contentPadding == contentPadding && other.tileColor == tileColor @@ -225,6 +252,9 @@ class ListTileThemeData with Diagnosticable { properties.add(ColorProperty('selectedColor', selectedColor, defaultValue: null)); properties.add(ColorProperty('iconColor', iconColor, defaultValue: null)); properties.add(ColorProperty('textColor', textColor, defaultValue: null)); + properties.add(DiagnosticsProperty('titleTextStyle', titleTextStyle, defaultValue: null)); + properties.add(DiagnosticsProperty('subtitleTextStyle', subtitleTextStyle, defaultValue: null)); + properties.add(DiagnosticsProperty('leadingAndTrailingTextStyle', leadingAndTrailingTextStyle, defaultValue: null)); properties.add(DiagnosticsProperty('contentPadding', contentPadding, defaultValue: null)); properties.add(ColorProperty('tileColor', tileColor, defaultValue: null)); properties.add(ColorProperty('selectedTileColor', selectedTileColor, defaultValue: null)); diff --git a/packages/flutter/test/material/list_tile_test.dart b/packages/flutter/test/material/list_tile_test.dart index 7cbba67314c5..aa2e5e91cad8 100644 --- a/packages/flutter/test/material/list_tile_test.dart +++ b/packages/flutter/test/material/list_tile_test.dart @@ -1578,10 +1578,11 @@ void main() { testWidgets('ListTile default tile color', (WidgetTester tester) async { bool isSelected = false; - const Color defaultColor = Colors.transparent; + final ThemeData theme = ThemeData(useMaterial3: true); await tester.pumpWidget( MaterialApp( + theme: theme, home: Material( child: Center( child: StatefulBuilder( @@ -1600,13 +1601,13 @@ void main() { ), ); - expect(find.byType(Material), paints..rect(color: defaultColor)); + expect(find.byType(Material), paints..rect(color: theme.colorScheme.surface)); // Tap on tile to change isSelected. await tester.tap(find.byType(ListTile)); await tester.pumpAndSettle(); - expect(find.byType(Material), paints..rect(color: defaultColor)); + expect(find.byType(Material), paints..rect(color: theme.colorScheme.surface)); }); testWidgets('ListTile layout at zero size', (WidgetTester tester) async { @@ -2064,18 +2065,15 @@ void main() { expect(textColor(trailingKey), theme.disabledColor); }); - testWidgets('selected, enabled ListTile default icon color, light and dark themes', (WidgetTester tester) async { - const ColorScheme lightColorScheme = ColorScheme.light(); - const ColorScheme darkColorScheme = ColorScheme.dark(); + testWidgets('selected, enabled ListTile default icon color', (WidgetTester tester) async { + final ThemeData theme = ThemeData(useMaterial3: true); + final ColorScheme colorScheme = theme.colorScheme; final Key leadingKey = UniqueKey(); final Key titleKey = UniqueKey(); final Key subtitleKey = UniqueKey(); final Key trailingKey = UniqueKey(); - Widget buildFrame({ required Brightness brightness, required bool selected }) { - final ThemeData theme = brightness == Brightness.light - ? ThemeData.from(colorScheme: const ColorScheme.light(), useMaterial3: true) - : ThemeData.from(colorScheme: const ColorScheme.dark(), useMaterial3: true); + Widget buildFrame({required bool selected }) { return MaterialApp( theme: theme, home: Material( @@ -2094,56 +2092,32 @@ void main() { Color iconColor(Key key) => tester.state(find.byKey(key)).iconTheme.color!; - await tester.pumpWidget(buildFrame(brightness: Brightness.light, selected: true)); - expect(iconColor(leadingKey), lightColorScheme.primary); - expect(iconColor(titleKey), lightColorScheme.primary); - expect(iconColor(subtitleKey), lightColorScheme.primary); - expect(iconColor(trailingKey), lightColorScheme.primary); - - await tester.pumpWidget(buildFrame(brightness: Brightness.light, selected: false)); - expect(iconColor(leadingKey), lightColorScheme.onSurface.withOpacity(0.38)); - expect(iconColor(titleKey), lightColorScheme.onSurface.withOpacity(0.38)); - expect(iconColor(subtitleKey), lightColorScheme.onSurface.withOpacity(0.38)); - expect(iconColor(trailingKey), lightColorScheme.onSurface.withOpacity(0.38)); - - await tester.pumpWidget(buildFrame(brightness: Brightness.dark, selected: true)); - await tester.pumpAndSettle(); // Animated theme change - expect(iconColor(leadingKey), darkColorScheme.primary); - expect(iconColor(titleKey), darkColorScheme.primary); - expect(iconColor(subtitleKey), darkColorScheme.primary); - expect(iconColor(trailingKey), darkColorScheme.primary); - - // For this configuration, ListTile defers to the default IconTheme. - // The default dark theme's IconTheme has color:white - await tester.pumpWidget(buildFrame(brightness: Brightness.dark, selected: false)); - expect(iconColor(leadingKey), darkColorScheme.onSurface.withOpacity(0.38)); - expect(iconColor(titleKey), darkColorScheme.onSurface.withOpacity(0.38)); - expect(iconColor(subtitleKey), darkColorScheme.onSurface.withOpacity(0.38)); - expect(iconColor(trailingKey), darkColorScheme.onSurface.withOpacity(0.38)); + await tester.pumpWidget(buildFrame(selected: true)); + expect(iconColor(leadingKey), colorScheme.primary); + expect(iconColor(titleKey), colorScheme.primary); + expect(iconColor(subtitleKey), colorScheme.primary); + expect(iconColor(trailingKey), colorScheme.primary); + + await tester.pumpWidget(buildFrame(selected: false)); + expect(iconColor(leadingKey), colorScheme.onSurface); + expect(iconColor(titleKey), colorScheme.onSurface); + expect(iconColor(subtitleKey), colorScheme.onSurface); + expect(iconColor(trailingKey), colorScheme.onSurface); }); testWidgets('ListTile font size', (WidgetTester tester) async { - Widget buildFrame({ - bool dense = false, - bool enabled = true, - bool selected = false, - ListTileStyle? style, - }) { + Widget buildFrame() { return MaterialApp( theme: ThemeData(useMaterial3: true), home: Material( child: Center( child: Builder( builder: (BuildContext context) { - return ListTile( - dense: dense, - enabled: enabled, - selected: selected, - style: style, - leading: const TestText('leading'), - title: const TestText('title'), - subtitle: const TestText('subtitle') , - trailing: const TestText('trailing'), + return const ListTile( + leading: TestText('leading'), + title: TestText('title'), + subtitle: TestText('subtitle') , + trailing: TestText('trailing'), ); }, ), @@ -2152,76 +2126,31 @@ void main() { ); } - // ListTile - ListTileStyle.list (default). + // ListTile default text sizes. await tester.pumpWidget(buildFrame()); - RenderParagraph leading = _getTextRenderObject(tester, 'leading'); - expect(leading.text.style!.fontSize, 14.0); - RenderParagraph title = _getTextRenderObject(tester, 'title'); + final RenderParagraph leading = _getTextRenderObject(tester, 'leading'); + expect(leading.text.style!.fontSize, 11.0); + final RenderParagraph title = _getTextRenderObject(tester, 'title'); expect(title.text.style!.fontSize, 16.0); - RenderParagraph subtitle = _getTextRenderObject(tester, 'subtitle'); + final RenderParagraph subtitle = _getTextRenderObject(tester, 'subtitle'); expect(subtitle.text.style!.fontSize, 14.0); - RenderParagraph trailing = _getTextRenderObject(tester, 'trailing'); - expect(trailing.text.style!.fontSize, 14.0); - - // ListTile - Densed - ListTileStyle.list (default). - await tester.pumpWidget(buildFrame(dense: true)); - await tester.pumpAndSettle(); - leading = _getTextRenderObject(tester, 'leading'); - expect(leading.text.style!.fontSize, 14.0); - title = _getTextRenderObject(tester, 'title'); - expect(title.text.style!.fontSize, 13.0); - subtitle = _getTextRenderObject(tester, 'subtitle'); - expect(subtitle.text.style!.fontSize, 12.0); - trailing = _getTextRenderObject(tester, 'trailing'); - expect(trailing.text.style!.fontSize, 14.0); - - // ListTile - ListTileStyle.drawer. - await tester.pumpWidget(buildFrame(style: ListTileStyle.drawer)); - await tester.pumpAndSettle(); - leading = _getTextRenderObject(tester, 'leading'); - expect(leading.text.style!.fontSize, 14.0); - title = _getTextRenderObject(tester, 'title'); - expect(title.text.style!.fontSize, 14.0); - subtitle = _getTextRenderObject(tester, 'subtitle'); - expect(subtitle.text.style!.fontSize, 14.0); - trailing = _getTextRenderObject(tester, 'trailing'); - expect(trailing.text.style!.fontSize, 14.0); - - // ListTile - Densed - ListTileStyle.drawer. - await tester.pumpWidget(buildFrame(dense: true, style: ListTileStyle.drawer)); - await tester.pumpAndSettle(); - leading = _getTextRenderObject(tester, 'leading'); - expect(leading.text.style!.fontSize, 14.0); - title = _getTextRenderObject(tester, 'title'); - expect(title.text.style!.fontSize, 13.0); - subtitle = _getTextRenderObject(tester, 'subtitle'); - expect(subtitle.text.style!.fontSize, 12.0); - trailing = _getTextRenderObject(tester, 'trailing'); - expect(trailing.text.style!.fontSize, 14.0); + final RenderParagraph trailing = _getTextRenderObject(tester, 'trailing'); + expect(trailing.text.style!.fontSize, 11.0); }); testWidgets('ListTile text color', (WidgetTester tester) async { - Widget buildFrame({ - bool dense = false, - bool enabled = true, - bool selected = false, - ListTileStyle? style, - }) { + Widget buildFrame() { return MaterialApp( theme: ThemeData(useMaterial3: true), home: Material( child: Center( child: Builder( builder: (BuildContext context) { - return ListTile( - dense: dense, - enabled: enabled, - selected: selected, - style: style, - leading: const TestText('leading'), - title: const TestText('title'), - subtitle: const TestText('subtitle') , - trailing: const TestText('trailing'), + return const ListTile( + leading: TestText('leading'), + title: TestText('title'), + subtitle: TestText('subtitle') , + trailing: TestText('trailing'), ); }, ), @@ -2232,28 +2161,16 @@ void main() { final ThemeData theme = ThemeData(useMaterial3: true); - // ListTile - ListTileStyle.list (default). + // ListTile default text colors. await tester.pumpWidget(buildFrame()); - RenderParagraph leading = _getTextRenderObject(tester, 'leading'); - expect(leading.text.style!.color, theme.textTheme.bodyMedium!.color); - RenderParagraph title = _getTextRenderObject(tester, 'title'); - expect(title.text.style!.color, theme.textTheme.titleMedium!.color); - RenderParagraph subtitle = _getTextRenderObject(tester, 'subtitle'); - expect(subtitle.text.style!.color, theme.textTheme.bodySmall!.color); - RenderParagraph trailing = _getTextRenderObject(tester, 'trailing'); - expect(trailing.text.style!.color, theme.textTheme.bodyMedium!.color); - - // ListTile - ListTileStyle.drawer. - await tester.pumpWidget(buildFrame(style: ListTileStyle.drawer)); - await tester.pumpAndSettle(); - leading = _getTextRenderObject(tester, 'leading'); - expect(leading.text.style!.color, theme.textTheme.bodyMedium!.color); - title = _getTextRenderObject(tester, 'title'); + final RenderParagraph leading = _getTextRenderObject(tester, 'leading'); + expect(leading.text.style!.color, theme.textTheme.labelSmall!.color); + final RenderParagraph title = _getTextRenderObject(tester, 'title'); expect(title.text.style!.color, theme.textTheme.bodyLarge!.color); - subtitle = _getTextRenderObject(tester, 'subtitle'); - expect(subtitle.text.style!.color, theme.textTheme.bodySmall!.color); - trailing = _getTextRenderObject(tester, 'trailing'); - expect(trailing.text.style!.color, theme.textTheme.bodyMedium!.color); + final RenderParagraph subtitle = _getTextRenderObject(tester, 'subtitle'); + expect(subtitle.text.style!.color, theme.textTheme.bodyMedium!.color); + final RenderParagraph trailing = _getTextRenderObject(tester, 'trailing'); + expect(trailing.text.style!.color, theme.textTheme.labelSmall!.color); }); testWidgets('Default ListTile debugFillProperties', (WidgetTester tester) async { @@ -2333,6 +2250,151 @@ void main() { ); }); + testWidgets('ListTile throws assertion when useMaterial3 and dense are true', (WidgetTester tester) async { + Widget buildFrame({required bool useMaterial3}) { + return MaterialApp( + theme: ThemeData(useMaterial3: useMaterial3), + home: Material( + child: Center( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return const ListTile( + dense: true, + title: Text('Title'), + ); + }, + ), + ), + ), + ); + } + + await tester.pumpWidget(buildFrame(useMaterial3: true)); + final AssertionError exception = tester.takeException() as AssertionError; + expect( + exception.message, + 'ListTile.dense cannot be true while Theme.useMaterial3 is true.', + ); + + await tester.pumpWidget(buildFrame(useMaterial3: false)); + expect(tester.takeException(), isNull); + }); + + testWidgets('ListTile.textColor respects MaterialStateColor', (WidgetTester tester) async { + bool enabled = false; + bool selected = false; + const Color defaultColor = Colors.blue; + const Color selectedColor = Colors.green; + const Color disabledColor = Colors.red; + + Widget buildFrame() { + return MaterialApp( + theme: ThemeData(useMaterial3: true), + home: Material( + child: Center( + child: Builder( + builder: (BuildContext context) { + return ListTile( + enabled: enabled, + selected: selected, + textColor: MaterialStateColor.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return disabledColor; + } + + if (states.contains(MaterialState.selected)) { + return selectedColor; + } + + return defaultColor; + }), + title: const TestText('title'), + subtitle: const TestText('subtitle') , + ); + }, + ), + ), + ), + ); + } + + // Test disabled state. + await tester.pumpWidget(buildFrame()); + RenderParagraph title = _getTextRenderObject(tester, 'title'); + expect(title.text.style!.color, disabledColor); + + // Test enabled state. + enabled = true; + await tester.pumpWidget(buildFrame()); + await tester.pumpAndSettle(); + title = _getTextRenderObject(tester, 'title'); + expect(title.text.style!.color, defaultColor); + + // Test selected state. + selected = true; + await tester.pumpWidget(buildFrame()); + await tester.pumpAndSettle(); + title = _getTextRenderObject(tester, 'title'); + expect(title.text.style!.color, selectedColor); + }); + + testWidgets('ListTile.iconColor respects MaterialStateColor', (WidgetTester tester) async { + bool enabled = false; + bool selected = false; + const Color defaultColor = Colors.blue; + const Color selectedColor = Colors.green; + const Color disabledColor = Colors.red; + final Key leadingKey = UniqueKey(); + + Widget buildFrame() { + return MaterialApp( + theme: ThemeData(useMaterial3: true), + home: Material( + child: Center( + child: Builder( + builder: (BuildContext context) { + return ListTile( + enabled: enabled, + selected: selected, + iconColor: MaterialStateColor.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return disabledColor; + } + + if (states.contains(MaterialState.selected)) { + return selectedColor; + } + + return defaultColor; + }), + leading: TestIcon(key: leadingKey), + ); + }, + ), + ), + ), + ); + } + + Color iconColor(Key key) => tester.state(find.byKey(key)).iconTheme.color!; + + // Test disabled state. + await tester.pumpWidget(buildFrame()); + expect(iconColor(leadingKey), disabledColor); + + // Test enabled state. + enabled = true; + await tester.pumpWidget(buildFrame()); + await tester.pumpAndSettle(); + expect(iconColor(leadingKey), defaultColor); + + // Test selected state. + selected = true; + await tester.pumpWidget(buildFrame()); + await tester.pumpAndSettle(); + expect(iconColor(leadingKey), selectedColor); + }); + group('Material 2', () { // Tests that are only relevant for Material 2. Once ThemeData.useMaterial3 // is turned on by default, these tests can be removed. @@ -2345,6 +2407,7 @@ void main() { ListTileStyle? style, }) { return MaterialApp( + theme: ThemeData(useMaterial3: false), home: Material( child: Center( child: Builder( @@ -2422,6 +2485,7 @@ void main() { ListTileStyle? style, }) { return MaterialApp( + theme: ThemeData(useMaterial3: false), home: Material( child: Center( child: Builder( @@ -2481,8 +2545,8 @@ void main() { Widget buildFrame({ required Brightness brightness, required bool selected }) { final ThemeData theme = brightness == Brightness.light - ? ThemeData.from(colorScheme: const ColorScheme.light()) - : ThemeData.from(colorScheme: const ColorScheme.dark()); + ? ThemeData.from(colorScheme: const ColorScheme.light(), useMaterial3: false) + : ThemeData.from(colorScheme: const ColorScheme.dark(), useMaterial3: false); return MaterialApp( theme: theme, home: Material( @@ -2528,6 +2592,40 @@ void main() { expect(iconColor(subtitleKey), Colors.white); expect(iconColor(trailingKey), Colors.white); }); + + testWidgets('ListTile default tile color', (WidgetTester tester) async { + bool isSelected = false; + const Color defaultColor = Colors.transparent; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(useMaterial3: false), + home: Material( + child: Center( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return ListTile( + selected: isSelected, + onTap: () { + setState(()=> isSelected = !isSelected); + }, + title: const Text('Title'), + ); + }, + ), + ), + ), + ), + ); + + expect(find.byType(Material), paints..rect(color: defaultColor)); + + // Tap on tile to change isSelected. + await tester.tap(find.byType(ListTile)); + await tester.pumpAndSettle(); + + expect(find.byType(Material), paints..rect(color: defaultColor)); + }); }); } diff --git a/packages/flutter/test/material/list_tile_theme_test.dart b/packages/flutter/test/material/list_tile_theme_test.dart index 89d43e840189..dd1cfc459018 100644 --- a/packages/flutter/test/material/list_tile_theme_test.dart +++ b/packages/flutter/test/material/list_tile_theme_test.dart @@ -59,6 +59,9 @@ void main() { expect(themeData.selectedColor, null); expect(themeData.iconColor, null); expect(themeData.textColor, null); + expect(themeData.titleTextStyle, null); + expect(themeData.subtitleTextStyle, null); + expect(themeData.leadingAndTrailingTextStyle, null); expect(themeData.contentPadding, null); expect(themeData.tileColor, null); expect(themeData.selectedTileColor, null); @@ -91,9 +94,12 @@ void main() { selectedColor: Color(0x00000001), iconColor: Color(0x00000002), textColor: Color(0x00000003), + titleTextStyle: TextStyle(color: Color(0x00000004)), + subtitleTextStyle: TextStyle(color: Color(0x00000005)), + leadingAndTrailingTextStyle: TextStyle(color: Color(0x00000006)), contentPadding: EdgeInsets.all(100), - tileColor: Color(0x00000004), - selectedTileColor: Color(0x00000005), + tileColor: Color(0x00000007), + selectedTileColor: Color(0x00000008), horizontalTitleGap: 200, minVerticalPadding: 300, minLeadingWidth: 400, @@ -116,9 +122,12 @@ void main() { 'selectedColor: Color(0x00000001)', 'iconColor: Color(0x00000002)', 'textColor: Color(0x00000003)', + 'titleTextStyle: TextStyle(inherit: true, color: Color(0x00000004))', + 'subtitleTextStyle: TextStyle(inherit: true, color: Color(0x00000005))', + 'leadingAndTrailingTextStyle: TextStyle(inherit: true, color: Color(0x00000006))', 'contentPadding: EdgeInsets.all(100.0)', - 'tileColor: Color(0x00000004)', - 'selectedTileColor: Color(0x00000005)', + 'tileColor: Color(0x00000007)', + 'selectedTileColor: Color(0x00000008)', 'horizontalTitleGap: 200.0', 'minVerticalPadding: 300.0', 'minLeadingWidth: 400.0', @@ -365,6 +374,99 @@ void main() { expect(textColor(trailingKey), theme.disabledColor); }); + testWidgets( + "ListTile respects ListTileTheme's titleTextStyle, subtitleTextStyle & leadingAndTrailingTextStyle", + (WidgetTester tester) async { + final ThemeData theme = ThemeData( + useMaterial3: true, + listTileTheme: const ListTileThemeData( + titleTextStyle: TextStyle(fontSize: 20.0), + subtitleTextStyle: TextStyle(fontSize: 17.5), + leadingAndTrailingTextStyle: TextStyle(fontSize: 15.0), + ), + ); + + Widget buildFrame() { + return MaterialApp( + theme: theme, + home: Material( + child: Center( + child: Builder( + builder: (BuildContext context) { + return const ListTile( + leading: TestText('leading'), + title: TestText('title'), + subtitle: TestText('subtitle') , + trailing: TestText('trailing'), + ); + }, + ), + ), + ), + ); + } + + await tester.pumpWidget(buildFrame()); + final RenderParagraph leading = _getTextRenderObject(tester, 'leading'); + expect(leading.text.style!.fontSize, 15.0); + final RenderParagraph title = _getTextRenderObject(tester, 'title'); + expect(title.text.style!.fontSize, 20.0); + final RenderParagraph subtitle = _getTextRenderObject(tester, 'subtitle'); + expect(subtitle.text.style!.fontSize, 17.5); + final RenderParagraph trailing = _getTextRenderObject(tester, 'trailing'); + expect(trailing.text.style!.fontSize, 15.0); + }); + + testWidgets( + "ListTile's titleTextStyle, subtitleTextStyle & leadingAndTrailingTextStyle are overridden by ListTile properties", + (WidgetTester tester) async { + final ThemeData theme = ThemeData( + useMaterial3: true, + listTileTheme: const ListTileThemeData( + titleTextStyle: TextStyle(fontSize: 20.0), + subtitleTextStyle: TextStyle(fontSize: 17.5), + leadingAndTrailingTextStyle: TextStyle(fontSize: 15.0), + ), + ); + + const TextStyle titleTextStyle = TextStyle(fontSize: 23.0); + const TextStyle subtitleTextStyle = TextStyle(fontSize: 20.0); + const TextStyle leadingAndTrailingTextStyle = TextStyle(fontSize: 18.0); + + Widget buildFrame() { + return MaterialApp( + theme: theme, + home: Material( + child: Center( + child: Builder( + builder: (BuildContext context) { + return const ListTile( + titleTextStyle: titleTextStyle, + subtitleTextStyle: subtitleTextStyle, + leadingAndTrailingTextStyle: leadingAndTrailingTextStyle, + leading: TestText('leading'), + title: TestText('title'), + subtitle: TestText('subtitle') , + trailing: TestText('trailing'), + ); + }, + ), + ), + ), + ); + } + + await tester.pumpWidget(buildFrame()); + final RenderParagraph leading = _getTextRenderObject(tester, 'leading'); + expect(leading.text.style!.fontSize, 18.0); + final RenderParagraph title = _getTextRenderObject(tester, 'title'); + expect(title.text.style!.fontSize, 23.0); + final RenderParagraph subtitle = _getTextRenderObject(tester, 'subtitle'); + expect(subtitle.text.style!.fontSize, 20.0); + final RenderParagraph trailing = _getTextRenderObject(tester, 'trailing'); + expect(trailing.text.style!.fontSize, 18.0); + }); + testWidgets("ListTile respects ListTileTheme's tileColor & selectedTileColor", (WidgetTester tester) async { late ListTileThemeData theme; bool isSelected = false; @@ -479,4 +581,134 @@ void main() { // Test shape. expect(inkWellBorder, shapeBorder); }); + + testWidgets('ListTile respects MaterialStateColor LisTileTheme.textColor', (WidgetTester tester) async { + bool enabled = false; + bool selected = false; + const Color defaultColor = Colors.blue; + const Color selectedColor = Colors.green; + const Color disabledColor = Colors.red; + + final ThemeData theme = ThemeData( + listTileTheme: ListTileThemeData( + textColor: MaterialStateColor.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return disabledColor; + } + + if (states.contains(MaterialState.selected)) { + return selectedColor; + } + + return defaultColor; + }), + ), + ); + Widget buildFrame() { + return MaterialApp( + theme: theme, + home: Material( + child: Center( + child: Builder( + builder: (BuildContext context) { + return ListTile( + enabled: enabled, + selected: selected, + title: const TestText('title'), + subtitle: const TestText('subtitle') , + ); + }, + ), + ), + ), + ); + } + + // Test disabled state. + await tester.pumpWidget(buildFrame()); + RenderParagraph title = _getTextRenderObject(tester, 'title'); + expect(title.text.style!.color, disabledColor); + + // Test enabled state. + enabled = true; + await tester.pumpWidget(buildFrame()); + await tester.pumpAndSettle(); + title = _getTextRenderObject(tester, 'title'); + expect(title.text.style!.color, defaultColor); + + // Test selected state. + selected = true; + await tester.pumpWidget(buildFrame()); + await tester.pumpAndSettle(); + title = _getTextRenderObject(tester, 'title'); + expect(title.text.style!.color, selectedColor); + }); + + testWidgets('ListTile respects MaterialStateColor LisTileTheme.iconColor', (WidgetTester tester) async { + bool enabled = false; + bool selected = false; + const Color defaultColor = Colors.blue; + const Color selectedColor = Colors.green; + const Color disabledColor = Colors.red; + final Key leadingKey = UniqueKey(); + + final ThemeData theme = ThemeData( + listTileTheme: ListTileThemeData( + iconColor: MaterialStateColor.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return disabledColor; + } + + if (states.contains(MaterialState.selected)) { + return selectedColor; + } + + return defaultColor; + }), + ), + ); + Widget buildFrame() { + return MaterialApp( + theme: theme, + home: Material( + child: Center( + child: Builder( + builder: (BuildContext context) { + return ListTile( + enabled: enabled, + selected: selected, + leading: TestIcon(key: leadingKey), + ); + }, + ), + ), + ), + ); + } + + Color iconColor(Key key) => tester.state(find.byKey(key)).iconTheme.color!; + + // Test disabled state. + await tester.pumpWidget(buildFrame()); + expect(iconColor(leadingKey), disabledColor); + + // Test enabled state. + enabled = true; + await tester.pumpWidget(buildFrame()); + await tester.pumpAndSettle(); + expect(iconColor(leadingKey), defaultColor); + + // Test selected state. + selected = true; + await tester.pumpWidget(buildFrame()); + await tester.pumpAndSettle(); + expect(iconColor(leadingKey), selectedColor); + }); +} + +RenderParagraph _getTextRenderObject(WidgetTester tester, String text) { + return tester.renderObject(find.descendant( + of: find.byType(ListTile), + matching: find.text(text), + )); }