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.
This commit is contained in:
黑深蓝 2025-04-09 12:47:20 +08:00 committed by GitHub
parent 7ab8ce9638
commit 9c4e49ea78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 377 additions and 6 deletions

View File

@ -393,7 +393,7 @@ class ListTile extends StatelessWidget {
this.title, this.title,
this.subtitle, this.subtitle,
this.trailing, this.trailing,
this.isThreeLine = false, this.isThreeLine,
this.dense, this.dense,
this.visualDensity, this.visualDensity,
this.shape, this.shape,
@ -425,7 +425,7 @@ class ListTile extends StatelessWidget {
this.minTileHeight, this.minTileHeight,
this.titleAlignment, this.titleAlignment,
this.internalAddSemanticForOnTap = true, this.internalAddSemanticForOnTap = true,
}) : assert(!isThreeLine || subtitle != null); }) : assert(isThreeLine != true || subtitle != null);
/// A widget to display before the title. /// 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 /// When using a [Text] widget for [title] and [subtitle], you can enforce
/// line limits using [Text.maxLines]. /// 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} /// {@template flutter.material.ListTile.dense}
/// Whether this list tile is part of a vertically dense list. /// Whether this list tile is part of a vertically dense list.
@ -987,7 +992,11 @@ class ListTile extends StatelessWidget {
trailing: trailingIcon, trailing: trailingIcon,
isDense: _isDenseLayout(theme, tileTheme), isDense: _isDenseLayout(theme, tileTheme),
visualDensity: visualDensity ?? tileTheme.visualDensity ?? theme.visualDensity, visualDensity: visualDensity ?? tileTheme.visualDensity ?? theme.visualDensity,
isThreeLine: isThreeLine, isThreeLine:
isThreeLine ??
tileTheme.isThreeLine ??
theme.listTileTheme.isThreeLine ??
false,
textDirection: textDirection, textDirection: textDirection,
titleBaselineType: titleBaselineType:
titleStyle.textBaseline ?? defaults.titleTextStyle!.textBaseline!, titleStyle.textBaseline ?? defaults.titleTextStyle!.textBaseline!,
@ -1021,7 +1030,6 @@ class ListTile extends StatelessWidget {
ifTrue: 'THREE_LINE', ifTrue: 'THREE_LINE',
ifFalse: 'TWO_LINE', ifFalse: 'TWO_LINE',
showName: true, showName: true,
defaultValue: false,
), ),
); );
properties.add( properties.add(

View File

@ -73,6 +73,7 @@ class ListTileThemeData with Diagnosticable {
this.minTileHeight, this.minTileHeight,
this.titleAlignment, this.titleAlignment,
this.controlAffinity, this.controlAffinity,
this.isThreeLine,
}); });
/// Overrides the default value of [ListTile.dense]. /// Overrides the default value of [ListTile.dense].
@ -139,6 +140,9 @@ class ListTileThemeData with Diagnosticable {
/// or [ExpansionTile.controlAffinity] or [SwitchListTile.controlAffinity] or [RadioListTile.controlAffinity]. /// or [ExpansionTile.controlAffinity] or [SwitchListTile.controlAffinity] or [RadioListTile.controlAffinity].
final ListTileControlAffinity? 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 /// Creates a copy of this object with the given fields replaced with the
/// new values. /// new values.
ListTileThemeData copyWith({ ListTileThemeData copyWith({
@ -187,6 +191,7 @@ class ListTileThemeData with Diagnosticable {
visualDensity: visualDensity ?? this.visualDensity, visualDensity: visualDensity ?? this.visualDensity,
titleAlignment: titleAlignment ?? this.titleAlignment, titleAlignment: titleAlignment ?? this.titleAlignment,
controlAffinity: controlAffinity ?? this.controlAffinity, controlAffinity: controlAffinity ?? this.controlAffinity,
isThreeLine: isThreeLine ?? this.isThreeLine,
); );
} }
@ -221,6 +226,7 @@ class ListTileThemeData with Diagnosticable {
visualDensity: t < 0.5 ? a?.visualDensity : b?.visualDensity, visualDensity: t < 0.5 ? a?.visualDensity : b?.visualDensity,
titleAlignment: t < 0.5 ? a?.titleAlignment : b?.titleAlignment, titleAlignment: t < 0.5 ? a?.titleAlignment : b?.titleAlignment,
controlAffinity: t < 0.5 ? a?.controlAffinity : b?.controlAffinity, 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, visualDensity,
titleAlignment, titleAlignment,
controlAffinity, controlAffinity,
isThreeLine,
]); ]);
@override @override
@ -278,7 +285,8 @@ class ListTileThemeData with Diagnosticable {
other.mouseCursor == mouseCursor && other.mouseCursor == mouseCursor &&
other.visualDensity == visualDensity && other.visualDensity == visualDensity &&
other.titleAlignment == titleAlignment && other.titleAlignment == titleAlignment &&
other.controlAffinity == controlAffinity; other.controlAffinity == controlAffinity &&
other.isThreeLine == isThreeLine;
} }
@override @override
@ -337,6 +345,7 @@ class ListTileThemeData with Diagnosticable {
defaultValue: null, defaultValue: null,
), ),
); );
properties.add(DiagnosticsProperty<bool>('isThreeLine', isThreeLine, defaultValue: null));
} }
} }
@ -573,6 +582,7 @@ class ListTileTheme extends InheritedTheme {
MaterialStateProperty<MouseCursor?>? mouseCursor, MaterialStateProperty<MouseCursor?>? mouseCursor,
VisualDensity? visualDensity, VisualDensity? visualDensity,
ListTileControlAffinity? controlAffinity, ListTileControlAffinity? controlAffinity,
bool? isThreeLine,
required Widget child, required Widget child,
}) { }) {
return Builder( return Builder(
@ -603,6 +613,7 @@ class ListTileTheme extends InheritedTheme {
mouseCursor: mouseCursor ?? parent.mouseCursor, mouseCursor: mouseCursor ?? parent.mouseCursor,
visualDensity: visualDensity ?? parent.visualDensity, visualDensity: visualDensity ?? parent.visualDensity,
controlAffinity: controlAffinity ?? parent.controlAffinity, controlAffinity: controlAffinity ?? parent.controlAffinity,
isThreeLine: isThreeLine ?? parent.isThreeLine,
), ),
child: child, child: child,
); );
@ -627,6 +638,7 @@ class ListTileTheme extends InheritedTheme {
horizontalTitleGap: horizontalTitleGap, horizontalTitleGap: horizontalTitleGap,
minVerticalPadding: minVerticalPadding, minVerticalPadding: minVerticalPadding,
minLeadingWidth: minLeadingWidth, minLeadingWidth: minLeadingWidth,
isThreeLine: _data?.isThreeLine,
), ),
child: child, child: child,
); );

View File

@ -4361,6 +4361,138 @@ void main() {
expect(trailingOffset.dy - tileOffset.dy, minVerticalPadding); 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: <Widget>[
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) { RenderParagraph _getTextRenderObject(WidgetTester tester, String text) {

View File

@ -77,6 +77,7 @@ void main() {
expect(themeData.mouseCursor, null); expect(themeData.mouseCursor, null);
expect(themeData.visualDensity, null); expect(themeData.visualDensity, null);
expect(themeData.titleAlignment, null); expect(themeData.titleAlignment, null);
expect(themeData.isThreeLine, null);
}); });
testWidgets('Default ListTileThemeData debugFillProperties', (WidgetTester tester) async { testWidgets('Default ListTileThemeData debugFillProperties', (WidgetTester tester) async {
@ -115,6 +116,7 @@ void main() {
mouseCursor: MaterialStateMouseCursor.clickable, mouseCursor: MaterialStateMouseCursor.clickable,
visualDensity: VisualDensity.comfortable, visualDensity: VisualDensity.comfortable,
titleAlignment: ListTileTitleAlignment.top, titleAlignment: ListTileTitleAlignment.top,
isThreeLine: true,
).debugFillProperties(builder); ).debugFillProperties(builder);
final List<String> description = final List<String> description =
@ -146,6 +148,7 @@ void main() {
'mouseCursor: WidgetStateMouseCursor(clickable)', 'mouseCursor: WidgetStateMouseCursor(clickable)',
'visualDensity: VisualDensity#00000(h: -1.0, v: -1.0)(horizontal: -1.0, vertical: -1.0)', 'visualDensity: VisualDensity#00000(h: -1.0, v: -1.0)(horizontal: -1.0, vertical: -1.0)',
'titleAlignment: ListTileTitleAlignment.top', 'titleAlignment: ListTileTitleAlignment.top',
'isThreeLine: true',
]), ]),
); );
}); });
@ -937,6 +940,7 @@ void main() {
minTileHeight: 30, minTileHeight: 30,
enableFeedback: true, enableFeedback: true,
titleAlignment: ListTileTitleAlignment.bottom, titleAlignment: ListTileTitleAlignment.bottom,
isThreeLine: true,
); );
final ListTileThemeData copy = original.copyWith( final ListTileThemeData copy = original.copyWith(
@ -958,6 +962,7 @@ void main() {
minTileHeight: 80, minTileHeight: 80,
enableFeedback: false, enableFeedback: false,
titleAlignment: ListTileTitleAlignment.top, titleAlignment: ListTileTitleAlignment.top,
isThreeLine: false,
); );
expect(copy.dense, false); expect(copy.dense, false);
@ -978,6 +983,7 @@ void main() {
expect(copy.minTileHeight, 80); expect(copy.minTileHeight, 80);
expect(copy.enableFeedback, false); expect(copy.enableFeedback, false);
expect(copy.titleAlignment, ListTileTitleAlignment.top); expect(copy.titleAlignment, ListTileTitleAlignment.top);
expect(copy.isThreeLine, false);
}); });
testWidgets('ListTileTheme.titleAlignment is overridden by ListTile.titleAlignment', ( testWidgets('ListTileTheme.titleAlignment is overridden by ListTile.titleAlignment', (
@ -1040,6 +1046,7 @@ void main() {
titleAlignment: ListTileTitleAlignment.bottom, titleAlignment: ListTileTitleAlignment.bottom,
mouseCursor: MaterialStateMouseCursor.textable, mouseCursor: MaterialStateMouseCursor.textable,
visualDensity: VisualDensity.comfortable, visualDensity: VisualDensity.comfortable,
isThreeLine: true,
), ),
), ),
home: Material( home: Material(
@ -1067,6 +1074,7 @@ void main() {
titleAlignment: ListTileTitleAlignment.top, titleAlignment: ListTileTitleAlignment.top,
mouseCursor: MaterialStateMouseCursor.clickable, mouseCursor: MaterialStateMouseCursor.clickable,
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
isThreeLine: false,
child: const ListTile(), child: const ListTile(),
); );
}, },
@ -1098,6 +1106,217 @@ void main() {
expect(theme.titleAlignment, ListTileTitleAlignment.top); expect(theme.titleAlignment, ListTileTitleAlignment.top);
expect(theme.mouseCursor, MaterialStateMouseCursor.clickable); expect(theme.mouseCursor, MaterialStateMouseCursor.clickable);
expect(theme.visualDensity, VisualDensity.compact); 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 <Widget>[
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 <Widget>[
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 <Widget>[
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),
);
}); });
} }