diff --git a/packages/flutter/lib/src/material/list_tile.dart b/packages/flutter/lib/src/material/list_tile.dart index 9a2310b1be..62a024d3ac 100644 --- a/packages/flutter/lib/src/material/list_tile.dart +++ b/packages/flutter/lib/src/material/list_tile.dart @@ -719,11 +719,17 @@ class ListTile extends StatelessWidget { this.tileColor, this.selectedTileColor, this.enableFeedback, + this.horizontalTitleGap = 16.0, + this.minVerticalPadding = 4.0, + this.minLeadingWidth = 40.0, }) : assert(isThreeLine != null), assert(enabled != null), assert(selected != null), assert(autofocus != null), assert(!isThreeLine || subtitle != null), + assert(horizontalTitleGap != null), + assert(minVerticalPadding != null), + assert(minLeadingWidth != null), super(key: key); /// A widget to display before the title. @@ -922,6 +928,15 @@ class ListTile extends StatelessWidget { /// * [Feedback] for providing platform-specific feedback to certain actions. final bool? enableFeedback; + /// The horizontal gap between the titles and the leading/trailing widgets. + final double horizontalTitleGap; + + /// The minimum padding on the top and bottom of the title and subtitle widgets. + final double minVerticalPadding; + + /// The minimum leading width. + final double minLeadingWidth; + /// Add a one pixel border in between each tile. If color isn't specified the /// [ThemeData.dividerColor] of the context's [Theme] is used. /// @@ -1133,6 +1148,9 @@ class ListTile extends StatelessWidget { textDirection: textDirection, titleBaselineType: titleStyle.textBaseline!, subtitleBaselineType: subtitleStyle?.textBaseline, + horizontalTitleGap: horizontalTitleGap, + minVerticalPadding: minVerticalPadding, + minLeadingWidth: minLeadingWidth, ), ), ), @@ -1161,12 +1179,18 @@ class _ListTile extends RenderObjectWidget { required this.visualDensity, required this.textDirection, required this.titleBaselineType, + required this.horizontalTitleGap, + required this.minVerticalPadding, + required this.minLeadingWidth, this.subtitleBaselineType, }) : assert(isThreeLine != null), assert(isDense != null), assert(visualDensity != null), assert(textDirection != null), assert(titleBaselineType != null), + assert(horizontalTitleGap != null), + assert(minVerticalPadding != null), + assert(minLeadingWidth != null), super(key: key); final Widget? leading; @@ -1179,6 +1203,9 @@ class _ListTile extends RenderObjectWidget { final TextDirection textDirection; final TextBaseline titleBaselineType; final TextBaseline? subtitleBaselineType; + final double horizontalTitleGap; + final double minVerticalPadding; + final double minLeadingWidth; @override _ListTileElement createElement() => _ListTileElement(this); @@ -1192,6 +1219,9 @@ class _ListTile extends RenderObjectWidget { textDirection: textDirection, titleBaselineType: titleBaselineType, subtitleBaselineType: subtitleBaselineType, + horizontalTitleGap: horizontalTitleGap, + minVerticalPadding: minVerticalPadding, + minLeadingWidth: minLeadingWidth, ); } @@ -1203,7 +1233,10 @@ class _ListTile extends RenderObjectWidget { ..visualDensity = visualDensity ..textDirection = textDirection ..titleBaselineType = titleBaselineType - ..subtitleBaselineType = subtitleBaselineType; + ..subtitleBaselineType = subtitleBaselineType + ..horizontalTitleGap = horizontalTitleGap + ..minLeadingWidth = minLeadingWidth + ..minVerticalPadding = minVerticalPadding; } } @@ -1319,23 +1352,26 @@ class _RenderListTile extends RenderBox { required TextDirection textDirection, required TextBaseline titleBaselineType, TextBaseline? subtitleBaselineType, + required double horizontalTitleGap, + required double minVerticalPadding, + required double minLeadingWidth, }) : assert(isDense != null), assert(visualDensity != null), assert(isThreeLine != null), assert(textDirection != null), assert(titleBaselineType != null), + assert(horizontalTitleGap != null), + assert(minVerticalPadding != null), + assert(minLeadingWidth != null), _isDense = isDense, _visualDensity = visualDensity, _isThreeLine = isThreeLine, _textDirection = textDirection, _titleBaselineType = titleBaselineType, - _subtitleBaselineType = subtitleBaselineType; - - static const double _minLeadingWidth = 40.0; - // The horizontal gap between the titles and the leading/trailing widgets - double get _horizontalTitleGap => 16.0 + visualDensity.horizontal * 2.0; - // The minimum padding on the top and bottom of the title and subtitle widgets. - static const double _minVerticalPadding = 4.0; + _subtitleBaselineType = subtitleBaselineType, + _horizontalTitleGap = horizontalTitleGap + visualDensity.horizontal * 2.0, + _minVerticalPadding = minVerticalPadding, + _minLeadingWidth = minLeadingWidth; final Map<_ListTileSlot, RenderBox> children = <_ListTileSlot, RenderBox>{}; @@ -1446,6 +1482,39 @@ class _RenderListTile extends RenderBox { markNeedsLayout(); } + double get horizontalTitleGap => _horizontalTitleGap; + double _horizontalTitleGap; + + set horizontalTitleGap(double value) { + assert(value != null); + if (_horizontalTitleGap == value) + return; + _horizontalTitleGap = value; + markNeedsLayout(); + } + + double get minVerticalPadding => _minVerticalPadding; + double _minVerticalPadding; + + set minVerticalPadding(double value) { + assert(value != null); + if (_minVerticalPadding == value) + return; + _minVerticalPadding = value; + markNeedsLayout(); + } + + double get minLeadingWidth => _minLeadingWidth; + double _minLeadingWidth; + + set minLeadingWidth(double value) { + assert(value != null); + if (_minLeadingWidth == value) + return; + _minLeadingWidth = value; + markNeedsLayout(); + } + @override void attach(PipelineOwner owner) { super.attach(owner); diff --git a/packages/flutter/test/material/list_tile_test.dart b/packages/flutter/test/material/list_tile_test.dart index 130e7e53dc..6961ef0fc0 100644 --- a/packages/flutter/test/material/list_tile_test.dart +++ b/packages/flutter/test/material/list_tile_test.dart @@ -1834,4 +1834,161 @@ void main() { expect(feedback.hapticCount, 0); }); }); + + testWidgets('ListTile horizontalTitleGap = 0.0', (WidgetTester tester) async { + Widget buildFrame(TextDirection textDirection) { + return MediaQuery( + data: const MediaQueryData( + padding: EdgeInsets.zero, + textScaleFactor: 1.0, + ), + child: Directionality( + textDirection: textDirection, + child: Material( + child: Container( + alignment: Alignment.topLeft, + child: const ListTile( + horizontalTitleGap: 0.0, + leading: Text('L'), + title: Text('title'), + trailing: Text('T'), + ), + ), + ), + ), + ); + } + + double left(String text) => tester.getTopLeft(find.text(text)).dx; + double right(String text) => tester.getTopRight(find.text(text)).dx; + + await tester.pumpWidget(buildFrame(TextDirection.ltr)); + + expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 56.0)); + expect(left('title'), 56.0); // horizontalTitleGap: 0 + + await tester.pumpWidget(buildFrame(TextDirection.rtl)); + + expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 56.0)); + expect(right('title'), 744.0); // horizontalTitleGap: 0 + }); + + testWidgets('ListTile horizontalTitleGap = (default) && ListTile minLeadingWidth = (default)', (WidgetTester tester) async { + Widget buildFrame(TextDirection textDirection) { + return MediaQuery( + data: const MediaQueryData( + padding: EdgeInsets.zero, + textScaleFactor: 1.0, + ), + child: Directionality( + textDirection: textDirection, + child: Material( + child: Container( + alignment: Alignment.topLeft, + child: const ListTile( + leading: Text('L'), + title: Text('title'), + trailing: Text('T'), + ), + ), + ), + ), + ); + } + + double left(String text) => tester.getTopLeft(find.text(text)).dx; + double right(String text) => tester.getTopRight(find.text(text)).dx; + + await tester.pumpWidget(buildFrame(TextDirection.ltr)); + + expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 56.0)); + // horizontalTitleGap: ListTileDefaultValue.horizontalTitleGap (16.0) + expect(left('title'), 72.0); + + await tester.pumpWidget(buildFrame(TextDirection.rtl)); + + expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 56.0)); + // horizontalTitleGap: ListTileDefaultValue.horizontalTitleGap (16.0) + expect(right('title'), 728.0); + }); + + testWidgets('ListTile minVerticalPadding = 80.0', (WidgetTester tester) async { + Widget buildFrame(TextDirection textDirection) { + return MediaQuery( + data: const MediaQueryData( + padding: EdgeInsets.zero, + textScaleFactor: 1.0, + ), + child: Directionality( + textDirection: textDirection, + child: Material( + child: Container( + alignment: Alignment.topLeft, + child: const ListTile( + minVerticalPadding: 80.0, + leading: Text('L'), + title: Text('title'), + trailing: Text('T'), + ), + ), + ), + ), + ); + } + + + await tester.pumpWidget(buildFrame(TextDirection.ltr)); + + // minVerticalPadding: 80.0 + // 80 + 80 + 16(Title) = 176 + expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 176.0)); + + await tester.pumpWidget(buildFrame(TextDirection.rtl)); + + // minVerticalPadding: 80.0 + // 80 + 80 + 16(Title) = 176 + expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 176.0)); + }); + + testWidgets('ListTile minLeadingWidth = 60.0', (WidgetTester tester) async { + Widget buildFrame(TextDirection textDirection) { + return MediaQuery( + data: const MediaQueryData( + padding: EdgeInsets.zero, + textScaleFactor: 1.0, + ), + child: Directionality( + textDirection: textDirection, + child: Material( + child: Container( + alignment: Alignment.topLeft, + child: const ListTile( + minLeadingWidth: 60.0, + leading: Text('L'), + title: Text('title'), + trailing: Text('T'), + ), + ), + ), + ), + ); + } + + double left(String text) => tester.getTopLeft(find.text(text)).dx; + double right(String text) => tester.getTopRight(find.text(text)).dx; + + await tester.pumpWidget(buildFrame(TextDirection.ltr)); + + expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 56.0)); + // minLeadingWidth: 60.0 + // 92.0 = 16.0(Default contentPadding) + 16.0(Default horizontalTitleGap) + 60.0 + expect(left('title'), 92.0); + + await tester.pumpWidget(buildFrame(TextDirection.rtl)); + + expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 56.0)); + // minLeadingWidth: 60.0 + // 708.0 = 800.0 - (16.0(Default contentPadding) + 16.0(Default horizontalTitleGap) + 60.0) + expect(right('title'), 708.0); + }); }