mirror of
https://github.com/flutter/flutter
synced 2024-10-13 11:42:54 +00:00
Improved positioning of leading and trailing widgets in overflowing ListTiles (#24767)
This commit is contained in:
parent
07e06171ba
commit
567db6f0d4
|
@ -160,10 +160,9 @@ enum ListTileControlAffinity {
|
|||
/// is true then the overall height of this tile and the size of the
|
||||
/// [DefaultTextStyle]s that wrap the [title] and [subtitle] widget are reduced.
|
||||
///
|
||||
/// List tiles are always a fixed height (which height depends on how
|
||||
/// [isThreeLine], [dense], and [subtitle] are configured); they do not grow in
|
||||
/// height based on their contents. If you are looking for a widget that allows
|
||||
/// for arbitrary layout in a row, consider [Row].
|
||||
/// It is the responsibility of the caller to ensure that [title] does not wrap,
|
||||
/// and to ensure that [subtitle] doesn't wrap (if [isThreeLine] is false) or
|
||||
/// wraps to two lines (if it is true).
|
||||
///
|
||||
/// List tiles are typically used in [ListView]s, or arranged in [Column]s in
|
||||
/// [Drawer]s and [Card]s.
|
||||
|
@ -246,20 +245,36 @@ class ListTile extends StatelessWidget {
|
|||
/// The primary content of the list tile.
|
||||
///
|
||||
/// Typically a [Text] widget.
|
||||
///
|
||||
/// This should not wrap.
|
||||
final Widget title;
|
||||
|
||||
/// Additional content displayed below the title.
|
||||
///
|
||||
/// Typically a [Text] widget.
|
||||
///
|
||||
/// If [isThreeLine] is false, this should not wrap.
|
||||
///
|
||||
/// If [isThreeLine] is true, this should be configured to take a maximum of
|
||||
/// two lines.
|
||||
final Widget subtitle;
|
||||
|
||||
/// A widget to display after the title.
|
||||
///
|
||||
/// Typically an [Icon] widget.
|
||||
///
|
||||
/// To show right-aligned metadata (assuming left-to-right reading order;
|
||||
/// left-aligned for right-to-left reading order), consider using a [Row] with
|
||||
/// [MainAxisAlign.baseline] alignment whose first item is [Expanded] and
|
||||
/// whose second child is the metadata text, instead of using the [trailing]
|
||||
/// property.
|
||||
final Widget trailing;
|
||||
|
||||
/// Whether this list tile is intended to display three lines of text.
|
||||
///
|
||||
/// If true, then [subtitle] must be non-null (since it is expected to give
|
||||
/// the second and third lines of text).
|
||||
///
|
||||
/// If false, the list tile is treated as having one line if the subtitle is
|
||||
/// null and treated as having two lines if the subtitle is non-null.
|
||||
final bool isThreeLine;
|
||||
|
@ -267,6 +282,8 @@ class ListTile extends StatelessWidget {
|
|||
/// Whether this list tile is part of a vertically dense list.
|
||||
///
|
||||
/// If this property is null then its value is based on [ListTileTheme.dense].
|
||||
///
|
||||
/// Dense list tiles default to a smaller height.
|
||||
final bool dense;
|
||||
|
||||
/// The tile's internal padding.
|
||||
|
@ -934,17 +951,19 @@ class _RenderListTile extends RenderBox {
|
|||
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(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);
|
||||
tileHeight = _defaultTileHeight;
|
||||
tileHeight = 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
|
||||
|
@ -966,8 +985,30 @@ class _RenderListTile extends RenderBox {
|
|||
}
|
||||
}
|
||||
|
||||
final double leadingY = (tileHeight - leadingSize.height) / 2.0;
|
||||
final double trailingY = (tileHeight - trailingSize.height) / 2.0;
|
||||
// This attempts to implement the redlines for the vertical position of the
|
||||
// leading and trailing icons on the spec page:
|
||||
// https://material.io/design/components/lists.html#specs
|
||||
// Some liberties have been taken to handle cases that aren't covered by
|
||||
// that specification, such as leading and trailing widgets with weird
|
||||
// sizes, "one-line" list tiles with title widgets that span multiple lines,
|
||||
// etc.
|
||||
double leadingY;
|
||||
double trailingY;
|
||||
if (isOneLine) {
|
||||
leadingY = (defaultTileHeight - leadingSize.height) / 2.0;
|
||||
trailingY = (defaultTileHeight - trailingSize.height) / 2.0;
|
||||
} else if (isTwoLine) {
|
||||
if (isDense) {
|
||||
leadingY = 12.0; // by extrapolation
|
||||
trailingY = 20.0; // by extrapolation
|
||||
} else {
|
||||
leadingY = leadingSize.height <= 40.0 ? 16.0 : 8.0;
|
||||
trailingY = 24.0;
|
||||
}
|
||||
} else {
|
||||
leadingY = 16.0;
|
||||
trailingY = 16.0;
|
||||
}
|
||||
|
||||
switch (textDirection) {
|
||||
case TextDirection.rtl: {
|
||||
|
|
|
@ -521,11 +521,11 @@ void main() {
|
|||
|
||||
// textDirection = LTR
|
||||
|
||||
// Two-line tile's height = 72, leading 24x32 widget is vertically centered
|
||||
// Two-line tile's height = 72, leading 24x32 widget is positioned 16.0 pixels from the top.
|
||||
await tester.pumpWidget(buildFrame(24.0, TextDirection.ltr));
|
||||
expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 72.0));
|
||||
expect(tester.getTopLeft(find.byKey(leadingKey)), const Offset(0.0, 20.0));
|
||||
expect(tester.getBottomRight(find.byKey(leadingKey)), const Offset(24.0, 52.0));
|
||||
expect(tester.getTopLeft(find.byKey(leadingKey)), const Offset(0.0, 16.0));
|
||||
expect(tester.getBottomRight(find.byKey(leadingKey)), const Offset(24.0, 16.0 + 32.0));
|
||||
|
||||
// Leading widget's width is 20, so default layout: the left edges of the
|
||||
// title and subtitle are at 56dps (contentPadding is zero).
|
||||
|
@ -536,8 +536,8 @@ void main() {
|
|||
// title and subtitle by 16.
|
||||
await tester.pumpWidget(buildFrame(56.0, TextDirection.ltr));
|
||||
expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 72.0));
|
||||
expect(tester.getTopLeft(find.byKey(leadingKey)), const Offset(0.0, 20.0));
|
||||
expect(tester.getBottomRight(find.byKey(leadingKey)), const Offset(56.0, 52.0));
|
||||
expect(tester.getTopLeft(find.byKey(leadingKey)), const Offset(0.0, 16.0));
|
||||
expect(tester.getBottomRight(find.byKey(leadingKey)), const Offset(56.0, 16.0 + 32.0));
|
||||
expect(left('title'), 72.0);
|
||||
expect(left('subtitle'), 72.0);
|
||||
|
||||
|
@ -545,16 +545,214 @@ void main() {
|
|||
|
||||
await tester.pumpWidget(buildFrame(24.0, TextDirection.rtl));
|
||||
expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 72.0));
|
||||
expect(tester.getTopRight(find.byKey(leadingKey)), const Offset(800.0, 20.0));
|
||||
expect(tester.getBottomLeft(find.byKey(leadingKey)), const Offset(800.0 - 24.0, 52.0));
|
||||
expect(tester.getTopRight(find.byKey(leadingKey)), const Offset(800.0, 16.0));
|
||||
expect(tester.getBottomLeft(find.byKey(leadingKey)), const Offset(800.0 - 24.0, 16.0 + 32.0));
|
||||
expect(right('title'), 800.0 - 56.0);
|
||||
expect(right('subtitle'), 800.0 - 56.0);
|
||||
|
||||
await tester.pumpWidget(buildFrame(56.0, TextDirection.rtl));
|
||||
expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 72.0));
|
||||
expect(tester.getTopRight(find.byKey(leadingKey)), const Offset(800.0, 20.0));
|
||||
expect(tester.getBottomLeft(find.byKey(leadingKey)), const Offset(800.0 - 56.0, 52.0));
|
||||
expect(tester.getTopRight(find.byKey(leadingKey)), const Offset(800.0, 16.0));
|
||||
expect(tester.getBottomLeft(find.byKey(leadingKey)), const Offset(800.0 - 56.0, 16.0 + 32.0));
|
||||
expect(right('title'), 800.0 - 72.0);
|
||||
expect(right('subtitle'), 800.0 - 72.0);
|
||||
});
|
||||
|
||||
testWidgets('ListTile leading and trailing positions', (WidgetTester tester) async {
|
||||
// This test is based on the redlines at
|
||||
// https://material.io/design/components/lists.html#specs
|
||||
|
||||
// DENSE "ONE"-LINE
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: ListView(
|
||||
children: const <Widget>[
|
||||
ListTile(
|
||||
dense: true,
|
||||
leading: CircleAvatar(),
|
||||
trailing: SizedBox(height: 24.0, width: 24.0, child: Placeholder()),
|
||||
title: Text('A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM'),
|
||||
),
|
||||
ListTile(
|
||||
dense: true,
|
||||
leading: CircleAvatar(),
|
||||
trailing: SizedBox(height: 24.0, width: 24.0, child: Placeholder()),
|
||||
title: Text('A'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
// LEFT TOP WIDTH HEIGHT
|
||||
expect(tester.getRect(find.byType(ListTile).at(0)), Rect.fromLTWH( 0.0, 0.0, 800.0, 177.0));
|
||||
expect(tester.getRect(find.byType(CircleAvatar).at(0)), Rect.fromLTWH( 16.0, 4.0, 40.0, 40.0));
|
||||
expect(tester.getRect(find.byType(Placeholder).at(0)), Rect.fromLTWH(800.0 - 24.0 - 16.0, 12.0, 24.0, 24.0));
|
||||
expect(tester.getRect(find.byType(ListTile).at(1)), Rect.fromLTWH( 0.0, 177.0, 800.0, 48.0));
|
||||
expect(tester.getRect(find.byType(CircleAvatar).at(1)), Rect.fromLTWH( 16.0, 177.0 + 4.0, 40.0, 40.0));
|
||||
expect(tester.getRect(find.byType(Placeholder).at(1)), Rect.fromLTWH(800.0 - 24.0 - 16.0, 177.0 + 12.0, 24.0, 24.0));
|
||||
|
||||
// NON-DENSE "ONE"-LINE
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: ListView(
|
||||
children: const <Widget>[
|
||||
ListTile(
|
||||
leading: CircleAvatar(),
|
||||
trailing: SizedBox(height: 24.0, width: 24.0, child: Placeholder()),
|
||||
title: 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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump(const Duration(seconds: 2)); // the text styles are animated when we change dense
|
||||
// LEFT TOP WIDTH HEIGHT
|
||||
expect(tester.getRect(find.byType(ListTile).at(0)), Rect.fromLTWH( 0.0, 0.0, 800.0, 216.0));
|
||||
expect(tester.getRect(find.byType(CircleAvatar).at(0)), Rect.fromLTWH( 16.0, 8.0, 40.0, 40.0));
|
||||
expect(tester.getRect(find.byType(Placeholder).at(0)), Rect.fromLTWH(800.0 - 24.0 - 16.0, 16.0, 24.0, 24.0));
|
||||
expect(tester.getRect(find.byType(ListTile).at(1)), Rect.fromLTWH( 0.0, 216.0, 800.0, 56.0));
|
||||
expect(tester.getRect(find.byType(CircleAvatar).at(1)), Rect.fromLTWH( 16.0, 216.0 + 8.0, 40.0, 40.0));
|
||||
expect(tester.getRect(find.byType(Placeholder).at(1)), Rect.fromLTWH(800.0 - 24.0 - 16.0, 216.0 + 16.0, 24.0, 24.0));
|
||||
|
||||
// DENSE "TWO"-LINE
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: ListView(
|
||||
children: const <Widget>[
|
||||
ListTile(
|
||||
dense: true,
|
||||
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(
|
||||
dense: true,
|
||||
leading: CircleAvatar(),
|
||||
trailing: SizedBox(height: 24.0, width: 24.0, child: Placeholder()),
|
||||
title: Text('A'),
|
||||
subtitle: Text('A'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
// LEFT TOP WIDTH HEIGHT
|
||||
expect(tester.getRect(find.byType(ListTile).at(0)), Rect.fromLTWH( 0.0, 0.0, 800.0, 180.0));
|
||||
expect(tester.getRect(find.byType(CircleAvatar).at(0)), Rect.fromLTWH( 16.0, 12.0, 40.0, 40.0));
|
||||
expect(tester.getRect(find.byType(Placeholder).at(0)), Rect.fromLTWH(800.0 - 24.0 - 16.0, 20.0, 24.0, 24.0));
|
||||
expect(tester.getRect(find.byType(ListTile).at(1)), Rect.fromLTWH( 0.0, 180.0, 800.0, 64.0));
|
||||
expect(tester.getRect(find.byType(CircleAvatar).at(1)), Rect.fromLTWH( 16.0, 180.0 + 12.0, 40.0, 40.0));
|
||||
expect(tester.getRect(find.byType(Placeholder).at(1)), Rect.fromLTWH(800.0 - 24.0 - 16.0, 180.0 + 20.0, 24.0, 24.0));
|
||||
|
||||
// NON-DENSE "TWO"-LINE
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
// LEFT TOP WIDTH HEIGHT
|
||||
expect(tester.getRect(find.byType(ListTile).at(0)), Rect.fromLTWH( 0.0, 0.0, 800.0, 180.0));
|
||||
expect(tester.getRect(find.byType(CircleAvatar).at(0)), Rect.fromLTWH( 16.0, 16.0, 40.0, 40.0));
|
||||
expect(tester.getRect(find.byType(Placeholder).at(0)), Rect.fromLTWH(800.0 - 24.0 - 16.0, 24.0, 24.0, 24.0));
|
||||
expect(tester.getRect(find.byType(ListTile).at(1)), Rect.fromLTWH( 0.0, 180.0, 800.0, 72.0));
|
||||
expect(tester.getRect(find.byType(CircleAvatar).at(1)), Rect.fromLTWH( 16.0, 180.0 + 16.0, 40.0, 40.0));
|
||||
expect(tester.getRect(find.byType(Placeholder).at(1)), Rect.fromLTWH(800.0 - 24.0 - 16.0, 180.0 + 24.0, 24.0, 24.0));
|
||||
|
||||
// DENSE "THREE"-LINE
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: ListView(
|
||||
children: const <Widget>[
|
||||
ListTile(
|
||||
dense: true,
|
||||
isThreeLine: true,
|
||||
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(
|
||||
dense: true,
|
||||
isThreeLine: true,
|
||||
leading: CircleAvatar(),
|
||||
trailing: SizedBox(height: 24.0, width: 24.0, child: Placeholder()),
|
||||
title: Text('A'),
|
||||
subtitle: Text('A'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
// LEFT TOP WIDTH HEIGHT
|
||||
expect(tester.getRect(find.byType(ListTile).at(0)), Rect.fromLTWH( 0.0, 0.0, 800.0, 180.0));
|
||||
expect(tester.getRect(find.byType(CircleAvatar).at(0)), Rect.fromLTWH( 16.0, 16.0, 40.0, 40.0));
|
||||
expect(tester.getRect(find.byType(Placeholder).at(0)), Rect.fromLTWH(800.0 - 24.0 - 16.0, 16.0, 24.0, 24.0));
|
||||
expect(tester.getRect(find.byType(ListTile).at(1)), Rect.fromLTWH( 0.0, 180.0, 800.0, 76.0));
|
||||
expect(tester.getRect(find.byType(CircleAvatar).at(1)), Rect.fromLTWH( 16.0, 180.0 + 16.0, 40.0, 40.0));
|
||||
expect(tester.getRect(find.byType(Placeholder).at(1)), Rect.fromLTWH(800.0 - 24.0 - 16.0, 180.0 + 16.0, 24.0, 24.0));
|
||||
|
||||
// NON-DENSE THREE-LINE
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: ListView(
|
||||
children: const <Widget>[
|
||||
ListTile(
|
||||
isThreeLine: true,
|
||||
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(
|
||||
isThreeLine: true,
|
||||
leading: CircleAvatar(),
|
||||
trailing: SizedBox(height: 24.0, width: 24.0, child: Placeholder()),
|
||||
title: Text('A'),
|
||||
subtitle: Text('A'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
// LEFT TOP WIDTH HEIGHT
|
||||
expect(tester.getRect(find.byType(ListTile).at(0)), Rect.fromLTWH( 0.0, 0.0, 800.0, 180.0));
|
||||
expect(tester.getRect(find.byType(CircleAvatar).at(0)), Rect.fromLTWH( 16.0, 16.0, 40.0, 40.0));
|
||||
expect(tester.getRect(find.byType(Placeholder).at(0)), Rect.fromLTWH(800.0 - 24.0 - 16.0, 16.0, 24.0, 24.0));
|
||||
expect(tester.getRect(find.byType(ListTile).at(1)), Rect.fromLTWH( 0.0, 180.0, 800.0, 88.0));
|
||||
expect(tester.getRect(find.byType(CircleAvatar).at(1)), Rect.fromLTWH( 16.0, 180.0 + 16.0, 40.0, 40.0));
|
||||
expect(tester.getRect(find.byType(Placeholder).at(1)), Rect.fromLTWH(800.0 - 24.0 - 16.0, 180.0 + 16.0, 24.0, 24.0));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue