From 2b317d584fe93c5c24be76729e79431fabf665cb Mon Sep 17 00:00:00 2001 From: hangyu Date: Tue, 19 Mar 2024 10:58:16 -0700 Subject: [PATCH] Add a `minTileHeight` to ListTile widget so its height can be customized to less than the default height. (#145244) fixes: https://github.com/flutter/flutter/issues/145369 --- .../lib/src/material/expansion_tile.dart | 5 +++ .../flutter/lib/src/material/list_tile.dart | 38 ++++++++++++++++--- .../lib/src/material/list_tile_theme.dart | 12 ++++++ .../flutter/test/material/list_tile_test.dart | 30 +++++++++++++++ .../test/material/list_tile_theme_test.dart | 9 +++++ 5 files changed, 88 insertions(+), 6 deletions(-) diff --git a/packages/flutter/lib/src/material/expansion_tile.dart b/packages/flutter/lib/src/material/expansion_tile.dart index 1c5d30d197e..6f50ba7d7a0 100644 --- a/packages/flutter/lib/src/material/expansion_tile.dart +++ b/packages/flutter/lib/src/material/expansion_tile.dart @@ -251,6 +251,7 @@ class ExpansionTile extends StatefulWidget { this.controller, this.dense, this.visualDensity, + this.minTileHeight, this.enableFeedback = true, this.enabled = true, this.expansionAnimationStyle, @@ -508,6 +509,9 @@ class ExpansionTile extends StatefulWidget { /// {@macro flutter.material.themedata.visualDensity} final VisualDensity? visualDensity; + /// {@macro flutter.material.ListTile.minTileHeight} + final double? minTileHeight; + /// {@macro flutter.material.ListTile.enableFeedback} final bool? enableFeedback; @@ -710,6 +714,7 @@ class _ExpansionTileState extends State with SingleTickerProvider title: widget.title, subtitle: widget.subtitle, trailing: widget.trailing ?? _buildTrailingIcon(context), + minTileHeight: widget.minTileHeight, ), ), ), diff --git a/packages/flutter/lib/src/material/list_tile.dart b/packages/flutter/lib/src/material/list_tile.dart index 1a846079b6f..6f6596aac47 100644 --- a/packages/flutter/lib/src/material/list_tile.dart +++ b/packages/flutter/lib/src/material/list_tile.dart @@ -364,6 +364,7 @@ class ListTile extends StatelessWidget { this.horizontalTitleGap, this.minVerticalPadding, this.minLeadingWidth, + this.minTileHeight, this.titleAlignment, }) : assert(!isThreeLine || subtitle != null); @@ -669,6 +670,16 @@ class ListTile extends StatelessWidget { /// that is also null, then a default value of 40 is used. final double? minLeadingWidth; + /// {@template flutter.material.ListTile.minTileHeight} + /// The minimum height allocated for the [ListTile] widget. + /// + /// If this is null, default tile heights are 56.0, 72.0, and 88.0 for one, + /// two, and three lines of text respectively. If `isDense` is true, these + /// defaults are changed to 48.0, 64.0, and 76.0. A visual density value or + /// a large title will also adjust the default tile heights. + /// {@endtemplate} + final double? minTileHeight; + /// Defines how [ListTile.leading] and [ListTile.trailing] are /// vertically aligned relative to the [ListTile]'s titles /// ([ListTile.title] and [ListTile.subtitle]). @@ -884,6 +895,7 @@ class ListTile extends StatelessWidget { horizontalTitleGap: horizontalTitleGap ?? tileTheme.horizontalTitleGap ?? 16, minVerticalPadding: minVerticalPadding ?? tileTheme.minVerticalPadding ?? defaults.minVerticalPadding!, minLeadingWidth: minLeadingWidth ?? tileTheme.minLeadingWidth ?? defaults.minLeadingWidth!, + minTileHeight: minTileHeight ?? tileTheme.minTileHeight, titleAlignment: effectiveTitleAlignment, ), ), @@ -978,6 +990,7 @@ class _ListTile extends SlottedMultiChildRenderObjectWidget<_ListTileSlot, Rende required this.horizontalTitleGap, required this.minVerticalPadding, required this.minLeadingWidth, + this.minTileHeight, this.subtitleBaselineType, required this.titleAlignment, }); @@ -995,6 +1008,7 @@ class _ListTile extends SlottedMultiChildRenderObjectWidget<_ListTileSlot, Rende final double horizontalTitleGap; final double minVerticalPadding; final double minLeadingWidth; + final double? minTileHeight; final ListTileTitleAlignment titleAlignment; @override @@ -1022,6 +1036,7 @@ class _ListTile extends SlottedMultiChildRenderObjectWidget<_ListTileSlot, Rende horizontalTitleGap: horizontalTitleGap, minVerticalPadding: minVerticalPadding, minLeadingWidth: minLeadingWidth, + minTileHeight: minTileHeight, titleAlignment: titleAlignment, ); } @@ -1037,6 +1052,7 @@ class _ListTile extends SlottedMultiChildRenderObjectWidget<_ListTileSlot, Rende ..subtitleBaselineType = subtitleBaselineType ..horizontalTitleGap = horizontalTitleGap ..minLeadingWidth = minLeadingWidth + ..minTileHeight = minTileHeight ..minVerticalPadding = minVerticalPadding ..titleAlignment = titleAlignment; } @@ -1053,7 +1069,8 @@ class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_ required double horizontalTitleGap, required double minVerticalPadding, required double minLeadingWidth, - required ListTileTitleAlignment titleAlignment, + double? minTileHeight, + required ListTileTitleAlignment titleAlignment }) : _isDense = isDense, _visualDensity = visualDensity, _isThreeLine = isThreeLine, @@ -1063,6 +1080,7 @@ class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_ _horizontalTitleGap = horizontalTitleGap, _minVerticalPadding = minVerticalPadding, _minLeadingWidth = minLeadingWidth, + _minTileHeight = minTileHeight, _titleAlignment = titleAlignment; RenderBox? get leading => childForSlot(_ListTileSlot.leading); @@ -1179,6 +1197,16 @@ class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_ markNeedsLayout(); } + double? _minTileHeight; + double? get minTileHeight => _minTileHeight; + set minTileHeight(double? value) { + if (_minTileHeight == value) { + return; + } + _minTileHeight = value; + markNeedsLayout(); + } + ListTileTitleAlignment get titleAlignment => _titleAlignment; ListTileTitleAlignment _titleAlignment; set titleAlignment(ListTileTitleAlignment value) { @@ -1238,7 +1266,7 @@ class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_ @override double computeMinIntrinsicHeight(double width) { return math.max( - _defaultTileHeight, + minTileHeight ?? _defaultTileHeight, title!.getMinIntrinsicHeight(width) + (subtitle?.getMinIntrinsicHeight(width) ?? 0.0), ); } @@ -1345,19 +1373,17 @@ class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_ assert(isOneLine); } - final double defaultTileHeight = _defaultTileHeight; - double tileHeight; double titleY; double? subtitleY; if (!hasSubtitle) { - tileHeight = math.max(defaultTileHeight, titleSize.height + 2.0 * _minVerticalPadding); + tileHeight = math.max(minTileHeight ?? _defaultTileHeight, titleSize.height + 2.0 * _minVerticalPadding); titleY = (tileHeight - titleSize.height) / 2.0; } else { assert(subtitleBaselineType != null); titleY = titleBaseline! - _boxBaseline(title!, titleBaselineType)!; subtitleY = subtitleBaseline! - _boxBaseline(subtitle!, subtitleBaselineType!)! + visualDensity.vertical * 2.0; - tileHeight = defaultTileHeight; + tileHeight = minTileHeight ?? _defaultTileHeight; // If the title and subtitle overlap, move the title upwards by half // the overlap and the subtitle down by the same amount, and adjust diff --git a/packages/flutter/lib/src/material/list_tile_theme.dart b/packages/flutter/lib/src/material/list_tile_theme.dart index 3b77ffd9799..9746941c389 100644 --- a/packages/flutter/lib/src/material/list_tile_theme.dart +++ b/packages/flutter/lib/src/material/list_tile_theme.dart @@ -63,6 +63,7 @@ class ListTileThemeData with Diagnosticable { this.enableFeedback, this.mouseCursor, this.visualDensity, + this.minTileHeight, this.titleAlignment, }); @@ -111,6 +112,9 @@ class ListTileThemeData with Diagnosticable { /// Overrides the default value of [ListTile.minLeadingWidth]. final double? minLeadingWidth; + /// Overrides the default value of [ListTile.minTileHeight]. + final double? minTileHeight; + /// Overrides the default value of [ListTile.enableFeedback]. final bool? enableFeedback; @@ -141,6 +145,7 @@ class ListTileThemeData with Diagnosticable { double? horizontalTitleGap, double? minVerticalPadding, double? minLeadingWidth, + double? minTileHeight, bool? enableFeedback, MaterialStateProperty? mouseCursor, bool? isThreeLine, @@ -163,6 +168,7 @@ class ListTileThemeData with Diagnosticable { horizontalTitleGap: horizontalTitleGap ?? this.horizontalTitleGap, minVerticalPadding: minVerticalPadding ?? this.minVerticalPadding, minLeadingWidth: minLeadingWidth ?? this.minLeadingWidth, + minTileHeight: minTileHeight ?? this.minTileHeight, enableFeedback: enableFeedback ?? this.enableFeedback, mouseCursor: mouseCursor ?? this.mouseCursor, visualDensity: visualDensity ?? this.visualDensity, @@ -191,6 +197,7 @@ class ListTileThemeData with Diagnosticable { horizontalTitleGap: lerpDouble(a?.horizontalTitleGap, b?.horizontalTitleGap, t), minVerticalPadding: lerpDouble(a?.minVerticalPadding, b?.minVerticalPadding, t), minLeadingWidth: lerpDouble(a?.minLeadingWidth, b?.minLeadingWidth, t), + minTileHeight: lerpDouble(a?.minTileHeight, b?.minTileHeight, 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, @@ -215,6 +222,7 @@ class ListTileThemeData with Diagnosticable { horizontalTitleGap, minVerticalPadding, minLeadingWidth, + minTileHeight, enableFeedback, mouseCursor, visualDensity, @@ -245,6 +253,7 @@ class ListTileThemeData with Diagnosticable { && other.horizontalTitleGap == horizontalTitleGap && other.minVerticalPadding == minVerticalPadding && other.minLeadingWidth == minLeadingWidth + && other.minTileHeight == minTileHeight && other.enableFeedback == enableFeedback && other.mouseCursor == mouseCursor && other.visualDensity == visualDensity @@ -269,6 +278,7 @@ class ListTileThemeData with Diagnosticable { properties.add(DoubleProperty('horizontalTitleGap', horizontalTitleGap, defaultValue: null)); properties.add(DoubleProperty('minVerticalPadding', minVerticalPadding, defaultValue: null)); properties.add(DoubleProperty('minLeadingWidth', minLeadingWidth, defaultValue: null)); + properties.add(DoubleProperty('minTileHeight', minTileHeight, defaultValue: null)); properties.add(DiagnosticsProperty('enableFeedback', enableFeedback, defaultValue: null)); properties.add(DiagnosticsProperty>('mouseCursor', mouseCursor, defaultValue: null)); properties.add(DiagnosticsProperty('visualDensity', visualDensity, defaultValue: null)); @@ -488,6 +498,7 @@ class ListTileTheme extends InheritedTheme { double? horizontalTitleGap, double? minVerticalPadding, double? minLeadingWidth, + double? minTileHeight, ListTileTitleAlignment? titleAlignment, MaterialStateProperty? mouseCursor, VisualDensity? visualDensity, @@ -515,6 +526,7 @@ class ListTileTheme extends InheritedTheme { horizontalTitleGap: horizontalTitleGap ?? parent.horizontalTitleGap, minVerticalPadding: minVerticalPadding ?? parent.minVerticalPadding, minLeadingWidth: minLeadingWidth ?? parent.minLeadingWidth, + minTileHeight: minTileHeight ?? parent.minTileHeight, titleAlignment: titleAlignment ?? parent.titleAlignment, mouseCursor: mouseCursor ?? parent.mouseCursor, visualDensity: visualDensity ?? parent.visualDensity, diff --git a/packages/flutter/test/material/list_tile_test.dart b/packages/flutter/test/material/list_tile_test.dart index 5b5947a40eb..019ad142e88 100644 --- a/packages/flutter/test/material/list_tile_test.dart +++ b/packages/flutter/test/material/list_tile_test.dart @@ -1802,6 +1802,36 @@ void main() { expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 56.0)); expect(right('title'), 708.0); }); + testWidgets('ListTile minTileHeight', (WidgetTester tester) async { + Widget buildFrame(TextDirection textDirection, { double? minTileHeight, }) { + return MediaQuery( + data: const MediaQueryData(), + child: Directionality( + textDirection: textDirection, + child: Material( + child: Container( + alignment: Alignment.topLeft, + child: ListTile( + minTileHeight: minTileHeight, + ), + ), + ), + ), + ); + } + + // Default list tile with height = 56.0 + await tester.pumpWidget(buildFrame(TextDirection.ltr)); + expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 56.0)); + + // Set list tile height = 30.0 + await tester.pumpWidget(buildFrame(TextDirection.ltr, minTileHeight: 30)); + expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 30.0)); + + // Set list tile height = 60.0 + await tester.pumpWidget(buildFrame(TextDirection.ltr, minTileHeight: 60)); + expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 60.0)); + }); testWidgets('colors are applied to leading and trailing text widgets', (WidgetTester tester) async { final Key leadingKey = UniqueKey(); diff --git a/packages/flutter/test/material/list_tile_theme_test.dart b/packages/flutter/test/material/list_tile_theme_test.dart index 1f475f8ee0d..da56add7d47 100644 --- a/packages/flutter/test/material/list_tile_theme_test.dart +++ b/packages/flutter/test/material/list_tile_theme_test.dart @@ -72,6 +72,7 @@ void main() { expect(themeData.horizontalTitleGap, null); expect(themeData.minVerticalPadding, null); expect(themeData.minLeadingWidth, null); + expect(themeData.minTileHeight, null); expect(themeData.enableFeedback, null); expect(themeData.mouseCursor, null); expect(themeData.visualDensity, null); @@ -108,6 +109,7 @@ void main() { horizontalTitleGap: 200, minVerticalPadding: 300, minLeadingWidth: 400, + minTileHeight: 30, enableFeedback: true, mouseCursor: MaterialStateMouseCursor.clickable, visualDensity: VisualDensity.comfortable, @@ -137,6 +139,7 @@ void main() { 'horizontalTitleGap: 200.0', 'minVerticalPadding: 300.0', 'minLeadingWidth: 400.0', + 'minTileHeight: 30.0', 'enableFeedback: true', 'mouseCursor: WidgetStateMouseCursor(clickable)', 'visualDensity: VisualDensity#00000(h: -1.0, v: -1.0)(horizontal: -1.0, vertical: -1.0)', @@ -916,6 +919,7 @@ void main() { horizontalTitleGap: 200, minVerticalPadding: 300, minLeadingWidth: 400, + minTileHeight: 30, enableFeedback: true, titleAlignment: ListTileTitleAlignment.bottom, ); @@ -936,6 +940,7 @@ void main() { horizontalTitleGap: 600, minVerticalPadding: 700, minLeadingWidth: 800, + minTileHeight: 80, enableFeedback: false, titleAlignment: ListTileTitleAlignment.top, ); @@ -955,6 +960,7 @@ void main() { expect(copy.horizontalTitleGap, 600); expect(copy.minVerticalPadding, 700); expect(copy.minLeadingWidth, 800); + expect(copy.minTileHeight, 80); expect(copy.enableFeedback, false); expect(copy.titleAlignment, ListTileTitleAlignment.top); }); @@ -1015,6 +1021,7 @@ void main() { horizontalTitleGap: 200, minVerticalPadding: 300, minLeadingWidth: 400, + minTileHeight: 30, enableFeedback: true, titleAlignment: ListTileTitleAlignment.bottom, mouseCursor: MaterialStateMouseCursor.textable, @@ -1041,6 +1048,7 @@ void main() { horizontalTitleGap: 600, minVerticalPadding: 700, minLeadingWidth: 800, + minTileHeight: 80, enableFeedback: false, titleAlignment: ListTileTitleAlignment.top, mouseCursor: MaterialStateMouseCursor.clickable, @@ -1071,6 +1079,7 @@ void main() { expect(theme.horizontalTitleGap, 600); expect(theme.minVerticalPadding, 700); expect(theme.minLeadingWidth, 800); + expect(theme.minTileHeight, 80); expect(theme.enableFeedback, false); expect(theme.titleAlignment, ListTileTitleAlignment.top); expect(theme.mouseCursor, MaterialStateMouseCursor.clickable);