diff --git a/packages/flutter/lib/material.dart b/packages/flutter/lib/material.dart index c5be32bbaec..217c646efb6 100644 --- a/packages/flutter/lib/material.dart +++ b/packages/flutter/lib/material.dart @@ -94,6 +94,7 @@ export 'src/material/input_border.dart'; export 'src/material/input_date_picker_form_field.dart'; export 'src/material/input_decorator.dart'; export 'src/material/list_tile.dart'; +export 'src/material/list_tile_theme.dart'; export 'src/material/material.dart'; export 'src/material/material_button.dart'; export 'src/material/material_localizations.dart'; diff --git a/packages/flutter/lib/src/material/checkbox_list_tile.dart b/packages/flutter/lib/src/material/checkbox_list_tile.dart index fd8915062ec..3051dcb06da 100644 --- a/packages/flutter/lib/src/material/checkbox_list_tile.dart +++ b/packages/flutter/lib/src/material/checkbox_list_tile.dart @@ -6,6 +6,7 @@ import 'package:flutter/widgets.dart'; import 'checkbox.dart'; import 'list_tile.dart'; +import 'list_tile_theme.dart'; import 'theme.dart'; import 'theme_data.dart'; diff --git a/packages/flutter/lib/src/material/drawer.dart b/packages/flutter/lib/src/material/drawer.dart index 9a54848b978..f69b6114db6 100644 --- a/packages/flutter/lib/src/material/drawer.dart +++ b/packages/flutter/lib/src/material/drawer.dart @@ -9,6 +9,7 @@ import 'colors.dart'; import 'debug.dart'; import 'drawer_theme.dart'; import 'list_tile.dart'; +import 'list_tile_theme.dart'; import 'material.dart'; import 'material_localizations.dart'; import 'theme.dart'; diff --git a/packages/flutter/lib/src/material/expansion_tile.dart b/packages/flutter/lib/src/material/expansion_tile.dart index d341c0ab337..173ebb42833 100644 --- a/packages/flutter/lib/src/material/expansion_tile.dart +++ b/packages/flutter/lib/src/material/expansion_tile.dart @@ -9,6 +9,7 @@ import 'colors.dart'; import 'expansion_tile_theme.dart'; import 'icons.dart'; import 'list_tile.dart'; +import 'list_tile_theme.dart'; import 'theme.dart'; const Duration _kExpand = Duration(milliseconds: 200); diff --git a/packages/flutter/lib/src/material/list_tile.dart b/packages/flutter/lib/src/material/list_tile.dart index cd104f1d15b..18ff56122ed 100644 --- a/packages/flutter/lib/src/material/list_tile.dart +++ b/packages/flutter/lib/src/material/list_tile.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:math' as math; -import 'dart:ui' show lerpDouble; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; @@ -15,6 +14,7 @@ import 'debug.dart'; import 'divider.dart'; import 'ink_decoration.dart'; import 'ink_well.dart'; +import 'list_tile_theme.dart'; import 'material_state.dart'; import 'theme.dart'; import 'theme_data.dart'; @@ -57,480 +57,6 @@ enum ListTileControlAffinity { platform, } -/// Used with [ListTileTheme] to define default property values for -/// descendant [ListTile] widgets, as well as classes that build -/// [ListTile]s, like [CheckboxListTile], [RadioListTile], and -/// [SwitchListTile]. -/// -/// Descendant widgets obtain the current [ListTileThemeData] object -/// using `ListTileTheme.of(context)`. Instances of -/// [ListTileThemeData] can be customized with -/// [ListTileThemeData.copyWith]. -/// -/// A [ListTileThemeData] is often specified as part of the -/// overall [Theme] with [ThemeData.listTileTheme]. -/// -/// All [ListTileThemeData] properties are `null` by default. -/// When a theme property is null, the [ListTile] will provide its own -/// default based on the overall [Theme]'s textTheme and -/// colorScheme. See the individual [ListTile] properties for details. -/// -/// The [Drawer] widget specifies a list tile theme for its children that -/// defines [style] to be [ListTileStyle.drawer]. -/// -/// See also: -/// -/// * [ThemeData], which describes the overall theme information for the -/// application. -@immutable -class ListTileThemeData with Diagnosticable { - /// Creates a [ListTileThemeData]. - const ListTileThemeData ({ - this.dense, - this.shape, - this.style, - this.selectedColor, - this.iconColor, - this.textColor, - this.contentPadding, - this.tileColor, - this.selectedTileColor, - this.horizontalTitleGap, - this.minVerticalPadding, - this.minLeadingWidth, - this.enableFeedback, - this.mouseCursor, - }); - - /// Overrides the default value of [ListTile.dense]. - final bool? dense; - - /// Overrides the default value of [ListTile.shape]. - final ShapeBorder? shape; - - /// Overrides the default value of [ListTile.style]. - final ListTileStyle? style; - - /// Overrides the default value of [ListTile.selectedColor]. - final Color? selectedColor; - - /// Overrides the default value of [ListTile.iconColor]. - final Color? iconColor; - - /// Overrides the default value of [ListTile.textColor]. - final Color? textColor; - - /// Overrides the default value of [ListTile.contentPadding]. - final EdgeInsetsGeometry? contentPadding; - - /// Overrides the default value of [ListTile.tileColor]. - final Color? tileColor; - - /// Overrides the default value of [ListTile.selectedTileColor]. - final Color? selectedTileColor; - - /// Overrides the default value of [ListTile.horizontalTitleGap]. - final double? horizontalTitleGap; - - /// Overrides the default value of [ListTile.minVerticalPadding]. - final double? minVerticalPadding; - - /// Overrides the default value of [ListTile.minLeadingWidth]. - final double? minLeadingWidth; - - /// Overrides the default value of [ListTile.enableFeedback]. - final bool? enableFeedback; - - /// If specified, overrides the default value of [ListTile.mouseCursor]. - final MaterialStateProperty? mouseCursor; - - /// Creates a copy of this object with the given fields replaced with the - /// new values. - ListTileThemeData copyWith({ - bool? dense, - ShapeBorder? shape, - ListTileStyle? style, - Color? selectedColor, - Color? iconColor, - Color? textColor, - EdgeInsetsGeometry? contentPadding, - Color? tileColor, - Color? selectedTileColor, - double? horizontalTitleGap, - double? minVerticalPadding, - double? minLeadingWidth, - bool? enableFeedback, - MaterialStateProperty? mouseCursor, - }) { - return ListTileThemeData( - dense: dense ?? this.dense, - shape: shape ?? this.shape, - style: style ?? this.style, - selectedColor: selectedColor ?? this.selectedColor, - iconColor: iconColor ?? this.iconColor, - textColor: textColor ?? this.textColor, - contentPadding: contentPadding ?? this.contentPadding, - tileColor: tileColor ?? this.tileColor, - selectedTileColor: selectedTileColor ?? this.selectedTileColor, - horizontalTitleGap: horizontalTitleGap ?? this.horizontalTitleGap, - minVerticalPadding: minVerticalPadding ?? this.minVerticalPadding, - minLeadingWidth: minLeadingWidth ?? this.minLeadingWidth, - enableFeedback: enableFeedback ?? this.enableFeedback, - mouseCursor: mouseCursor ?? this.mouseCursor, - ); - } - - /// Linearly interpolate between ListTileThemeData objects. - static ListTileThemeData? lerp(ListTileThemeData? a, ListTileThemeData? b, double t) { - assert (t != null); - if (a == null && b == null) - return null; - return ListTileThemeData( - dense: t < 0.5 ? a?.dense : b?.dense, - shape: ShapeBorder.lerp(a?.shape, b?.shape, t), - style: t < 0.5 ? a?.style : b?.style, - selectedColor: Color.lerp(a?.selectedColor, b?.selectedColor, t), - iconColor: Color.lerp(a?.iconColor, b?.iconColor, t), - textColor: Color.lerp(a?.textColor, b?.textColor, 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), - horizontalTitleGap: lerpDouble(a?.horizontalTitleGap, b?.horizontalTitleGap, t), - minVerticalPadding: lerpDouble(a?.minVerticalPadding, b?.minVerticalPadding, t), - minLeadingWidth: lerpDouble(a?.minLeadingWidth, b?.minLeadingWidth, t), - enableFeedback: t < 0.5 ? a?.enableFeedback : b?.enableFeedback, - mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor, - ); - } - - @override - int get hashCode => Object.hash( - dense, - shape, - style, - selectedColor, - iconColor, - textColor, - contentPadding, - tileColor, - selectedTileColor, - horizontalTitleGap, - minVerticalPadding, - minLeadingWidth, - enableFeedback, - mouseCursor, - ); - - @override - bool operator ==(Object other) { - if (identical(this, other)) - return true; - if (other.runtimeType != runtimeType) - return false; - return other is ListTileThemeData - && other.dense == dense - && other.shape == shape - && other.style == style - && other.selectedColor == selectedColor - && other.iconColor == iconColor - && other.textColor == textColor - && other.contentPadding == contentPadding - && other.tileColor == tileColor - && other.selectedTileColor == selectedTileColor - && other.horizontalTitleGap == horizontalTitleGap - && other.minVerticalPadding == minVerticalPadding - && other.minLeadingWidth == minLeadingWidth - && other.enableFeedback == enableFeedback - && other.mouseCursor == mouseCursor; - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('dense', dense, defaultValue: null)); - properties.add(DiagnosticsProperty('shape', shape, defaultValue: null)); - properties.add(EnumProperty('style', style, defaultValue: null)); - properties.add(ColorProperty('selectedColor', selectedColor, defaultValue: null)); - properties.add(ColorProperty('iconColor', iconColor, defaultValue: null)); - properties.add(ColorProperty('textColor', textColor, defaultValue: null)); - properties.add(DiagnosticsProperty('contentPadding', contentPadding, defaultValue: null)); - properties.add(ColorProperty('tileColor', tileColor, defaultValue: null)); - properties.add(ColorProperty('selectedTileColor', selectedTileColor, defaultValue: null)); - properties.add(DoubleProperty('horizontalTitleGap', horizontalTitleGap, defaultValue: null)); - properties.add(DoubleProperty('minVerticalPadding', minVerticalPadding, defaultValue: null)); - properties.add(DoubleProperty('minLeadingWidth', minLeadingWidth, defaultValue: null)); - properties.add(DiagnosticsProperty('enableFeedback', enableFeedback, defaultValue: null)); - properties.add(DiagnosticsProperty>('mouseCursor', mouseCursor, defaultValue: null)); - } -} - -/// An inherited widget that defines color and style parameters for [ListTile]s -/// in this widget's subtree. -/// -/// Values specified here are used for [ListTile] properties that are not given -/// an explicit non-null value. -/// -/// The [Drawer] widget specifies a tile theme for its children which sets -/// [style] to [ListTileStyle.drawer]. -class ListTileTheme extends InheritedTheme { - /// Creates a list tile theme that defines the color and style parameters for - /// descendant [ListTile]s. - /// - /// Only the [data] parameter should be used. The other parameters are - /// redundant (are now obsolete) and will be deprecated in a future update. - const ListTileTheme({ - Key? key, - ListTileThemeData? data, - bool? dense, - ShapeBorder? shape, - ListTileStyle? style, - Color? selectedColor, - Color? iconColor, - Color? textColor, - EdgeInsetsGeometry? contentPadding, - Color? tileColor, - Color? selectedTileColor, - bool? enableFeedback, - MaterialStateProperty? mouseCursor, - double? horizontalTitleGap, - double? minVerticalPadding, - double? minLeadingWidth, - required Widget child, - }) : assert( - data == null || - (shape ?? - selectedColor ?? - iconColor ?? - textColor ?? - contentPadding ?? - tileColor ?? - selectedTileColor ?? - enableFeedback ?? - mouseCursor ?? - horizontalTitleGap ?? - minVerticalPadding ?? - minLeadingWidth) == null), - _data = data, - _dense = dense, - _shape = shape, - _style = style, - _selectedColor = selectedColor, - _iconColor = iconColor, - _textColor = textColor, - _contentPadding = contentPadding, - _tileColor = tileColor, - _selectedTileColor = selectedTileColor, - _enableFeedback = enableFeedback, - _mouseCursor = mouseCursor, - _horizontalTitleGap = horizontalTitleGap, - _minVerticalPadding = minVerticalPadding, - _minLeadingWidth = minLeadingWidth, - super(key: key, child: child); - - final ListTileThemeData? _data; - final bool? _dense; - final ShapeBorder? _shape; - final ListTileStyle? _style; - final Color? _selectedColor; - final Color? _iconColor; - final Color? _textColor; - final EdgeInsetsGeometry? _contentPadding; - final Color? _tileColor; - final Color? _selectedTileColor; - final double? _horizontalTitleGap; - final double? _minVerticalPadding; - final double? _minLeadingWidth; - final bool? _enableFeedback; - final MaterialStateProperty? _mouseCursor; - - /// The configuration of this theme. - ListTileThemeData get data { - return _data ?? ListTileThemeData( - dense: _dense, - shape: _shape, - style: _style, - selectedColor: _selectedColor, - iconColor: _iconColor, - textColor: _textColor, - contentPadding: _contentPadding, - tileColor: _tileColor, - selectedTileColor: _selectedTileColor, - enableFeedback: _enableFeedback, - mouseCursor: _mouseCursor, - horizontalTitleGap: _horizontalTitleGap, - minVerticalPadding: _minVerticalPadding, - minLeadingWidth: _minLeadingWidth, - ); - } - - /// Overrides the default value of [ListTile.dense]. - /// - /// This property is obsolete: please use the [data] - /// [ListTileThemeData.dense] property instead. - bool? get dense => _data != null ? _data!.dense : _dense; - - /// Overrides the default value of [ListTile.shape]. - /// - /// This property is obsolete: please use the [data] - /// [ListTileThemeData.shape] property instead. - ShapeBorder? get shape => _data != null ? _data!.shape : _shape; - - /// Overrides the default value of [ListTile.style]. - /// - /// This property is obsolete: please use the [data] - /// [ListTileThemeData.style] property instead. - ListTileStyle? get style => _data != null ? _data!.style : _style; - - /// Overrides the default value of [ListTile.selectedColor]. - /// - /// This property is obsolete: please use the [data] - /// [ListTileThemeData.selectedColor] property instead. - Color? get selectedColor => _data != null ? _data!.selectedColor : _selectedColor; - - /// Overrides the default value of [ListTile.iconColor]. - /// - /// This property is obsolete: please use the [data] - /// [ListTileThemeData.iconColor] property instead. - Color? get iconColor => _data != null ? _data!.iconColor : _iconColor; - - /// Overrides the default value of [ListTile.textColor]. - /// - /// This property is obsolete: please use the [data] - /// [ListTileThemeData.textColor] property instead. - Color? get textColor => _data != null ? _data!.textColor : _textColor; - - /// Overrides the default value of [ListTile.contentPadding]. - /// - /// This property is obsolete: please use the [data] - /// [ListTileThemeData.contentPadding] property instead. - EdgeInsetsGeometry? get contentPadding => _data != null ? _data!.contentPadding : _contentPadding; - - /// Overrides the default value of [ListTile.tileColor]. - /// - /// This property is obsolete: please use the [data] - /// [ListTileThemeData.tileColor] property instead. - Color? get tileColor => _data != null ? _data!.tileColor : _tileColor; - - /// Overrides the default value of [ListTile.selectedTileColor]. - /// - /// This property is obsolete: please use the [data] - /// [ListTileThemeData.selectedTileColor] property instead. - Color? get selectedTileColor => _data != null ? _data!.selectedTileColor : _selectedTileColor; - - /// Overrides the default value of [ListTile.horizontalTitleGap]. - /// - /// This property is obsolete: please use the [data] - /// [ListTileThemeData.horizontalTitleGap] property instead. - double? get horizontalTitleGap => _data != null ? _data!.horizontalTitleGap : _horizontalTitleGap; - - /// Overrides the default value of [ListTile.minVerticalPadding]. - /// - /// This property is obsolete: please use the [data] - /// [ListTileThemeData.minVerticalPadding] property instead. - double? get minVerticalPadding => _data != null ? _data!.minVerticalPadding : _minVerticalPadding; - - /// Overrides the default value of [ListTile.minLeadingWidth]. - /// - /// This property is obsolete: please use the [data] - /// [ListTileThemeData.minLeadingWidth] property instead. - double? get minLeadingWidth => _data != null ? _data!.minLeadingWidth : _minLeadingWidth; - - /// Overrides the default value of [ListTile.enableFeedback]. - /// - /// This property is obsolete: please use the [data] - /// [ListTileThemeData.enableFeedback] property instead. - bool? get enableFeedback => _data != null ? _data!.enableFeedback : _enableFeedback; - - /// The [data] property of the closest instance of this class that - /// encloses the given context. - /// - /// If there is no enclosing [ListTileTheme] widget, then - /// [ThemeData.listTileTheme] is used (see [Theme.of]). - /// - /// Typical usage is as follows: - /// - /// ```dart - /// ListTileThemeData theme = ListTileTheme.of(context); - /// ``` - static ListTileThemeData of(BuildContext context) { - final ListTileTheme? result = context.dependOnInheritedWidgetOfExactType(); - return result?.data ?? Theme.of(context).listTileTheme; - } - - /// Creates a list tile theme that controls the color and style parameters for - /// [ListTile]s, and merges in the current list tile theme, if any. - /// - /// The [child] argument must not be null. - static Widget merge({ - Key? key, - bool? dense, - ShapeBorder? shape, - ListTileStyle? style, - Color? selectedColor, - Color? iconColor, - Color? textColor, - EdgeInsetsGeometry? contentPadding, - Color? tileColor, - Color? selectedTileColor, - bool? enableFeedback, - double? horizontalTitleGap, - double? minVerticalPadding, - double? minLeadingWidth, - required Widget child, - }) { - assert(child != null); - return Builder( - builder: (BuildContext context) { - final ListTileThemeData parent = ListTileTheme.of(context); - return ListTileTheme( - key: key, - data: ListTileThemeData( - dense: dense ?? parent.dense, - shape: shape ?? parent.shape, - style: style ?? parent.style, - selectedColor: selectedColor ?? parent.selectedColor, - iconColor: iconColor ?? parent.iconColor, - textColor: textColor ?? parent.textColor, - contentPadding: contentPadding ?? parent.contentPadding, - tileColor: tileColor ?? parent.tileColor, - selectedTileColor: selectedTileColor ?? parent.selectedTileColor, - enableFeedback: enableFeedback ?? parent.enableFeedback, - horizontalTitleGap: horizontalTitleGap ?? parent.horizontalTitleGap, - minVerticalPadding: minVerticalPadding ?? parent.minVerticalPadding, - minLeadingWidth: minLeadingWidth ?? parent.minLeadingWidth, - ), - child: child, - ); - }, - ); - } - - @override - Widget wrap(BuildContext context, Widget child) { - return ListTileTheme( - data: ListTileThemeData( - dense: dense, - shape: shape, - style: style, - selectedColor: selectedColor, - iconColor: iconColor, - textColor: textColor, - contentPadding: contentPadding, - tileColor: tileColor, - selectedTileColor: selectedTileColor, - enableFeedback: enableFeedback, - horizontalTitleGap: horizontalTitleGap, - minVerticalPadding: minVerticalPadding, - minLeadingWidth: minLeadingWidth, - ), - child: child, - ); - } - - @override - bool updateShouldNotify(ListTileTheme oldWidget) => data != oldWidget.data; -} - /// A single fixed-height row that typically contains some text as well as /// a leading or trailing icon. /// @@ -1262,7 +788,7 @@ class ListTile extends StatelessWidget { subtitle: subtitleText, trailing: trailingIcon, isDense: _isDenseLayout(theme, tileTheme), - visualDensity: visualDensity ?? theme.visualDensity, + visualDensity: visualDensity ?? tileTheme.visualDensity ?? theme.visualDensity, isThreeLine: isThreeLine, textDirection: textDirection, titleBaselineType: titleStyle.textBaseline!, diff --git a/packages/flutter/lib/src/material/list_tile_theme.dart b/packages/flutter/lib/src/material/list_tile_theme.dart new file mode 100644 index 00000000000..6624424954c --- /dev/null +++ b/packages/flutter/lib/src/material/list_tile_theme.dart @@ -0,0 +1,499 @@ +// 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 'dart:ui' show lerpDouble; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +import 'list_tile.dart'; +import 'material_state.dart'; +import 'theme.dart'; +import 'theme_data.dart'; + +/// Used with [ListTileTheme] to define default property values for +/// descendant [ListTile] widgets, as well as classes that build +/// [ListTile]s, like [CheckboxListTile], [RadioListTile], and +/// [SwitchListTile]. +/// +/// Descendant widgets obtain the current [ListTileThemeData] object +/// using `ListTileTheme.of(context)`. Instances of +/// [ListTileThemeData] can be customized with +/// [ListTileThemeData.copyWith]. +/// +/// A [ListTileThemeData] is often specified as part of the +/// overall [Theme] with [ThemeData.listTileTheme]. +/// +/// All [ListTileThemeData] properties are `null` by default. +/// When a theme property is null, the [ListTile] will provide its own +/// default based on the overall [Theme]'s textTheme and +/// colorScheme. See the individual [ListTile] properties for details. +/// +/// The [Drawer] widget specifies a list tile theme for its children that +/// defines [style] to be [ListTileStyle.drawer]. +/// +/// See also: +/// +/// * [ThemeData], which describes the overall theme information for the +/// application. +@immutable +class ListTileThemeData with Diagnosticable { + /// Creates a [ListTileThemeData]. + const ListTileThemeData ({ + this.dense, + this.shape, + this.style, + this.selectedColor, + this.iconColor, + this.textColor, + this.contentPadding, + this.tileColor, + this.selectedTileColor, + this.horizontalTitleGap, + this.minVerticalPadding, + this.minLeadingWidth, + this.enableFeedback, + this.mouseCursor, + this.visualDensity, + }); + + /// Overrides the default value of [ListTile.dense]. + final bool? dense; + + /// Overrides the default value of [ListTile.shape]. + final ShapeBorder? shape; + + /// Overrides the default value of [ListTile.style]. + final ListTileStyle? style; + + /// Overrides the default value of [ListTile.selectedColor]. + final Color? selectedColor; + + /// Overrides the default value of [ListTile.iconColor]. + final Color? iconColor; + + /// Overrides the default value of [ListTile.textColor]. + final Color? textColor; + + /// Overrides the default value of [ListTile.contentPadding]. + final EdgeInsetsGeometry? contentPadding; + + /// Overrides the default value of [ListTile.tileColor]. + final Color? tileColor; + + /// Overrides the default value of [ListTile.selectedTileColor]. + final Color? selectedTileColor; + + /// Overrides the default value of [ListTile.horizontalTitleGap]. + final double? horizontalTitleGap; + + /// Overrides the default value of [ListTile.minVerticalPadding]. + final double? minVerticalPadding; + + /// Overrides the default value of [ListTile.minLeadingWidth]. + final double? minLeadingWidth; + + /// Overrides the default value of [ListTile.enableFeedback]. + final bool? enableFeedback; + + /// If specified, overrides the default value of [ListTile.mouseCursor]. + final MaterialStateProperty? mouseCursor; + + /// If specified, overrides the default value of [ListTile.visualDensity]. + final VisualDensity? visualDensity; + + /// Creates a copy of this object with the given fields replaced with the + /// new values. + ListTileThemeData copyWith({ + bool? dense, + ShapeBorder? shape, + ListTileStyle? style, + Color? selectedColor, + Color? iconColor, + Color? textColor, + EdgeInsetsGeometry? contentPadding, + Color? tileColor, + Color? selectedTileColor, + double? horizontalTitleGap, + double? minVerticalPadding, + double? minLeadingWidth, + bool? enableFeedback, + MaterialStateProperty? mouseCursor, + bool? isThreeLine, + VisualDensity? visualDensity, + }) { + return ListTileThemeData( + dense: dense ?? this.dense, + shape: shape ?? this.shape, + style: style ?? this.style, + selectedColor: selectedColor ?? this.selectedColor, + iconColor: iconColor ?? this.iconColor, + textColor: textColor ?? this.textColor, + contentPadding: contentPadding ?? this.contentPadding, + tileColor: tileColor ?? this.tileColor, + selectedTileColor: selectedTileColor ?? this.selectedTileColor, + horizontalTitleGap: horizontalTitleGap ?? this.horizontalTitleGap, + minVerticalPadding: minVerticalPadding ?? this.minVerticalPadding, + minLeadingWidth: minLeadingWidth ?? this.minLeadingWidth, + enableFeedback: enableFeedback ?? this.enableFeedback, + mouseCursor: mouseCursor ?? this.mouseCursor, + visualDensity: visualDensity ?? this.visualDensity, + ); + } + + /// Linearly interpolate between ListTileThemeData objects. + static ListTileThemeData? lerp(ListTileThemeData? a, ListTileThemeData? b, double t) { + assert (t != null); + if (a == null && b == null) + return null; + return ListTileThemeData( + dense: t < 0.5 ? a?.dense : b?.dense, + shape: ShapeBorder.lerp(a?.shape, b?.shape, t), + style: t < 0.5 ? a?.style : b?.style, + selectedColor: Color.lerp(a?.selectedColor, b?.selectedColor, t), + iconColor: Color.lerp(a?.iconColor, b?.iconColor, t), + textColor: Color.lerp(a?.textColor, b?.textColor, 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), + horizontalTitleGap: lerpDouble(a?.horizontalTitleGap, b?.horizontalTitleGap, t), + minVerticalPadding: lerpDouble(a?.minVerticalPadding, b?.minVerticalPadding, t), + minLeadingWidth: lerpDouble(a?.minLeadingWidth, b?.minLeadingWidth, t), + enableFeedback: t < 0.5 ? a?.enableFeedback : b?.enableFeedback, + mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor, + visualDensity: t < 0.5 ? a?.visualDensity : b?.visualDensity, + ); + } + + @override + int get hashCode => Object.hash( + dense, + shape, + style, + selectedColor, + iconColor, + textColor, + contentPadding, + tileColor, + selectedTileColor, + horizontalTitleGap, + minVerticalPadding, + minLeadingWidth, + enableFeedback, + mouseCursor, + visualDensity, + ); + + @override + bool operator ==(Object other) { + if (identical(this, other)) + return true; + if (other.runtimeType != runtimeType) + return false; + return other is ListTileThemeData + && other.dense == dense + && other.shape == shape + && other.style == style + && other.selectedColor == selectedColor + && other.iconColor == iconColor + && other.textColor == textColor + && other.contentPadding == contentPadding + && other.tileColor == tileColor + && other.selectedTileColor == selectedTileColor + && other.horizontalTitleGap == horizontalTitleGap + && other.minVerticalPadding == minVerticalPadding + && other.minLeadingWidth == minLeadingWidth + && other.enableFeedback == enableFeedback + && other.mouseCursor == mouseCursor + && other.visualDensity == visualDensity; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('dense', dense, defaultValue: null)); + properties.add(DiagnosticsProperty('shape', shape, defaultValue: null)); + properties.add(EnumProperty('style', style, defaultValue: null)); + properties.add(ColorProperty('selectedColor', selectedColor, defaultValue: null)); + properties.add(ColorProperty('iconColor', iconColor, defaultValue: null)); + properties.add(ColorProperty('textColor', textColor, defaultValue: null)); + properties.add(DiagnosticsProperty('contentPadding', contentPadding, defaultValue: null)); + properties.add(ColorProperty('tileColor', tileColor, defaultValue: null)); + properties.add(ColorProperty('selectedTileColor', selectedTileColor, defaultValue: null)); + properties.add(DoubleProperty('horizontalTitleGap', horizontalTitleGap, defaultValue: null)); + properties.add(DoubleProperty('minVerticalPadding', minVerticalPadding, defaultValue: null)); + properties.add(DoubleProperty('minLeadingWidth', minLeadingWidth, defaultValue: null)); + properties.add(DiagnosticsProperty('enableFeedback', enableFeedback, defaultValue: null)); + properties.add(DiagnosticsProperty>('mouseCursor', mouseCursor, defaultValue: null)); + properties.add(DiagnosticsProperty('visualDensity', visualDensity, defaultValue: null)); + } +} + +/// An inherited widget that defines color and style parameters for [ListTile]s +/// in this widget's subtree. +/// +/// Values specified here are used for [ListTile] properties that are not given +/// an explicit non-null value. +/// +/// The [Drawer] widget specifies a tile theme for its children which sets +/// [style] to [ListTileStyle.drawer]. +class ListTileTheme extends InheritedTheme { + /// Creates a list tile theme that defines the color and style parameters for + /// descendant [ListTile]s. + /// + /// Only the [data] parameter should be used. The other parameters are + /// redundant (are now obsolete) and will be deprecated in a future update. + const ListTileTheme({ + Key? key, + ListTileThemeData? data, + bool? dense, + ShapeBorder? shape, + ListTileStyle? style, + Color? selectedColor, + Color? iconColor, + Color? textColor, + EdgeInsetsGeometry? contentPadding, + Color? tileColor, + Color? selectedTileColor, + bool? enableFeedback, + MaterialStateProperty? mouseCursor, + double? horizontalTitleGap, + double? minVerticalPadding, + double? minLeadingWidth, + required Widget child, + }) : assert( + data == null || + (shape ?? + selectedColor ?? + iconColor ?? + textColor ?? + contentPadding ?? + tileColor ?? + selectedTileColor ?? + enableFeedback ?? + mouseCursor ?? + horizontalTitleGap ?? + minVerticalPadding ?? + minLeadingWidth) == null), + _data = data, + _dense = dense, + _shape = shape, + _style = style, + _selectedColor = selectedColor, + _iconColor = iconColor, + _textColor = textColor, + _contentPadding = contentPadding, + _tileColor = tileColor, + _selectedTileColor = selectedTileColor, + _enableFeedback = enableFeedback, + _mouseCursor = mouseCursor, + _horizontalTitleGap = horizontalTitleGap, + _minVerticalPadding = minVerticalPadding, + _minLeadingWidth = minLeadingWidth, + super(key: key, child: child); + + final ListTileThemeData? _data; + final bool? _dense; + final ShapeBorder? _shape; + final ListTileStyle? _style; + final Color? _selectedColor; + final Color? _iconColor; + final Color? _textColor; + final EdgeInsetsGeometry? _contentPadding; + final Color? _tileColor; + final Color? _selectedTileColor; + final double? _horizontalTitleGap; + final double? _minVerticalPadding; + final double? _minLeadingWidth; + final bool? _enableFeedback; + final MaterialStateProperty? _mouseCursor; + + /// The configuration of this theme. + ListTileThemeData get data { + return _data ?? ListTileThemeData( + dense: _dense, + shape: _shape, + style: _style, + selectedColor: _selectedColor, + iconColor: _iconColor, + textColor: _textColor, + contentPadding: _contentPadding, + tileColor: _tileColor, + selectedTileColor: _selectedTileColor, + enableFeedback: _enableFeedback, + mouseCursor: _mouseCursor, + horizontalTitleGap: _horizontalTitleGap, + minVerticalPadding: _minVerticalPadding, + minLeadingWidth: _minLeadingWidth, + ); + } + + /// Overrides the default value of [ListTile.dense]. + /// + /// This property is obsolete: please use the [data] + /// [ListTileThemeData.dense] property instead. + bool? get dense => _data != null ? _data!.dense : _dense; + + /// Overrides the default value of [ListTile.shape]. + /// + /// This property is obsolete: please use the [data] + /// [ListTileThemeData.shape] property instead. + ShapeBorder? get shape => _data != null ? _data!.shape : _shape; + + /// Overrides the default value of [ListTile.style]. + /// + /// This property is obsolete: please use the [data] + /// [ListTileThemeData.style] property instead. + ListTileStyle? get style => _data != null ? _data!.style : _style; + + /// Overrides the default value of [ListTile.selectedColor]. + /// + /// This property is obsolete: please use the [data] + /// [ListTileThemeData.selectedColor] property instead. + Color? get selectedColor => _data != null ? _data!.selectedColor : _selectedColor; + + /// Overrides the default value of [ListTile.iconColor]. + /// + /// This property is obsolete: please use the [data] + /// [ListTileThemeData.iconColor] property instead. + Color? get iconColor => _data != null ? _data!.iconColor : _iconColor; + + /// Overrides the default value of [ListTile.textColor]. + /// + /// This property is obsolete: please use the [data] + /// [ListTileThemeData.textColor] property instead. + Color? get textColor => _data != null ? _data!.textColor : _textColor; + + /// Overrides the default value of [ListTile.contentPadding]. + /// + /// This property is obsolete: please use the [data] + /// [ListTileThemeData.contentPadding] property instead. + EdgeInsetsGeometry? get contentPadding => _data != null ? _data!.contentPadding : _contentPadding; + + /// Overrides the default value of [ListTile.tileColor]. + /// + /// This property is obsolete: please use the [data] + /// [ListTileThemeData.tileColor] property instead. + Color? get tileColor => _data != null ? _data!.tileColor : _tileColor; + + /// Overrides the default value of [ListTile.selectedTileColor]. + /// + /// This property is obsolete: please use the [data] + /// [ListTileThemeData.selectedTileColor] property instead. + Color? get selectedTileColor => _data != null ? _data!.selectedTileColor : _selectedTileColor; + + /// Overrides the default value of [ListTile.horizontalTitleGap]. + /// + /// This property is obsolete: please use the [data] + /// [ListTileThemeData.horizontalTitleGap] property instead. + double? get horizontalTitleGap => _data != null ? _data!.horizontalTitleGap : _horizontalTitleGap; + + /// Overrides the default value of [ListTile.minVerticalPadding]. + /// + /// This property is obsolete: please use the [data] + /// [ListTileThemeData.minVerticalPadding] property instead. + double? get minVerticalPadding => _data != null ? _data!.minVerticalPadding : _minVerticalPadding; + + /// Overrides the default value of [ListTile.minLeadingWidth]. + /// + /// This property is obsolete: please use the [data] + /// [ListTileThemeData.minLeadingWidth] property instead. + double? get minLeadingWidth => _data != null ? _data!.minLeadingWidth : _minLeadingWidth; + + /// Overrides the default value of [ListTile.enableFeedback]. + /// + /// This property is obsolete: please use the [data] + /// [ListTileThemeData.enableFeedback] property instead. + bool? get enableFeedback => _data != null ? _data!.enableFeedback : _enableFeedback; + + /// The [data] property of the closest instance of this class that + /// encloses the given context. + /// + /// If there is no enclosing [ListTileTheme] widget, then + /// [ThemeData.listTileTheme] is used (see [Theme.of]). + /// + /// Typical usage is as follows: + /// + /// ```dart + /// ListTileThemeData theme = ListTileTheme.of(context); + /// ``` + static ListTileThemeData of(BuildContext context) { + final ListTileTheme? result = context.dependOnInheritedWidgetOfExactType(); + return result?.data ?? Theme.of(context).listTileTheme; + } + + /// Creates a list tile theme that controls the color and style parameters for + /// [ListTile]s, and merges in the current list tile theme, if any. + /// + /// The [child] argument must not be null. + static Widget merge({ + Key? key, + bool? dense, + ShapeBorder? shape, + ListTileStyle? style, + Color? selectedColor, + Color? iconColor, + Color? textColor, + EdgeInsetsGeometry? contentPadding, + Color? tileColor, + Color? selectedTileColor, + bool? enableFeedback, + double? horizontalTitleGap, + double? minVerticalPadding, + double? minLeadingWidth, + required Widget child, + }) { + assert(child != null); + return Builder( + builder: (BuildContext context) { + final ListTileThemeData parent = ListTileTheme.of(context); + return ListTileTheme( + key: key, + data: ListTileThemeData( + dense: dense ?? parent.dense, + shape: shape ?? parent.shape, + style: style ?? parent.style, + selectedColor: selectedColor ?? parent.selectedColor, + iconColor: iconColor ?? parent.iconColor, + textColor: textColor ?? parent.textColor, + contentPadding: contentPadding ?? parent.contentPadding, + tileColor: tileColor ?? parent.tileColor, + selectedTileColor: selectedTileColor ?? parent.selectedTileColor, + enableFeedback: enableFeedback ?? parent.enableFeedback, + horizontalTitleGap: horizontalTitleGap ?? parent.horizontalTitleGap, + minVerticalPadding: minVerticalPadding ?? parent.minVerticalPadding, + minLeadingWidth: minLeadingWidth ?? parent.minLeadingWidth, + ), + child: child, + ); + }, + ); + } + + @override + Widget wrap(BuildContext context, Widget child) { + return ListTileTheme( + data: ListTileThemeData( + dense: dense, + shape: shape, + style: style, + selectedColor: selectedColor, + iconColor: iconColor, + textColor: textColor, + contentPadding: contentPadding, + tileColor: tileColor, + selectedTileColor: selectedTileColor, + enableFeedback: enableFeedback, + horizontalTitleGap: horizontalTitleGap, + minVerticalPadding: minVerticalPadding, + minLeadingWidth: minLeadingWidth, + ), + child: child, + ); + } + + @override + bool updateShouldNotify(ListTileTheme oldWidget) => data != oldWidget.data; +} diff --git a/packages/flutter/lib/src/material/radio_list_tile.dart b/packages/flutter/lib/src/material/radio_list_tile.dart index 6ff7adaf848..9459a69a90a 100644 --- a/packages/flutter/lib/src/material/radio_list_tile.dart +++ b/packages/flutter/lib/src/material/radio_list_tile.dart @@ -5,6 +5,7 @@ import 'package:flutter/widgets.dart'; import 'list_tile.dart'; +import 'list_tile_theme.dart'; import 'radio.dart'; import 'theme.dart'; import 'theme_data.dart'; diff --git a/packages/flutter/lib/src/material/switch_list_tile.dart b/packages/flutter/lib/src/material/switch_list_tile.dart index 14b67aaefdd..609ee903757 100644 --- a/packages/flutter/lib/src/material/switch_list_tile.dart +++ b/packages/flutter/lib/src/material/switch_list_tile.dart @@ -5,6 +5,7 @@ import 'package:flutter/widgets.dart'; import 'list_tile.dart'; +import 'list_tile_theme.dart'; import 'switch.dart'; import 'theme.dart'; import 'theme_data.dart'; diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart index 2cbf5aa906f..6c19b057539 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -31,6 +31,7 @@ import 'ink_splash.dart'; import 'ink_well.dart' show InteractiveInkFeatureFactory; import 'input_decorator.dart'; import 'list_tile.dart'; +import 'list_tile_theme.dart'; import 'navigation_bar_theme.dart'; import 'navigation_rail_theme.dart'; import 'outlined_button_theme.dart'; diff --git a/packages/flutter/test/material/list_tile_test.dart b/packages/flutter/test/material/list_tile_test.dart index be7e772626a..c583606c523 100644 --- a/packages/flutter/test/material/list_tile_test.dart +++ b/packages/flutter/test/material/list_tile_test.dart @@ -51,133 +51,6 @@ class TestTextState extends State { } void main() { - test('ListTileThemeData copyWith, ==, hashCode basics', () { - expect(const ListTileThemeData(), const ListTileThemeData().copyWith()); - expect(const ListTileThemeData().hashCode, const ListTileThemeData().copyWith().hashCode); - }); - - test('ListTileThemeData defaults', () { - const ListTileThemeData themeData = ListTileThemeData(); - expect(themeData.dense, null); - expect(themeData.shape, null); - expect(themeData.style, null); - expect(themeData.selectedColor, null); - expect(themeData.iconColor, null); - expect(themeData.textColor, null); - expect(themeData.contentPadding, null); - expect(themeData.tileColor, null); - expect(themeData.selectedTileColor, null); - expect(themeData.horizontalTitleGap, null); - expect(themeData.minVerticalPadding, null); - expect(themeData.minLeadingWidth, null); - expect(themeData.enableFeedback, null); - expect(themeData.mouseCursor, null); - }); - - testWidgets('Default ListTileThemeData debugFillProperties', (WidgetTester tester) async { - final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); - const ListTileThemeData().debugFillProperties(builder); - - final List description = builder.properties - .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) - .map((DiagnosticsNode node) => node.toString()) - .toList(); - - expect(description, []); - }); - - testWidgets('ListTileThemeData implements debugFillProperties', (WidgetTester tester) async { - final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); - const ListTileThemeData( - dense: true, - shape: StadiumBorder(), - style: ListTileStyle.drawer, - selectedColor: Color(0x00000001), - iconColor: Color(0x00000002), - textColor: Color(0x00000003), - contentPadding: EdgeInsets.all(100), - tileColor: Color(0x00000004), - selectedTileColor: Color(0x00000005), - horizontalTitleGap: 200, - minVerticalPadding: 300, - minLeadingWidth: 400, - enableFeedback: true, - mouseCursor: MaterialStateMouseCursor.clickable, - ).debugFillProperties(builder); - - final List description = builder.properties - .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) - .map((DiagnosticsNode node) => node.toString()) - .toList(); - - expect(description, [ - 'dense: true', - 'shape: StadiumBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none))', - 'style: drawer', - 'selectedColor: Color(0x00000001)', - 'iconColor: Color(0x00000002)', - 'textColor: Color(0x00000003)', - 'contentPadding: EdgeInsets.all(100.0)', - 'tileColor: Color(0x00000004)', - 'selectedTileColor: Color(0x00000005)', - 'horizontalTitleGap: 200.0', - 'minVerticalPadding: 300.0', - 'minLeadingWidth: 400.0', - 'enableFeedback: true', - 'mouseCursor: MaterialStateMouseCursor(clickable)', - ]); - }); - - testWidgets('ListTileTheme backwards compatibility constructor', (WidgetTester tester) async { - late ListTileThemeData theme; - - await tester.pumpWidget( - MaterialApp( - home: Material( - child: ListTileTheme( - dense: true, - shape: const StadiumBorder(), - style: ListTileStyle.drawer, - selectedColor: const Color(0x00000001), - iconColor: const Color(0x00000002), - textColor: const Color(0x00000003), - contentPadding: const EdgeInsets.all(100), - tileColor: const Color(0x00000004), - selectedTileColor: const Color(0x00000005), - horizontalTitleGap: 200, - minVerticalPadding: 300, - minLeadingWidth: 400, - enableFeedback: true, - mouseCursor: MaterialStateMouseCursor.clickable, - child: Center( - child: Builder( - builder: (BuildContext context) { - theme = ListTileTheme.of(context); - return const Placeholder(); - }, - ), - ), - ), - ), - ), - ); - - expect(theme.dense, true); - expect(theme.shape, const StadiumBorder()); - expect(theme.style, ListTileStyle.drawer); - expect(theme.selectedColor, const Color(0x00000001)); - expect(theme.iconColor, const Color(0x00000002)); - expect(theme.textColor, const Color(0x00000003)); - expect(theme.contentPadding, const EdgeInsets.all(100)); - expect(theme.tileColor, const Color(0x00000004)); - expect(theme.selectedTileColor, const Color(0x00000005)); - expect(theme.horizontalTitleGap, 200); - expect(theme.minVerticalPadding, 300); - expect(theme.minLeadingWidth, 400); - expect(theme.enableFeedback, true); - expect(theme.mouseCursor, MaterialStateMouseCursor.clickable); - }); - testWidgets('ListTile geometry (LTR)', (WidgetTester tester) async { // See https://material.io/go/design-lists @@ -403,126 +276,6 @@ void main() { expect(callCount, 1); }); - testWidgets('ListTileTheme', (WidgetTester tester) async { - final Key titleKey = UniqueKey(); - final Key subtitleKey = UniqueKey(); - final Key leadingKey = UniqueKey(); - final Key trailingKey = UniqueKey(); - late ThemeData theme; - - Widget buildFrame({ - bool enabled = true, - bool dense = false, - bool selected = false, - ShapeBorder? shape, - Color? selectedColor, - Color? iconColor, - Color? textColor, - }) { - return MaterialApp( - home: Material( - child: Center( - child: ListTileTheme( - data: ListTileThemeData( - dense: dense, - shape: shape, - selectedColor: selectedColor, - iconColor: iconColor, - textColor: textColor, - mouseCursor: MaterialStateProperty.resolveWith((Set states) { - if (states.contains(MaterialState.disabled)) { - return SystemMouseCursors.forbidden; - } - - return SystemMouseCursors.click; - }), - ), - child: Builder( - builder: (BuildContext context) { - theme = Theme.of(context); - return ListTile( - enabled: enabled, - selected: selected, - leading: TestIcon(key: leadingKey), - trailing: TestIcon(key: trailingKey), - title: TestText('title', key: titleKey), - subtitle: TestText('subtitle', key: subtitleKey), - ); - }, - ), - ), - ), - ), - ); - } - - const Color green = Color(0xFF00FF00); - const Color red = Color(0xFFFF0000); - const ShapeBorder roundedShape = RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(4.0)), - ); - - Color iconColor(Key key) => tester.state(find.byKey(key)).iconTheme.color!; - Color textColor(Key key) => tester.state(find.byKey(key)).textStyle.color!; - ShapeBorder inkWellBorder() => tester.widget(find.descendant(of: find.byType(ListTile), matching: find.byType(InkWell))).customBorder!; - - // A selected ListTile's leading, trailing, and text get the primary color by default - await tester.pumpWidget(buildFrame(selected: true)); - await tester.pump(const Duration(milliseconds: 300)); // DefaultTextStyle changes animate - expect(iconColor(leadingKey), theme.primaryColor); - expect(iconColor(trailingKey), theme.primaryColor); - expect(textColor(titleKey), theme.primaryColor); - expect(textColor(subtitleKey), theme.primaryColor); - - // A selected ListTile's leading, trailing, and text get the ListTileTheme's selectedColor - await tester.pumpWidget(buildFrame(selected: true, selectedColor: green)); - await tester.pump(const Duration(milliseconds: 300)); // DefaultTextStyle changes animate - expect(iconColor(leadingKey), green); - expect(iconColor(trailingKey), green); - expect(textColor(titleKey), green); - expect(textColor(subtitleKey), green); - - // An unselected ListTile's leading and trailing get the ListTileTheme's iconColor - // An unselected ListTile's title texts get the ListTileTheme's textColor - await tester.pumpWidget(buildFrame(iconColor: red, textColor: green)); - await tester.pump(const Duration(milliseconds: 300)); // DefaultTextStyle changes animate - expect(iconColor(leadingKey), red); - expect(iconColor(trailingKey), red); - expect(textColor(titleKey), green); - expect(textColor(subtitleKey), green); - - // If the item is disabled it's rendered with the theme's disabled color. - await tester.pumpWidget(buildFrame(enabled: false)); - await tester.pump(const Duration(milliseconds: 300)); // DefaultTextStyle changes animate - expect(iconColor(leadingKey), theme.disabledColor); - expect(iconColor(trailingKey), theme.disabledColor); - expect(textColor(titleKey), theme.disabledColor); - expect(textColor(subtitleKey), theme.disabledColor); - - // If the item is disabled it's rendered with the theme's disabled color. - // Even if it's selected. - await tester.pumpWidget(buildFrame(enabled: false, selected: true)); - await tester.pump(const Duration(milliseconds: 300)); // DefaultTextStyle changes animate - expect(iconColor(leadingKey), theme.disabledColor); - expect(iconColor(trailingKey), theme.disabledColor); - expect(textColor(titleKey), theme.disabledColor); - expect(textColor(subtitleKey), theme.disabledColor); - - // A selected ListTile's InkWell gets the ListTileTheme's shape - await tester.pumpWidget(buildFrame(selected: true, shape: roundedShape)); - expect(inkWellBorder(), roundedShape); - - // Cursor updates when hovering disabled ListTile - await tester.pumpWidget(buildFrame(enabled: false)); - final Offset listTile = tester.getCenter(find.byKey(titleKey)); - final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); - await gesture.addPointer(); - addTearDown(gesture.removePointer); - await gesture.moveTo(listTile); - await tester.pumpAndSettle(); - expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden); - }); - testWidgets('ListTile semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); @@ -686,7 +439,7 @@ void main() { expect(right('L'), 790.0); // 800 - contentPadding.start }); - testWidgets('ListTileTheme wide leading Widget', (WidgetTester tester) async { + testWidgets('ListTile wide leading Widget', (WidgetTester tester) async { const Key leadingKey = ValueKey('L'); Widget buildFrame(double leadingWidth, TextDirection textDirection) { @@ -1870,88 +1623,6 @@ void main() { expect(find.byType(Material), paints..path(color: defaultColor)); }); - testWidgets("ListTile respects ListTileTheme's tileColor & selectedTileColor", (WidgetTester tester) async { - late ListTileThemeData theme; - bool isSelected = false; - - await tester.pumpWidget( - MaterialApp( - home: Material( - child: ListTileTheme( - data: ListTileThemeData( - tileColor: Colors.green.shade500, - selectedTileColor: Colors.red.shade500, - ), - child: Center( - child: StatefulBuilder( - builder: (BuildContext context, StateSetter setState) { - theme = ListTileTheme.of(context); - return ListTile( - selected: isSelected, - onTap: () { - setState(()=> isSelected = !isSelected); - }, - title: const Text('Title'), - ); - }, - ), - ), - ), - ), - ), - ); - - expect(find.byType(Material), paints..path(color: theme.tileColor)); - - // Tap on tile to change isSelected. - await tester.tap(find.byType(ListTile)); - await tester.pumpAndSettle(); - - expect(find.byType(Material), paints..path(color: theme.selectedTileColor)); - }); - - testWidgets("ListTileTheme's tileColor & selectedTileColor are overridden by ListTile properties", (WidgetTester tester) async { - bool isSelected = false; - final Color tileColor = Colors.green.shade500; - final Color selectedTileColor = Colors.red.shade500; - - await tester.pumpWidget( - MaterialApp( - home: Material( - child: ListTileTheme( - data: const ListTileThemeData( - selectedTileColor: Colors.green, - tileColor: Colors.red, - ), - child: Center( - child: StatefulBuilder( - builder: (BuildContext context, StateSetter setState) { - return ListTile( - tileColor: tileColor, - selectedTileColor: selectedTileColor, - selected: isSelected, - onTap: () { - setState(()=> isSelected = !isSelected); - }, - title: const Text('Title'), - ); - }, - ), - ), - ), - ), - ), - ); - - expect(find.byType(Material), paints..path(color: tileColor)); - - // Tap on tile to change isSelected. - await tester.tap(find.byType(ListTile)); - await tester.pumpAndSettle(); - - expect(find.byType(Material), paints..path(color: selectedTileColor)); - }); - testWidgets('ListTile layout at zero size', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/66636 const Key key = Key('key'); @@ -2408,66 +2079,6 @@ void main() { expect(textColor(trailingKey), theme.disabledColor); }); - testWidgets('ListTileTheme colors are applied to leading and trailing text widgets', (WidgetTester tester) async { - final Key leadingKey = UniqueKey(); - final Key trailingKey = UniqueKey(); - - const Color selectedColor = Colors.orange; - const Color defaultColor = Colors.black; - - late ThemeData theme; - Widget buildFrame({ - bool enabled = true, - bool selected = false, - }) { - return MaterialApp( - home: Material( - child: Center( - child: ListTileTheme( - data: const ListTileThemeData( - selectedColor: selectedColor, - textColor: defaultColor, - ), - child: Builder( - builder: (BuildContext context) { - theme = Theme.of(context); - return ListTile( - enabled: enabled, - selected: selected, - leading: TestText('leading', key: leadingKey), - title: const TestText('title'), - trailing: TestText('trailing', key: trailingKey), - ); - }, - ), - ), - ), - ), - ); - } - - Color textColor(Key key) => tester.state(find.byKey(key)).textStyle.color!; - - await tester.pumpWidget(buildFrame()); - // Enabled color should use ListTileTheme.textColor. - expect(textColor(leadingKey), defaultColor); - expect(textColor(trailingKey), defaultColor); - - await tester.pumpWidget(buildFrame(selected: true)); - // Wait for text color to animate. - await tester.pumpAndSettle(); - // Selected color should use ListTileTheme.selectedColor. - expect(textColor(leadingKey), selectedColor); - expect(textColor(trailingKey), selectedColor); - - await tester.pumpWidget(buildFrame(enabled: false)); - // Wait for text color to animate. - await tester.pumpAndSettle(); - // Disabled color should be ThemeData.disabledColor. - expect(textColor(leadingKey), theme.disabledColor); - expect(textColor(trailingKey), theme.disabledColor); - }); - testWidgets('selected, enabled ListTile default icon color, light and dark themes', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/pull/77004 diff --git a/packages/flutter/test/material/list_tile_theme_test.dart b/packages/flutter/test/material/list_tile_theme_test.dart new file mode 100644 index 00000000000..a400c4af6ae --- /dev/null +++ b/packages/flutter/test/material/list_tile_theme_test.dart @@ -0,0 +1,448 @@ +// 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/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../rendering/mock_canvas.dart'; + +class TestIcon extends StatefulWidget { + const TestIcon({ Key? key }) : super(key: key); + + @override + TestIconState createState() => TestIconState(); +} + +class TestIconState extends State { + late IconThemeData iconTheme; + + @override + Widget build(BuildContext context) { + iconTheme = IconTheme.of(context); + return const Icon(Icons.add); + } +} + +class TestText extends StatefulWidget { + const TestText(this.text, { Key? key }) : super(key: key); + + final String text; + + @override + TestTextState createState() => TestTextState(); +} + +class TestTextState extends State { + late TextStyle textStyle; + + @override + Widget build(BuildContext context) { + textStyle = DefaultTextStyle.of(context).style; + return Text(widget.text); + } +} + +void main() { + test('ListTileThemeData copyWith, ==, hashCode basics', () { + expect(const ListTileThemeData(), const ListTileThemeData().copyWith()); + expect(const ListTileThemeData().hashCode, const ListTileThemeData().copyWith().hashCode); + }); + + test('ListTileThemeData defaults', () { + const ListTileThemeData themeData = ListTileThemeData(); + expect(themeData.dense, null); + expect(themeData.shape, null); + expect(themeData.style, null); + expect(themeData.selectedColor, null); + expect(themeData.iconColor, null); + expect(themeData.textColor, null); + expect(themeData.contentPadding, null); + expect(themeData.tileColor, null); + expect(themeData.selectedTileColor, null); + expect(themeData.horizontalTitleGap, null); + expect(themeData.minVerticalPadding, null); + expect(themeData.minLeadingWidth, null); + expect(themeData.enableFeedback, null); + expect(themeData.mouseCursor, null); + expect(themeData.visualDensity, null); + }); + + testWidgets('Default ListTileThemeData debugFillProperties', (WidgetTester tester) async { + final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); + const ListTileThemeData().debugFillProperties(builder); + + final List description = builder.properties + .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) + .map((DiagnosticsNode node) => node.toString()) + .toList(); + + expect(description, []); + }); + + testWidgets('ListTileThemeData implements debugFillProperties', (WidgetTester tester) async { + final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); + const ListTileThemeData( + dense: true, + shape: StadiumBorder(), + style: ListTileStyle.drawer, + selectedColor: Color(0x00000001), + iconColor: Color(0x00000002), + textColor: Color(0x00000003), + contentPadding: EdgeInsets.all(100), + tileColor: Color(0x00000004), + selectedTileColor: Color(0x00000005), + horizontalTitleGap: 200, + minVerticalPadding: 300, + minLeadingWidth: 400, + enableFeedback: true, + mouseCursor: MaterialStateMouseCursor.clickable, + visualDensity: VisualDensity.comfortable, + ).debugFillProperties(builder); + + final List description = builder.properties + .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) + .map((DiagnosticsNode node) => node.toString()) + .toList(); + + expect(description[0], 'dense: true'); + expect(description[1], 'shape: StadiumBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none))'); + expect(description[2], 'style: drawer'); + expect(description[3], 'selectedColor: Color(0x00000001)'); + expect(description[4], 'iconColor: Color(0x00000002)'); + expect(description[5], 'textColor: Color(0x00000003)'); + expect(description[6], 'contentPadding: EdgeInsets.all(100.0)'); + expect(description[7], 'tileColor: Color(0x00000004)'); + expect(description[8], 'selectedTileColor: Color(0x00000005)'); + expect(description[9], 'horizontalTitleGap: 200.0'); + expect(description[10], 'minVerticalPadding: 300.0'); + expect(description[11], 'minLeadingWidth: 400.0'); + expect(description[12], 'enableFeedback: true'); + expect(description[13], 'mouseCursor: MaterialStateMouseCursor(clickable)'); + expect( + description[14], + equalsIgnoringHashCodes('visualDensity: VisualDensity#00000(h: -1.0, v: -1.0)(horizontal: -1.0, vertical: -1.0)'), + ); + }); + + testWidgets('ListTileTheme backwards compatibility constructor', (WidgetTester tester) async { + late ListTileThemeData theme; + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: ListTileTheme( + dense: true, + shape: const StadiumBorder(), + style: ListTileStyle.drawer, + selectedColor: const Color(0x00000001), + iconColor: const Color(0x00000002), + textColor: const Color(0x00000003), + contentPadding: const EdgeInsets.all(100), + tileColor: const Color(0x00000004), + selectedTileColor: const Color(0x00000005), + horizontalTitleGap: 200, + minVerticalPadding: 300, + minLeadingWidth: 400, + enableFeedback: true, + mouseCursor: MaterialStateMouseCursor.clickable, + child: Center( + child: Builder( + builder: (BuildContext context) { + theme = ListTileTheme.of(context); + return const Placeholder(); + }, + ), + ), + ), + ), + ), + ); + + expect(theme.dense, true); + expect(theme.shape, const StadiumBorder()); + expect(theme.style, ListTileStyle.drawer); + expect(theme.selectedColor, const Color(0x00000001)); + expect(theme.iconColor, const Color(0x00000002)); + expect(theme.textColor, const Color(0x00000003)); + expect(theme.contentPadding, const EdgeInsets.all(100)); + expect(theme.tileColor, const Color(0x00000004)); + expect(theme.selectedTileColor, const Color(0x00000005)); + expect(theme.horizontalTitleGap, 200); + expect(theme.minVerticalPadding, 300); + expect(theme.minLeadingWidth, 400); + expect(theme.enableFeedback, true); + expect(theme.mouseCursor, MaterialStateMouseCursor.clickable); + }); + + testWidgets('ListTileTheme', (WidgetTester tester) async { + final Key listTileKey = UniqueKey(); + final Key titleKey = UniqueKey(); + final Key subtitleKey = UniqueKey(); + final Key leadingKey = UniqueKey(); + final Key trailingKey = UniqueKey(); + late ThemeData theme; + + Widget buildFrame({ + bool enabled = true, + bool dense = false, + bool selected = false, + ShapeBorder? shape, + Color? selectedColor, + Color? iconColor, + Color? textColor, + }) { + return MaterialApp( + home: Material( + child: Center( + child: ListTileTheme( + data: ListTileThemeData( + dense: dense, + shape: shape, + selectedColor: selectedColor, + iconColor: iconColor, + textColor: textColor, + mouseCursor: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return SystemMouseCursors.forbidden; + } + + return SystemMouseCursors.click; + }), + visualDensity: VisualDensity.compact, + ), + child: Builder( + builder: (BuildContext context) { + theme = Theme.of(context); + return ListTile( + key: listTileKey, + enabled: enabled, + selected: selected, + leading: TestIcon(key: leadingKey), + trailing: TestIcon(key: trailingKey), + title: TestText('title', key: titleKey), + subtitle: TestText('subtitle', key: subtitleKey), + ); + }, + ), + ), + ), + ), + ); + } + + const Color green = Color(0xFF00FF00); + const Color red = Color(0xFFFF0000); + const ShapeBorder roundedShape = RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(4.0)), + ); + + Color iconColor(Key key) => tester.state(find.byKey(key)).iconTheme.color!; + Color textColor(Key key) => tester.state(find.byKey(key)).textStyle.color!; + ShapeBorder inkWellBorder() => tester.widget(find.descendant(of: find.byType(ListTile), matching: find.byType(InkWell))).customBorder!; + + // A selected ListTile's leading, trailing, and text get the primary color by default + await tester.pumpWidget(buildFrame(selected: true)); + await tester.pump(const Duration(milliseconds: 300)); // DefaultTextStyle changes animate + expect(iconColor(leadingKey), theme.primaryColor); + expect(iconColor(trailingKey), theme.primaryColor); + expect(textColor(titleKey), theme.primaryColor); + expect(textColor(subtitleKey), theme.primaryColor); + + // A selected ListTile's leading, trailing, and text get the ListTileTheme's selectedColor + await tester.pumpWidget(buildFrame(selected: true, selectedColor: green)); + await tester.pump(const Duration(milliseconds: 300)); // DefaultTextStyle changes animate + expect(iconColor(leadingKey), green); + expect(iconColor(trailingKey), green); + expect(textColor(titleKey), green); + expect(textColor(subtitleKey), green); + + // An unselected ListTile's leading and trailing get the ListTileTheme's iconColor + // An unselected ListTile's title texts get the ListTileTheme's textColor + await tester.pumpWidget(buildFrame(iconColor: red, textColor: green)); + await tester.pump(const Duration(milliseconds: 300)); // DefaultTextStyle changes animate + expect(iconColor(leadingKey), red); + expect(iconColor(trailingKey), red); + expect(textColor(titleKey), green); + expect(textColor(subtitleKey), green); + + // If the item is disabled it's rendered with the theme's disabled color. + await tester.pumpWidget(buildFrame(enabled: false)); + await tester.pump(const Duration(milliseconds: 300)); // DefaultTextStyle changes animate + expect(iconColor(leadingKey), theme.disabledColor); + expect(iconColor(trailingKey), theme.disabledColor); + expect(textColor(titleKey), theme.disabledColor); + expect(textColor(subtitleKey), theme.disabledColor); + + // If the item is disabled it's rendered with the theme's disabled color. + // Even if it's selected. + await tester.pumpWidget(buildFrame(enabled: false, selected: true)); + await tester.pump(const Duration(milliseconds: 300)); // DefaultTextStyle changes animate + expect(iconColor(leadingKey), theme.disabledColor); + expect(iconColor(trailingKey), theme.disabledColor); + expect(textColor(titleKey), theme.disabledColor); + expect(textColor(subtitleKey), theme.disabledColor); + + // A selected ListTile's InkWell gets the ListTileTheme's shape + await tester.pumpWidget(buildFrame(selected: true, shape: roundedShape)); + expect(inkWellBorder(), roundedShape); + + // Cursor updates when hovering disabled ListTile + await tester.pumpWidget(buildFrame(enabled: false)); + final Offset listTile = tester.getCenter(find.byKey(titleKey)); + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(); + addTearDown(gesture.removePointer); + await gesture.moveTo(listTile); + await tester.pumpAndSettle(); + expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden); + + // VisualDensity is respected + final RenderBox box = tester.renderObject(find.byKey(listTileKey)); + expect(box.size, equals(const Size(800, 64.0))); + }); + + testWidgets('ListTileTheme colors are applied to leading and trailing text widgets', (WidgetTester tester) async { + final Key leadingKey = UniqueKey(); + final Key trailingKey = UniqueKey(); + + const Color selectedColor = Colors.orange; + const Color defaultColor = Colors.black; + + late ThemeData theme; + Widget buildFrame({ + bool enabled = true, + bool selected = false, + }) { + return MaterialApp( + home: Material( + child: Center( + child: ListTileTheme( + data: const ListTileThemeData( + selectedColor: selectedColor, + textColor: defaultColor, + ), + child: Builder( + builder: (BuildContext context) { + theme = Theme.of(context); + return ListTile( + enabled: enabled, + selected: selected, + leading: TestText('leading', key: leadingKey), + title: const TestText('title'), + trailing: TestText('trailing', key: trailingKey), + ); + }, + ), + ), + ), + ), + ); + } + + Color textColor(Key key) => tester.state(find.byKey(key)).textStyle.color!; + + await tester.pumpWidget(buildFrame()); + // Enabled color should use ListTileTheme.textColor. + expect(textColor(leadingKey), defaultColor); + expect(textColor(trailingKey), defaultColor); + + await tester.pumpWidget(buildFrame(selected: true)); + // Wait for text color to animate. + await tester.pumpAndSettle(); + // Selected color should use ListTileTheme.selectedColor. + expect(textColor(leadingKey), selectedColor); + expect(textColor(trailingKey), selectedColor); + + await tester.pumpWidget(buildFrame(enabled: false)); + // Wait for text color to animate. + await tester.pumpAndSettle(); + // Disabled color should be ThemeData.disabledColor. + expect(textColor(leadingKey), theme.disabledColor); + expect(textColor(trailingKey), theme.disabledColor); + }); + + testWidgets("ListTile respects ListTileTheme's tileColor & selectedTileColor", (WidgetTester tester) async { + late ListTileThemeData theme; + bool isSelected = false; + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: ListTileTheme( + data: ListTileThemeData( + tileColor: Colors.green.shade500, + selectedTileColor: Colors.red.shade500, + ), + child: Center( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + theme = ListTileTheme.of(context); + return ListTile( + selected: isSelected, + onTap: () { + setState(()=> isSelected = !isSelected); + }, + title: const Text('Title'), + ); + }, + ), + ), + ), + ), + ), + ); + + expect(find.byType(Material), paints..path(color: theme.tileColor)); + + // Tap on tile to change isSelected. + await tester.tap(find.byType(ListTile)); + await tester.pumpAndSettle(); + + expect(find.byType(Material), paints..path(color: theme.selectedTileColor)); + }); + + testWidgets("ListTileTheme's tileColor & selectedTileColor are overridden by ListTile properties", (WidgetTester tester) async { + bool isSelected = false; + final Color tileColor = Colors.green.shade500; + final Color selectedTileColor = Colors.red.shade500; + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: ListTileTheme( + data: const ListTileThemeData( + selectedTileColor: Colors.green, + tileColor: Colors.red, + ), + child: Center( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return ListTile( + tileColor: tileColor, + selectedTileColor: selectedTileColor, + selected: isSelected, + onTap: () { + setState(()=> isSelected = !isSelected); + }, + title: const Text('Title'), + ); + }, + ), + ), + ), + ), + ), + ); + + expect(find.byType(Material), paints..path(color: tileColor)); + + // Tap on tile to change isSelected. + await tester.tap(find.byType(ListTile)); + await tester.pumpAndSettle(); + + expect(find.byType(Material), paints..path(color: selectedTileColor)); + }); +}