From 9c4e49ea788b48d2c139adbef1c46413776856d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=91=E6=B7=B1=E8=93=9D?= Date: Wed, 9 Apr 2025 12:47:20 +0800 Subject: [PATCH] fix(ListTileTheme): isThreeLine is missing. (#165481) fix: https://github.com/flutter/flutter/issues/165453 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. --- .../flutter/lib/src/material/list_tile.dart | 18 +- .../lib/src/material/list_tile_theme.dart | 14 +- .../flutter/test/material/list_tile_test.dart | 132 +++++++++++ .../test/material/list_tile_theme_test.dart | 219 ++++++++++++++++++ 4 files changed, 377 insertions(+), 6 deletions(-) diff --git a/packages/flutter/lib/src/material/list_tile.dart b/packages/flutter/lib/src/material/list_tile.dart index e2c9ee21c1f..7cdeef8db9f 100644 --- a/packages/flutter/lib/src/material/list_tile.dart +++ b/packages/flutter/lib/src/material/list_tile.dart @@ -393,7 +393,7 @@ class ListTile extends StatelessWidget { this.title, this.subtitle, this.trailing, - this.isThreeLine = false, + this.isThreeLine, this.dense, this.visualDensity, this.shape, @@ -425,7 +425,7 @@ class ListTile extends StatelessWidget { this.minTileHeight, this.titleAlignment, this.internalAddSemanticForOnTap = true, - }) : assert(!isThreeLine || subtitle != null); + }) : assert(isThreeLine != true || subtitle != null); /// A widget to display before the title. /// @@ -482,7 +482,12 @@ class ListTile extends StatelessWidget { /// /// When using a [Text] widget for [title] and [subtitle], you can enforce /// line limits using [Text.maxLines]. - final bool isThreeLine; + /// + /// See also: + /// + /// * [ListTileTheme.of], which returns the nearest [ListTileTheme]'s + /// [ListTileThemeData]. + final bool? isThreeLine; /// {@template flutter.material.ListTile.dense} /// Whether this list tile is part of a vertically dense list. @@ -987,7 +992,11 @@ class ListTile extends StatelessWidget { trailing: trailingIcon, isDense: _isDenseLayout(theme, tileTheme), visualDensity: visualDensity ?? tileTheme.visualDensity ?? theme.visualDensity, - isThreeLine: isThreeLine, + isThreeLine: + isThreeLine ?? + tileTheme.isThreeLine ?? + theme.listTileTheme.isThreeLine ?? + false, textDirection: textDirection, titleBaselineType: titleStyle.textBaseline ?? defaults.titleTextStyle!.textBaseline!, @@ -1021,7 +1030,6 @@ class ListTile extends StatelessWidget { ifTrue: 'THREE_LINE', ifFalse: 'TWO_LINE', showName: true, - defaultValue: false, ), ); properties.add( diff --git a/packages/flutter/lib/src/material/list_tile_theme.dart b/packages/flutter/lib/src/material/list_tile_theme.dart index 3dd6ce9d6c1..03b1d6570ec 100644 --- a/packages/flutter/lib/src/material/list_tile_theme.dart +++ b/packages/flutter/lib/src/material/list_tile_theme.dart @@ -73,6 +73,7 @@ class ListTileThemeData with Diagnosticable { this.minTileHeight, this.titleAlignment, this.controlAffinity, + this.isThreeLine, }); /// Overrides the default value of [ListTile.dense]. @@ -139,6 +140,9 @@ class ListTileThemeData with Diagnosticable { /// or [ExpansionTile.controlAffinity] or [SwitchListTile.controlAffinity] or [RadioListTile.controlAffinity]. final ListTileControlAffinity? controlAffinity; + /// If specified, overrides the default value of [ListTile.isThreeLine]. + final bool? isThreeLine; + /// Creates a copy of this object with the given fields replaced with the /// new values. ListTileThemeData copyWith({ @@ -187,6 +191,7 @@ class ListTileThemeData with Diagnosticable { visualDensity: visualDensity ?? this.visualDensity, titleAlignment: titleAlignment ?? this.titleAlignment, controlAffinity: controlAffinity ?? this.controlAffinity, + isThreeLine: isThreeLine ?? this.isThreeLine, ); } @@ -221,6 +226,7 @@ class ListTileThemeData with Diagnosticable { visualDensity: t < 0.5 ? a?.visualDensity : b?.visualDensity, titleAlignment: t < 0.5 ? a?.titleAlignment : b?.titleAlignment, controlAffinity: t < 0.5 ? a?.controlAffinity : b?.controlAffinity, + isThreeLine: t < 0.5 ? a?.isThreeLine : b?.isThreeLine, ); } @@ -247,6 +253,7 @@ class ListTileThemeData with Diagnosticable { visualDensity, titleAlignment, controlAffinity, + isThreeLine, ]); @override @@ -278,7 +285,8 @@ class ListTileThemeData with Diagnosticable { other.mouseCursor == mouseCursor && other.visualDensity == visualDensity && other.titleAlignment == titleAlignment && - other.controlAffinity == controlAffinity; + other.controlAffinity == controlAffinity && + other.isThreeLine == isThreeLine; } @override @@ -337,6 +345,7 @@ class ListTileThemeData with Diagnosticable { defaultValue: null, ), ); + properties.add(DiagnosticsProperty('isThreeLine', isThreeLine, defaultValue: null)); } } @@ -573,6 +582,7 @@ class ListTileTheme extends InheritedTheme { MaterialStateProperty? mouseCursor, VisualDensity? visualDensity, ListTileControlAffinity? controlAffinity, + bool? isThreeLine, required Widget child, }) { return Builder( @@ -603,6 +613,7 @@ class ListTileTheme extends InheritedTheme { mouseCursor: mouseCursor ?? parent.mouseCursor, visualDensity: visualDensity ?? parent.visualDensity, controlAffinity: controlAffinity ?? parent.controlAffinity, + isThreeLine: isThreeLine ?? parent.isThreeLine, ), child: child, ); @@ -627,6 +638,7 @@ class ListTileTheme extends InheritedTheme { horizontalTitleGap: horizontalTitleGap, minVerticalPadding: minVerticalPadding, minLeadingWidth: minLeadingWidth, + isThreeLine: _data?.isThreeLine, ), child: child, ); diff --git a/packages/flutter/test/material/list_tile_test.dart b/packages/flutter/test/material/list_tile_test.dart index ec52249f1e9..b3f607a109c 100644 --- a/packages/flutter/test/material/list_tile_test.dart +++ b/packages/flutter/test/material/list_tile_test.dart @@ -4361,6 +4361,138 @@ void main() { expect(trailingOffset.dy - tileOffset.dy, minVerticalPadding); }); }); + + // Regression test for https://github.com/flutter/flutter/issues/165453 + testWidgets('ListTile isThreeLine', (WidgetTester tester) async { + const double height = 300; + const double avatarTop = 130.0; + const double placeholderTop = 138.0; + + Widget buildFrame({bool? themeDataIsThreeLine, bool? themeIsThreeLine, bool? isThreeLine}) { + return MaterialApp( + key: UniqueKey(), + theme: + themeDataIsThreeLine != null + ? ThemeData(listTileTheme: ListTileThemeData(isThreeLine: themeDataIsThreeLine)) + : null, + home: Material( + child: ListTileTheme( + data: + themeIsThreeLine != null ? ListTileThemeData(isThreeLine: themeIsThreeLine) : null, + child: ListView( + children: [ + ListTile( + isThreeLine: isThreeLine, + leading: const CircleAvatar(), + trailing: const SizedBox(height: 24.0, width: 24.0, child: Placeholder()), + title: const Text('A'), + subtitle: const Text('A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM'), + ), + ListTile( + isThreeLine: isThreeLine, + leading: const CircleAvatar(), + trailing: const SizedBox(height: 24.0, width: 24.0, child: Placeholder()), + title: const Text('A'), + subtitle: const Text('A'), + ), + ], + ), + ), + ), + ); + } + + void expectTwoLine() { + expect( + tester.getRect(find.byType(ListTile).at(0)), + const Rect.fromLTWH(0.0, 0.0, 800.0, height), + ); + expect( + tester.getRect(find.byType(CircleAvatar).at(0)), + const Rect.fromLTWH(16.0, avatarTop, 40.0, 40.0), + ); + expect( + tester.getRect(find.byType(Placeholder).at(0)), + const Rect.fromLTWH(800.0 - 24.0 - 24.0, placeholderTop, 24.0, 24.0), + ); + expect( + tester.getRect(find.byType(ListTile).at(1)), + const Rect.fromLTWH(0.0, height, 800.0, 72.0), + ); + expect( + tester.getRect(find.byType(CircleAvatar).at(1)), + const Rect.fromLTWH(16.0, height + 16.0, 40.0, 40.0), + ); + expect( + tester.getRect(find.byType(Placeholder).at(1)), + const Rect.fromLTWH(800.0 - 24.0 - 24.0, height + 24.0, 24.0, 24.0), + ); + } + + void expectThreeLine() { + expect( + tester.getRect(find.byType(ListTile).at(0)), + const Rect.fromLTWH(0.0, 0.0, 800.0, height), + ); + expect( + tester.getRect(find.byType(CircleAvatar).at(0)), + const Rect.fromLTWH(16.0, 8.0, 40.0, 40.0), + ); + expect( + tester.getRect(find.byType(Placeholder).at(0)), + const Rect.fromLTWH(800.0 - 24.0 - 24.0, 8.0, 24.0, 24.0), + ); + expect( + tester.getRect(find.byType(ListTile).at(1)), + const Rect.fromLTWH(0.0, height, 800.0, 88.0), + ); + expect( + tester.getRect(find.byType(CircleAvatar).at(1)), + const Rect.fromLTWH(16.0, height + 8.0, 40.0, 40.0), + ); + expect( + tester.getRect(find.byType(Placeholder).at(1)), + const Rect.fromLTWH(800.0 - 24.0 - 24.0, height + 8.0, 24.0, 24.0), + ); + } + + await tester.pumpWidget(buildFrame()); + expectTwoLine(); + + await tester.pumpWidget(buildFrame(themeDataIsThreeLine: true)); + expectThreeLine(); + + await tester.pumpWidget(buildFrame(themeDataIsThreeLine: false, themeIsThreeLine: true)); + expectThreeLine(); + + await tester.pumpWidget(buildFrame(themeDataIsThreeLine: true, themeIsThreeLine: false)); + expectTwoLine(); + + await tester.pumpWidget(buildFrame(isThreeLine: true)); + expectThreeLine(); + + await tester.pumpWidget(buildFrame(themeIsThreeLine: true, isThreeLine: false)); + expectTwoLine(); + + await tester.pumpWidget(buildFrame(themeDataIsThreeLine: true, isThreeLine: false)); + expectTwoLine(); + + await tester.pumpWidget( + buildFrame(themeDataIsThreeLine: true, themeIsThreeLine: true, isThreeLine: false), + ); + expectTwoLine(); + + await tester.pumpWidget(buildFrame(themeIsThreeLine: false, isThreeLine: true)); + expectThreeLine(); + + await tester.pumpWidget(buildFrame(themeDataIsThreeLine: false, isThreeLine: true)); + expectThreeLine(); + + await tester.pumpWidget( + buildFrame(themeDataIsThreeLine: false, themeIsThreeLine: false, isThreeLine: true), + ); + expectThreeLine(); + }); } RenderParagraph _getTextRenderObject(WidgetTester tester, String text) { diff --git a/packages/flutter/test/material/list_tile_theme_test.dart b/packages/flutter/test/material/list_tile_theme_test.dart index 3c12053b7a6..c9c71c79837 100644 --- a/packages/flutter/test/material/list_tile_theme_test.dart +++ b/packages/flutter/test/material/list_tile_theme_test.dart @@ -77,6 +77,7 @@ void main() { expect(themeData.mouseCursor, null); expect(themeData.visualDensity, null); expect(themeData.titleAlignment, null); + expect(themeData.isThreeLine, null); }); testWidgets('Default ListTileThemeData debugFillProperties', (WidgetTester tester) async { @@ -115,6 +116,7 @@ void main() { mouseCursor: MaterialStateMouseCursor.clickable, visualDensity: VisualDensity.comfortable, titleAlignment: ListTileTitleAlignment.top, + isThreeLine: true, ).debugFillProperties(builder); final List description = @@ -146,6 +148,7 @@ void main() { 'mouseCursor: WidgetStateMouseCursor(clickable)', 'visualDensity: VisualDensity#00000(h: -1.0, v: -1.0)(horizontal: -1.0, vertical: -1.0)', 'titleAlignment: ListTileTitleAlignment.top', + 'isThreeLine: true', ]), ); }); @@ -937,6 +940,7 @@ void main() { minTileHeight: 30, enableFeedback: true, titleAlignment: ListTileTitleAlignment.bottom, + isThreeLine: true, ); final ListTileThemeData copy = original.copyWith( @@ -958,6 +962,7 @@ void main() { minTileHeight: 80, enableFeedback: false, titleAlignment: ListTileTitleAlignment.top, + isThreeLine: false, ); expect(copy.dense, false); @@ -978,6 +983,7 @@ void main() { expect(copy.minTileHeight, 80); expect(copy.enableFeedback, false); expect(copy.titleAlignment, ListTileTitleAlignment.top); + expect(copy.isThreeLine, false); }); testWidgets('ListTileTheme.titleAlignment is overridden by ListTile.titleAlignment', ( @@ -1040,6 +1046,7 @@ void main() { titleAlignment: ListTileTitleAlignment.bottom, mouseCursor: MaterialStateMouseCursor.textable, visualDensity: VisualDensity.comfortable, + isThreeLine: true, ), ), home: Material( @@ -1067,6 +1074,7 @@ void main() { titleAlignment: ListTileTitleAlignment.top, mouseCursor: MaterialStateMouseCursor.clickable, visualDensity: VisualDensity.compact, + isThreeLine: false, child: const ListTile(), ); }, @@ -1098,6 +1106,217 @@ void main() { expect(theme.titleAlignment, ListTileTitleAlignment.top); expect(theme.mouseCursor, MaterialStateMouseCursor.clickable); expect(theme.visualDensity, VisualDensity.compact); + expect(theme.isThreeLine, false); + }); + + // Regression test for https://github.com/flutter/flutter/issues/165453 + testWidgets('ListTileThemeData isThreeLine', (WidgetTester tester) async { + const double height = 300; + const double avatarTop = 130.0; + const double placeholderTop = 138.0; + + Widget buildFrame({bool? isThreeLine}) { + return MaterialApp( + key: UniqueKey(), + theme: + isThreeLine != null + ? ThemeData(listTileTheme: ListTileThemeData(isThreeLine: isThreeLine)) + : null, + home: Material( + child: ListView( + children: const [ + ListTile( + leading: CircleAvatar(), + trailing: SizedBox(height: 24.0, width: 24.0, child: Placeholder()), + title: Text('A'), + subtitle: Text('A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM'), + ), + ListTile( + leading: CircleAvatar(), + trailing: SizedBox(height: 24.0, width: 24.0, child: Placeholder()), + title: Text('A'), + subtitle: Text('A'), + ), + ], + ), + ), + ); + } + + void expectTwoLine() { + expect( + tester.getRect(find.byType(ListTile).at(0)), + const Rect.fromLTWH(0.0, 0.0, 800.0, height), + ); + expect( + tester.getRect(find.byType(CircleAvatar).at(0)), + const Rect.fromLTWH(16.0, avatarTop, 40.0, 40.0), + ); + expect( + tester.getRect(find.byType(Placeholder).at(0)), + const Rect.fromLTWH(800.0 - 24.0 - 24.0, placeholderTop, 24.0, 24.0), + ); + expect( + tester.getRect(find.byType(ListTile).at(1)), + const Rect.fromLTWH(0.0, height, 800.0, 72.0), + ); + expect( + tester.getRect(find.byType(CircleAvatar).at(1)), + const Rect.fromLTWH(16.0, height + 16.0, 40.0, 40.0), + ); + expect( + tester.getRect(find.byType(Placeholder).at(1)), + const Rect.fromLTWH(800.0 - 24.0 - 24.0, height + 24.0, 24.0, 24.0), + ); + } + + void expectThreeLine() { + expect( + tester.getRect(find.byType(ListTile).at(0)), + const Rect.fromLTWH(0.0, 0.0, 800.0, height), + ); + expect( + tester.getRect(find.byType(CircleAvatar).at(0)), + const Rect.fromLTWH(16.0, 8.0, 40.0, 40.0), + ); + expect( + tester.getRect(find.byType(Placeholder).at(0)), + const Rect.fromLTWH(800.0 - 24.0 - 24.0, 8.0, 24.0, 24.0), + ); + expect( + tester.getRect(find.byType(ListTile).at(1)), + const Rect.fromLTWH(0.0, height, 800.0, 88.0), + ); + expect( + tester.getRect(find.byType(CircleAvatar).at(1)), + const Rect.fromLTWH(16.0, height + 8.0, 40.0, 40.0), + ); + expect( + tester.getRect(find.byType(Placeholder).at(1)), + const Rect.fromLTWH(800.0 - 24.0 - 24.0, height + 8.0, 24.0, 24.0), + ); + } + + await tester.pumpWidget(buildFrame()); + expectTwoLine(); + + await tester.pumpWidget(buildFrame(isThreeLine: false)); + expectTwoLine(); + + await tester.pumpWidget(buildFrame(isThreeLine: true)); + expectThreeLine(); + }); + + // Regression test for https://github.com/flutter/flutter/issues/165453 + testWidgets('ListTileTheme isThreeLine', (WidgetTester tester) async { + const double height = 300; + const double avatarTop = 130.0; + const double placeholderTop = 138.0; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(listTileTheme: const ListTileThemeData(isThreeLine: true)), + home: Material( + child: ListTileTheme( + data: const ListTileThemeData(isThreeLine: false), + child: ListView( + children: const [ + ListTile( + leading: CircleAvatar(), + trailing: SizedBox(height: 24.0, width: 24.0, child: Placeholder()), + title: Text('A'), + subtitle: Text('A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM'), + ), + ListTile( + leading: CircleAvatar(), + trailing: SizedBox(height: 24.0, width: 24.0, child: Placeholder()), + title: Text('A'), + subtitle: Text('A'), + ), + ], + ), + ), + ), + ), + ); + + expect( + tester.getRect(find.byType(ListTile).at(0)), + const Rect.fromLTWH(0.0, 0.0, 800.0, height), + ); + expect( + tester.getRect(find.byType(CircleAvatar).at(0)), + const Rect.fromLTWH(16.0, avatarTop, 40.0, 40.0), + ); + expect( + tester.getRect(find.byType(Placeholder).at(0)), + const Rect.fromLTWH(800.0 - 24.0 - 24.0, placeholderTop, 24.0, 24.0), + ); + expect( + tester.getRect(find.byType(ListTile).at(1)), + const Rect.fromLTWH(0.0, height, 800.0, 72.0), + ); + expect( + tester.getRect(find.byType(CircleAvatar).at(1)), + const Rect.fromLTWH(16.0, height + 16.0, 40.0, 40.0), + ); + expect( + tester.getRect(find.byType(Placeholder).at(1)), + const Rect.fromLTWH(800.0 - 24.0 - 24.0, height + 24.0, 24.0, 24.0), + ); + + // THREE-LINE + await tester.pumpWidget( + MaterialApp( + key: UniqueKey(), + home: Material( + child: ListTileTheme( + data: const ListTileThemeData(isThreeLine: true), + child: ListView( + children: const [ + ListTile( + leading: CircleAvatar(), + trailing: SizedBox(height: 24.0, width: 24.0, child: Placeholder()), + title: Text('A'), + subtitle: Text('A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM'), + ), + ListTile( + leading: CircleAvatar(), + trailing: SizedBox(height: 24.0, width: 24.0, child: Placeholder()), + title: Text('A'), + subtitle: Text('A'), + ), + ], + ), + ), + ), + ), + ); + + expect( + tester.getRect(find.byType(ListTile).at(0)), + const Rect.fromLTWH(0.0, 0.0, 800.0, height), + ); + expect( + tester.getRect(find.byType(CircleAvatar).at(0)), + const Rect.fromLTWH(16.0, 8.0, 40.0, 40.0), + ); + expect( + tester.getRect(find.byType(Placeholder).at(0)), + const Rect.fromLTWH(800.0 - 24.0 - 24.0, 8.0, 24.0, 24.0), + ); + expect( + tester.getRect(find.byType(ListTile).at(1)), + const Rect.fromLTWH(0.0, height, 800.0, 88.0), + ); + expect( + tester.getRect(find.byType(CircleAvatar).at(1)), + const Rect.fromLTWH(16.0, height + 8.0, 40.0, 40.0), + ); + expect( + tester.getRect(find.byType(Placeholder).at(1)), + const Rect.fromLTWH(800.0 - 24.0 - 24.0, height + 8.0, 24.0, 24.0), + ); }); }