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.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(

View File

@ -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<bool>('isThreeLine', isThreeLine, defaultValue: null));
}
}
@ -573,6 +582,7 @@ class ListTileTheme extends InheritedTheme {
MaterialStateProperty<MouseCursor?>? 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,
);

View File

@ -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: <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) {

View File

@ -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<String> 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 <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),
);
});
}