Unverified Commit 42b20cf9 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Added ListTile.titleAlignment, ListTileThemeData.titleAlignment (#119872)

* added ListTile.textAlignment

* changed titlesHeight to titleHeight

* fixed a typo

* Add tests and example

* Update tests

* update example test

---------
Co-authored-by: 's avatartahatesser <tessertaha@gmail.com>
parent 0521c60c
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flutter code sample for [ListTile].
import 'package:flutter/material.dart';
void main() => runApp(const ListTileApp());
class ListTileApp extends StatelessWidget {
const ListTileApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(useMaterial3: true),
home: const ListTileExample(),
);
}
}
class ListTileExample extends StatefulWidget {
const ListTileExample({super.key});
@override
State<ListTileExample> createState() => _ListTileExampleState();
}
class _ListTileExampleState extends State<ListTileExample> {
ListTileTitleAlignment? titleAlignment;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('ListTile.titleAlignment Sample')),
body: Column(
children: <Widget>[
const Divider(),
ListTile(
titleAlignment: titleAlignment,
leading: Checkbox(
value: true,
onChanged:(bool? value) { },
),
title: const Text('Headline Text'),
subtitle: const Text('Tapping on the trailing widget will show a menu that allows you to change the title alignment. The title alignment is set to threeLine by default if `ThemeData.useMaterial3` is true. Otherwise, defaults to titleHeight.'),
trailing: PopupMenuButton<ListTileTitleAlignment>(
onSelected: (ListTileTitleAlignment? value) {
setState(() {
titleAlignment = value;
});
},
itemBuilder: (BuildContext context) => <PopupMenuEntry<ListTileTitleAlignment>>[
const PopupMenuItem<ListTileTitleAlignment>(
value: ListTileTitleAlignment.threeLine,
child: Text('threeLine'),
),
const PopupMenuItem<ListTileTitleAlignment>(
value: ListTileTitleAlignment.titleHeight,
child: Text('titleHeight'),
),
const PopupMenuItem<ListTileTitleAlignment>(
value: ListTileTitleAlignment.top,
child: Text('top'),
),
const PopupMenuItem<ListTileTitleAlignment>(
value: ListTileTitleAlignment.center,
child: Text('center'),
),
const PopupMenuItem<ListTileTitleAlignment>(
value: ListTileTitleAlignment.bottom,
child: Text('bottom'),
),
],
),
),
const Divider(),
],
),
);
}
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter_api_samples/material/list_tile/list_tile.4.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Can choose different title aligments from popup menu', (WidgetTester tester) async {
await tester.pumpWidget(
const example.ListTileApp(),
);
Offset titleOffset = tester.getTopLeft(find.text('Headline Text'));
Offset leadingOffset = tester.getTopLeft(find.byType(Checkbox));
Offset trailingOffset = tester.getTopRight(find.byIcon(Icons.adaptive.more));
// The default title alignment is threeLine.
expect(leadingOffset.dy - titleOffset.dy, 48.0);
expect(trailingOffset.dy - titleOffset.dy, 60.0);
await tester.tap(find.byIcon(Icons.adaptive.more));
await tester.pumpAndSettle();
// Change the title alignment to titleHeight.
await tester.tap(find.text('titleHeight'));
await tester.pumpAndSettle();
titleOffset = tester.getTopLeft(find.text('Headline Text'));
leadingOffset = tester.getTopLeft(find.byType(Checkbox));
trailingOffset = tester.getTopRight(find.byIcon(Icons.adaptive.more));
expect(leadingOffset.dy - titleOffset.dy, 8.0);
expect(trailingOffset.dy - titleOffset.dy, 20.0);
await tester.tap(find.byIcon(Icons.adaptive.more));
await tester.pumpAndSettle();
// Change the title alignment to bottom.
await tester.tap(find.text('bottom'));
await tester.pumpAndSettle();
titleOffset = tester.getTopLeft(find.text('Headline Text'));
leadingOffset = tester.getTopLeft(find.byType(Checkbox));
trailingOffset = tester.getTopRight(find.byIcon(Icons.adaptive.more));
expect(leadingOffset.dy - titleOffset.dy, 96.0);
expect(trailingOffset.dy - titleOffset.dy, 108.0);
});
}
......@@ -63,6 +63,49 @@ enum ListTileControlAffinity {
platform,
}
/// Defines how [ListTile.leading] and [ListTile.trailing] are
/// vertically aligned relative to the [ListTile]'s titles
/// ([ListTile.title] and [ListTile.subtitle]).
///
/// See also:
///
/// * [ListTile.titleAlignment], to configure the title alignment for an
/// individual [ListTile].
/// * [ListTileThemeData.titleAlignment], to configure the title alignment
/// for all of the [ListTile]s under a [ListTileTheme].
/// * [ThemeData.listTileTheme], to configure the [ListTileTheme]
/// for an entire app.
enum ListTileTitleAlignment {
/// The top of the [ListTile.leading] and [ListTile.trailing] widgets are
/// placed [ListTile.minVerticalPadding] below the top of the [ListTile.title]
/// if [ListTile.isThreeLine] is true, otherwise they're centered relative
/// to the [ListTile.title] and [ListTile.subtitle] widgets.
///
/// This is the default when [ThemeData.useMaterial3] is true.
threeLine,
/// The tops of the [ListTile.leading] and [ListTile.trailing] widgets are
/// placed 16 units below the top of the [ListTile.title]
/// if the titles' overall height is greater than 72, otherwise they're
/// centered relative to the [ListTile.title] and [ListTile.subtitle] widgets.
///
/// This is the default when [ThemeData.useMaterial3] is false.
titleHeight,
/// The tops of the [ListTile.leading] and [ListTile.trailing] widgets are
/// placed [ListTile.minVerticalPadding] below the top of the [ListTile.title].
top,
/// The [ListTile.leading] and [ListTile.trailing] widgets are
/// centered relative to the [ListTile]'s titles.
center,
/// The bottoms of the [ListTile.leading] and [ListTile.trailing] widgets are
/// placed [ListTile.minVerticalPadding] above the bottom of the [ListTile]'s
/// titles.
bottom,
}
/// A single fixed-height row that typically contains some text as well as
/// a leading or trailing icon.
///
......@@ -156,6 +199,14 @@ enum ListTileControlAffinity {
/// ** See code in examples/api/lib/material/list_tile/list_tile.3.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// This sample shows [ListTile.titleAlignment] can be used to configure the
/// [leading] and [trailing] widgets alignment relative to the [title] and
/// [subtitle] widgets.
///
/// ** See code in examples/api/lib/material/list_tile/list_tile.4.dart **
/// {@end-tool}
///
/// {@tool snippet}
/// To use a [ListTile] within a [Row], it needs to be wrapped in an
/// [Expanded] widget. [ListTile] requires fixed width constraints,
......@@ -317,6 +368,7 @@ class ListTile extends StatelessWidget {
this.horizontalTitleGap,
this.minVerticalPadding,
this.minLeadingWidth,
this.titleAlignment,
}) : assert(!isThreeLine || subtitle != null);
/// A widget to display before the title.
......@@ -616,6 +668,20 @@ class ListTile extends StatelessWidget {
/// that is also null, then a default value of 40 is used.
final double? minLeadingWidth;
/// Defines how [ListTile.leading] and [ListTile.trailing] are
/// vertically aligned relative to the [ListTile]'s titles
/// ([ListTile.title] and [ListTile.subtitle]).
///
/// If this property is null then [ListTileThemeData.titleAlignment]
/// is used. If that is also null then [ListTileTitleAlignment.threeLine]
/// is used.
///
/// See also:
///
/// * [ListTileTheme.of], which returns the nearest [ListTileTheme]'s
/// [ListTileThemeData].
final ListTileTitleAlignment? titleAlignment;
/// Add a one pixel border in between each tile. If color isn't specified the
/// [ThemeData.dividerColor] of the context's [Theme] is used.
///
......@@ -761,6 +827,7 @@ class ListTile extends StatelessWidget {
final EdgeInsets resolvedContentPadding = contentPadding?.resolve(textDirection)
?? tileTheme.contentPadding?.resolve(textDirection)
?? defaults.contentPadding!.resolve(textDirection);
// Show basic cursor when ListTile isn't enabled or gesture callbacks are null.
final Set<MaterialState> mouseStates = <MaterialState>{
if (!enabled || (onTap == null && onLongPress == null)) MaterialState.disabled,
......@@ -769,6 +836,10 @@ class ListTile extends StatelessWidget {
?? tileTheme.mouseCursor?.resolve(mouseStates)
?? MaterialStateMouseCursor.clickable.resolve(mouseStates);
final ListTileTitleAlignment effectiveTitleAlignment = titleAlignment
?? tileTheme.titleAlignment
?? (theme.useMaterial3 ? ListTileTitleAlignment.threeLine : ListTileTitleAlignment.titleHeight);
return InkWell(
customBorder: shape ?? tileTheme.shape,
onTap: enabled ? onTap : null,
......@@ -812,7 +883,7 @@ class ListTile extends StatelessWidget {
horizontalTitleGap: horizontalTitleGap ?? tileTheme.horizontalTitleGap ?? 16,
minVerticalPadding: minVerticalPadding ?? tileTheme.minVerticalPadding ?? defaults.minVerticalPadding!,
minLeadingWidth: minLeadingWidth ?? tileTheme.minLeadingWidth ?? defaults.minLeadingWidth!,
material3: theme.useMaterial3,
titleAlignment: effectiveTitleAlignment,
),
),
),
......@@ -856,6 +927,7 @@ class ListTile extends StatelessWidget {
properties.add(DoubleProperty('horizontalTitleGap', horizontalTitleGap, defaultValue: null));
properties.add(DoubleProperty('minVerticalPadding', minVerticalPadding, defaultValue: null));
properties.add(DoubleProperty('minLeadingWidth', minLeadingWidth, defaultValue: null));
properties.add(DiagnosticsProperty<ListTileTitleAlignment>('titleAlignment', titleAlignment, defaultValue: null));
}
}
......@@ -910,7 +982,7 @@ class _ListTile extends RenderObjectWidget with SlottedMultiChildRenderObjectWid
required this.minVerticalPadding,
required this.minLeadingWidth,
this.subtitleBaselineType,
required this.material3,
required this.titleAlignment,
});
final Widget? leading;
......@@ -926,7 +998,7 @@ class _ListTile extends RenderObjectWidget with SlottedMultiChildRenderObjectWid
final double horizontalTitleGap;
final double minVerticalPadding;
final double minLeadingWidth;
final bool material3;
final ListTileTitleAlignment titleAlignment;
@override
Iterable<_ListTileSlot> get slots => _ListTileSlot.values;
......@@ -957,7 +1029,7 @@ class _ListTile extends RenderObjectWidget with SlottedMultiChildRenderObjectWid
horizontalTitleGap: horizontalTitleGap,
minVerticalPadding: minVerticalPadding,
minLeadingWidth: minLeadingWidth,
material3: material3,
titleAlignment: titleAlignment,
);
}
......@@ -973,7 +1045,7 @@ class _ListTile extends RenderObjectWidget with SlottedMultiChildRenderObjectWid
..horizontalTitleGap = horizontalTitleGap
..minLeadingWidth = minLeadingWidth
..minVerticalPadding = minVerticalPadding
..material3 = material3;
..titleAlignment = titleAlignment;
}
}
......@@ -988,7 +1060,7 @@ class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_
required double horizontalTitleGap,
required double minVerticalPadding,
required double minLeadingWidth,
required bool material3,
required ListTileTitleAlignment titleAlignment,
}) : _isDense = isDense,
_visualDensity = visualDensity,
_isThreeLine = isThreeLine,
......@@ -998,7 +1070,7 @@ class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_
_horizontalTitleGap = horizontalTitleGap,
_minVerticalPadding = minVerticalPadding,
_minLeadingWidth = minLeadingWidth,
_material3 = material3;
_titleAlignment = titleAlignment;
RenderBox? get leading => childForSlot(_ListTileSlot.leading);
RenderBox? get title => childForSlot(_ListTileSlot.title);
......@@ -1114,13 +1186,13 @@ class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_
markNeedsLayout();
}
bool get material3 => _material3;
bool _material3;
set material3(bool value) {
if (_material3 == value) {
ListTileTitleAlignment get titleAlignment => _titleAlignment;
ListTileTitleAlignment _titleAlignment;
set titleAlignment(ListTileTitleAlignment value) {
if (_titleAlignment == value) {
return;
}
_material3 = value;
_titleAlignment = value;
markNeedsLayout();
}
......@@ -1314,7 +1386,9 @@ class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_
final double leadingY;
final double trailingY;
if (material3) {
switch (titleAlignment) {
case ListTileTitleAlignment.threeLine: {
if (isThreeLine) {
leadingY = _minVerticalPadding;
trailingY = _minVerticalPadding;
......@@ -1322,10 +1396,12 @@ class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_
leadingY = (tileHeight - leadingSize.height) / 2.0;
trailingY = (tileHeight - trailingSize.height) / 2.0;
}
} else {
break;
}
case ListTileTitleAlignment.titleHeight: {
// 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
// https://m2.material.io/components/lists#specs
// The interpretation for these redlines is as follows:
// - For large tiles (> 72dp), both leading and trailing controls should be
// a fixed distance from top. As per guidelines this is set to 16dp.
......@@ -1339,6 +1415,23 @@ class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_
leadingY = math.min((tileHeight - leadingSize.height) / 2.0, 16.0);
trailingY = (tileHeight - trailingSize.height) / 2.0;
}
break;
}
case ListTileTitleAlignment.top: {
leadingY = _minVerticalPadding;
trailingY = _minVerticalPadding;
break;
}
case ListTileTitleAlignment.center: {
leadingY = (tileHeight - leadingSize.height) / 2.0;
trailingY = (tileHeight - trailingSize.height) / 2.0;
break;
}
case ListTileTitleAlignment.bottom: {
leadingY = tileHeight - leadingSize.height - _minVerticalPadding;
trailingY = tileHeight - trailingSize.height - _minVerticalPadding;
break;
}
}
switch (textDirection) {
......
......@@ -63,6 +63,7 @@ class ListTileThemeData with Diagnosticable {
this.enableFeedback,
this.mouseCursor,
this.visualDensity,
this.titleAlignment,
});
/// Overrides the default value of [ListTile.dense].
......@@ -119,6 +120,9 @@ class ListTileThemeData with Diagnosticable {
/// If specified, overrides the default value of [ListTile.visualDensity].
final VisualDensity? visualDensity;
/// If specified, overrides the default value of [ListTile.titleAlignment].
final ListTileTitleAlignment? titleAlignment;
/// Creates a copy of this object with the given fields replaced with the
/// new values.
ListTileThemeData copyWith({
......@@ -141,6 +145,7 @@ class ListTileThemeData with Diagnosticable {
MaterialStateProperty<MouseCursor?>? mouseCursor,
bool? isThreeLine,
VisualDensity? visualDensity,
ListTileTitleAlignment? titleAlignment,
}) {
return ListTileThemeData(
dense: dense ?? this.dense,
......@@ -161,6 +166,7 @@ class ListTileThemeData with Diagnosticable {
enableFeedback: enableFeedback ?? this.enableFeedback,
mouseCursor: mouseCursor ?? this.mouseCursor,
visualDensity: visualDensity ?? this.visualDensity,
titleAlignment: titleAlignment ?? this.titleAlignment,
);
}
......@@ -188,6 +194,7 @@ class ListTileThemeData with Diagnosticable {
enableFeedback: t < 0.5 ? a?.enableFeedback : b?.enableFeedback,
mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor,
visualDensity: t < 0.5 ? a?.visualDensity : b?.visualDensity,
titleAlignment: t < 0.5 ? a?.titleAlignment : b?.titleAlignment,
);
}
......@@ -211,6 +218,7 @@ class ListTileThemeData with Diagnosticable {
enableFeedback,
mouseCursor,
visualDensity,
titleAlignment,
);
@override
......@@ -239,7 +247,8 @@ class ListTileThemeData with Diagnosticable {
&& other.minLeadingWidth == minLeadingWidth
&& other.enableFeedback == enableFeedback
&& other.mouseCursor == mouseCursor
&& other.visualDensity == visualDensity;
&& other.visualDensity == visualDensity
&& other.titleAlignment == titleAlignment;
}
@override
......@@ -263,6 +272,7 @@ class ListTileThemeData with Diagnosticable {
properties.add(DiagnosticsProperty<bool>('enableFeedback', enableFeedback, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: null));
properties.add(DiagnosticsProperty<VisualDensity>('visualDensity', visualDensity, defaultValue: null));
properties.add(DiagnosticsProperty<ListTileTitleAlignment>('titleAlignment', titleAlignment, defaultValue: null));
}
}
......@@ -477,6 +487,7 @@ class ListTileTheme extends InheritedTheme {
double? horizontalTitleGap,
double? minVerticalPadding,
double? minLeadingWidth,
ListTileTitleAlignment? titleAlignment,
required Widget child,
}) {
return Builder(
......@@ -498,6 +509,7 @@ class ListTileTheme extends InheritedTheme {
horizontalTitleGap: horizontalTitleGap ?? parent.horizontalTitleGap,
minVerticalPadding: minVerticalPadding ?? parent.minVerticalPadding,
minLeadingWidth: minLeadingWidth ?? parent.minLeadingWidth,
titleAlignment: titleAlignment ?? parent.titleAlignment,
),
child: child,
);
......
......@@ -1998,6 +1998,7 @@ void main() {
horizontalTitleGap: 4.0,
minVerticalPadding: 2.0,
minLeadingWidth: 6.0,
titleAlignment: ListTileTitleAlignment.bottom,
).debugFillProperties(builder);
final List<String> description = builder.properties
......@@ -2035,6 +2036,7 @@ void main() {
'horizontalTitleGap: 4.0',
'minVerticalPadding: 2.0',
'minLeadingWidth: 6.0',
'titleAlignment: ListTileTitleAlignment.bottom',
]),
);
});
......@@ -2222,6 +2224,260 @@ void main() {
expect(tester.takeException(), isNull);
});
testWidgets('titleAlignment position with title widget', (WidgetTester tester) async {
final Key leadingKey = GlobalKey();
final Key trailingKey = GlobalKey();
const double leadingHeight = 24.0;
const double titleHeight = 50.0;
const double trailingHeight = 24.0;
const double minVerticalPadding = 10.0;
const double tileHeight = minVerticalPadding * 2 + titleHeight;
Widget buildFrame({ ListTileTitleAlignment? titleAlignment }) {
return MaterialApp(
theme: ThemeData(useMaterial3: true),
home: Material(
child: Center(
child: ListTile(
titleAlignment: titleAlignment,
minVerticalPadding: minVerticalPadding,
leading: SizedBox(key: leadingKey, width: 24.0, height: leadingHeight),
title: const SizedBox(width: 20.0, height: titleHeight),
trailing: SizedBox(key: trailingKey, width: 24.0, height: trailingHeight),
),
),
),
);
}
// If [ThemeData.useMaterial3] is true, the default title alignment is
// [ListTileTitleAlignment.threeLine], which positions the leading and
// trailing widgets center vertically in the tile if the [ListTile.isThreeLine]
// property is false.
await tester.pumpWidget(buildFrame());
Offset tileOffset = tester.getTopLeft(find.byType(ListTile));
Offset leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
Offset trailingOffset = tester.getTopRight(find.byKey(trailingKey));
// Leading and trailing widgets are centered vertically in the tile.
const double centerPosition = (tileHeight / 2) - (leadingHeight / 2);
expect(leadingOffset.dy - tileOffset.dy, centerPosition);
expect(trailingOffset.dy - tileOffset.dy, centerPosition);
// Test [ListTileTitleAlignment.threeLine] alignment.
await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.threeLine));
tileOffset = tester.getTopLeft(find.byType(ListTile));
leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
trailingOffset = tester.getTopRight(find.byKey(trailingKey));
// Leading and trailing widgets are centered vertically in the tile,
// If the [ListTile.isThreeLine] property is false.
expect(leadingOffset.dy - tileOffset.dy, centerPosition);
expect(trailingOffset.dy - tileOffset.dy, centerPosition);
// Test [ListTileTitleAlignment.titleHeight] alignment.
await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.titleHeight));
tileOffset = tester.getTopLeft(find.byType(ListTile));
leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
trailingOffset = tester.getTopRight(find.byKey(trailingKey));
// If the tile height is less than 72.0 pixels, the leading widget is placed
// 16.0 pixels below the top of the title widget, and the trailing is centered
// vertically in the tile.
const double titlePosition = 16.0;
expect(leadingOffset.dy - tileOffset.dy, titlePosition);
expect(trailingOffset.dy - tileOffset.dy, centerPosition);
// Test [ListTileTitleAlignment.top] alignment.
await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.top));
tileOffset = tester.getTopLeft(find.byType(ListTile));
leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
trailingOffset = tester.getTopRight(find.byKey(trailingKey));
// Leading and trailing widgets are placed minVerticalPadding below
// the top of the title widget.
const double topPosition = minVerticalPadding;
expect(leadingOffset.dy - tileOffset.dy, topPosition);
expect(trailingOffset.dy - tileOffset.dy, topPosition);
// Test [ListTileTitleAlignment.center] alignment.
await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.center));
tileOffset = tester.getTopLeft(find.byType(ListTile));
leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
trailingOffset = tester.getTopRight(find.byKey(trailingKey));
// Leading and trailing widgets are centered vertically in the tile.
expect(leadingOffset.dy - tileOffset.dy, centerPosition);
expect(trailingOffset.dy - tileOffset.dy, centerPosition);
// Test [ListTileTitleAlignment.bottom] alignment.
await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.bottom));
tileOffset = tester.getTopLeft(find.byType(ListTile));
leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
trailingOffset = tester.getTopRight(find.byKey(trailingKey));
// Leading and trailing widgets are placed minVerticalPadding above
// the bottom of the subtitle widget.
const double bottomPosition = tileHeight - minVerticalPadding - leadingHeight;
expect(leadingOffset.dy - tileOffset.dy, bottomPosition);
expect(trailingOffset.dy - tileOffset.dy, bottomPosition);
});
testWidgets('titleAlignment position with title and subtitle widgets', (WidgetTester tester) async {
final Key leadingKey = GlobalKey();
final Key trailingKey = GlobalKey();
const double leadingHeight = 24.0;
const double titleHeight = 50.0;
const double subtitleHeight = 50.0;
const double trailingHeight = 24.0;
const double minVerticalPadding = 10.0;
const double tileHeight = minVerticalPadding * 2 + titleHeight + subtitleHeight;
Widget buildFrame({ ListTileTitleAlignment? titleAlignment }) {
return MaterialApp(
theme: ThemeData(useMaterial3: true),
home: Material(
child: Center(
child: ListTile(
titleAlignment: titleAlignment,
minVerticalPadding: minVerticalPadding,
leading: SizedBox(key: leadingKey, width: 24.0, height: leadingHeight),
title: const SizedBox(width: 20.0, height: titleHeight),
subtitle: const SizedBox(width: 20.0, height: subtitleHeight),
trailing: SizedBox(key: trailingKey, width: 24.0, height: trailingHeight),
),
),
),
);
}
// If [ThemeData.useMaterial3] is true, the default title alignment is
// [ListTileTitleAlignment.threeLine], which positions the leading and
// trailing widgets center vertically in the tile if the [ListTile.isThreeLine]
// property is false.
await tester.pumpWidget(buildFrame());
Offset tileOffset = tester.getTopLeft(find.byType(ListTile));
Offset leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
Offset trailingOffset = tester.getTopRight(find.byKey(trailingKey));
// Leading and trailing widgets are centered vertically in the tile.
const double centerPosition = (tileHeight / 2) - (leadingHeight / 2);
expect(leadingOffset.dy - tileOffset.dy, centerPosition);
expect(trailingOffset.dy - tileOffset.dy, centerPosition);
// Test [ListTileTitleAlignment.threeLine] alignment.
await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.threeLine));
tileOffset = tester.getTopLeft(find.byType(ListTile));
leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
trailingOffset = tester.getTopRight(find.byKey(trailingKey));
// Leading and trailing widgets are centered vertically in the tile,
// If the [ListTile.isThreeLine] property is false.
expect(leadingOffset.dy - tileOffset.dy, centerPosition);
expect(trailingOffset.dy - tileOffset.dy, centerPosition);
// Test [ListTileTitleAlignment.titleHeight] alignment.
await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.titleHeight));
tileOffset = tester.getTopLeft(find.byType(ListTile));
leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
trailingOffset = tester.getTopRight(find.byKey(trailingKey));
// Leading and trailing widgets are positioned 16.0 pixels below the
// top of the title widget.
const double titlePosition = 16.0;
expect(leadingOffset.dy - tileOffset.dy, titlePosition);
expect(trailingOffset.dy - tileOffset.dy, titlePosition);
// Test [ListTileTitleAlignment.top] alignment.
await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.top));
tileOffset = tester.getTopLeft(find.byType(ListTile));
leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
trailingOffset = tester.getTopRight(find.byKey(trailingKey));
// Leading and trailing widgets are placed minVerticalPadding below
// the top of the title widget.
const double topPosition = minVerticalPadding;
expect(leadingOffset.dy - tileOffset.dy, topPosition);
expect(trailingOffset.dy - tileOffset.dy, topPosition);
// Test [ListTileTitleAlignment.center] alignment.
await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.center));
tileOffset = tester.getTopLeft(find.byType(ListTile));
leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
trailingOffset = tester.getTopRight(find.byKey(trailingKey));
// Leading and trailing widgets are centered vertically in the tile.
expect(leadingOffset.dy - tileOffset.dy, centerPosition);
expect(trailingOffset.dy - tileOffset.dy, centerPosition);
// Test [ListTileTitleAlignment.bottom] alignment.
await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.bottom));
tileOffset = tester.getTopLeft(find.byType(ListTile));
leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
trailingOffset = tester.getTopRight(find.byKey(trailingKey));
// Leading and trailing widgets are placed minVerticalPadding above
// the bottom of the subtitle widget.
const double bottomPosition = tileHeight - minVerticalPadding - leadingHeight;
expect(leadingOffset.dy - tileOffset.dy, bottomPosition);
expect(trailingOffset.dy - tileOffset.dy, bottomPosition);
});
testWidgets("ListTile.isThreeLine updates ListTileTitleAlignment.threeLine's alignment", (WidgetTester tester) async {
final Key leadingKey = GlobalKey();
final Key trailingKey = GlobalKey();
const double leadingHeight = 24.0;
const double titleHeight = 50.0;
const double subtitleHeight = 50.0;
const double trailingHeight = 24.0;
const double minVerticalPadding = 10.0;
const double tileHeight = minVerticalPadding * 2 + titleHeight + subtitleHeight;
Widget buildFrame({ ListTileTitleAlignment? titleAlignment, bool isThreeLine = false }) {
return MaterialApp(
theme: ThemeData(useMaterial3: true),
home: Material(
child: Center(
child: ListTile(
titleAlignment: titleAlignment,
minVerticalPadding: minVerticalPadding,
leading: SizedBox(key: leadingKey, width: 24.0, height: leadingHeight),
title: const SizedBox(width: 20.0, height: titleHeight),
subtitle: const SizedBox(width: 20.0, height: subtitleHeight),
trailing: SizedBox(key: trailingKey, width: 24.0, height: trailingHeight),
isThreeLine: isThreeLine,
),
),
),
);
}
// If [ThemeData.useMaterial3] is true, then title alignment should
// default to [ListTileTitleAlignment.threeLine].
await tester.pumpWidget(buildFrame());
Offset tileOffset = tester.getTopLeft(find.byType(ListTile));
Offset leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
Offset trailingOffset = tester.getTopRight(find.byKey(trailingKey));
// By default, leading and trailing widgets are centered vertically
// in the tile.
const double centerPosition = (tileHeight / 2) - (leadingHeight / 2);
expect(leadingOffset.dy - tileOffset.dy, centerPosition);
expect(trailingOffset.dy - tileOffset.dy, centerPosition);
// Set [ListTile.isThreeLine] to true to update the alignment.
await tester.pumpWidget(buildFrame(isThreeLine: true));
tileOffset = tester.getTopLeft(find.byType(ListTile));
leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
trailingOffset = tester.getTopRight(find.byKey(trailingKey));
// The leading and trailing widgets are placed minVerticalPadding
// to the top of the tile widget.
const double topPosition = minVerticalPadding;
expect(leadingOffset.dy - tileOffset.dy, topPosition);
expect(trailingOffset.dy - tileOffset.dy, topPosition);
});
group('Material 2', () {
// Tests that are only relevant for Material 2. Once ThemeData.useMaterial3
// is turned on by default, these tests can be removed.
......@@ -3462,6 +3718,258 @@ void main() {
expect(find.byType(Material), paints..rect(color: defaultColor));
});
testWidgets('titleAlignment position with title widget', (WidgetTester tester) async {
final Key leadingKey = GlobalKey();
final Key trailingKey = GlobalKey();
const double leadingHeight = 24.0;
const double titleHeight = 50.0;
const double trailingHeight = 24.0;
const double minVerticalPadding = 10.0;
const double tileHeight = minVerticalPadding * 2 + titleHeight;
Widget buildFrame({ ListTileTitleAlignment? titleAlignment }) {
return MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Material(
child: Center(
child: ListTile(
titleAlignment: titleAlignment,
minVerticalPadding: minVerticalPadding,
leading: SizedBox(key: leadingKey, width: 24.0, height: leadingHeight),
title: const SizedBox(width: 20.0, height: titleHeight),
trailing: SizedBox(key: trailingKey, width: 24.0, height: trailingHeight),
),
),
),
);
}
// If [ThemeData.useMaterial3] is false, the default title alignment is
// [ListTileTitleAlignment.titleHeight], If the tile height is less than
// 72.0 pixels, the leading is placed 16.0 pixels below the top of
// the title widget and the trailing is centered vertically in the tile.
await tester.pumpWidget(buildFrame());
Offset tileOffset = tester.getTopLeft(find.byType(ListTile));
Offset leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
Offset trailingOffset = tester.getTopRight(find.byKey(trailingKey));
// Leading and trailing widgets are centered vertically in the tile.
const double titlePosition = 16.0;
const double centerPosition = (tileHeight / 2) - (leadingHeight / 2);
expect(leadingOffset.dy - tileOffset.dy, titlePosition);
expect(trailingOffset.dy - tileOffset.dy, centerPosition);
// Test [ListTileTitleAlignment.threeLine] alignment.
await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.threeLine));
tileOffset = tester.getTopLeft(find.byType(ListTile));
leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
trailingOffset = tester.getTopRight(find.byKey(trailingKey));
// Leading and trailing widgets are centered vertically in the tile,
// If the [ListTile.isThreeLine] property is false.
expect(leadingOffset.dy - tileOffset.dy, centerPosition);
expect(trailingOffset.dy - tileOffset.dy, centerPosition);
// Test [ListTileTitleAlignment.titleHeight] alignment.
await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.titleHeight));
tileOffset = tester.getTopLeft(find.byType(ListTile));
leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
trailingOffset = tester.getTopRight(find.byKey(trailingKey));
// If the tile height is less than 72.0 pixels, the leading is placed
// 16.0 pixels below the top of the tile widget, and the trailing is
// centered vertically in the tile.
expect(leadingOffset.dy - tileOffset.dy, titlePosition);
expect(trailingOffset.dy - tileOffset.dy, centerPosition);
// Test [ListTileTitleAlignment.top] alignment.
await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.top));
tileOffset = tester.getTopLeft(find.byType(ListTile));
leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
trailingOffset = tester.getTopRight(find.byKey(trailingKey));
// Leading and trailing widgets are placed minVerticalPadding below
// the top of the title widget.
const double topPosition = minVerticalPadding;
expect(leadingOffset.dy - tileOffset.dy, topPosition);
expect(trailingOffset.dy - tileOffset.dy, topPosition);
// Test [ListTileTitleAlignment.center] alignment.
await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.center));
tileOffset = tester.getTopLeft(find.byType(ListTile));
leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
trailingOffset = tester.getTopRight(find.byKey(trailingKey));
// Leading and trailing widgets are vertically centered in the tile.
expect(leadingOffset.dy - tileOffset.dy, centerPosition);
expect(trailingOffset.dy - tileOffset.dy, centerPosition);
// Test [ListTileTitleAlignment.bottom] alignment.
await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.bottom));
tileOffset = tester.getTopLeft(find.byType(ListTile));
leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
trailingOffset = tester.getTopRight(find.byKey(trailingKey));
// Leading and trailing widgets are placed minVerticalPadding above
// the bottom of the subtitle widget.
const double bottomPosition = tileHeight - minVerticalPadding - leadingHeight;
expect(leadingOffset.dy - tileOffset.dy, bottomPosition);
expect(trailingOffset.dy - tileOffset.dy, bottomPosition);
});
testWidgets('titleAlignment position with title and subtitle widgets', (WidgetTester tester) async {
final Key leadingKey = GlobalKey();
final Key trailingKey = GlobalKey();
const double leadingHeight = 24.0;
const double titleHeight = 50.0;
const double subtitleHeight = 50.0;
const double trailingHeight = 24.0;
const double minVerticalPadding = 10.0;
const double tileHeight = minVerticalPadding * 2 + titleHeight + subtitleHeight;
Widget buildFrame({ ListTileTitleAlignment? titleAlignment }) {
return MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Material(
child: Center(
child: ListTile(
titleAlignment: titleAlignment,
minVerticalPadding: minVerticalPadding,
leading: SizedBox(key: leadingKey, width: 24.0, height: leadingHeight),
title: const SizedBox(width: 20.0, height: titleHeight),
subtitle: const SizedBox(width: 20.0, height: subtitleHeight),
trailing: SizedBox(key: trailingKey, width: 24.0, height: trailingHeight),
),
),
),
);
}
// If [ThemeData.useMaterial3] is false, the default title alignment is
// [ListTileTitleAlignment.titleHeight], which positions the leading and
// trailing widgets 16.0 pixels below the top of the tile widget.
await tester.pumpWidget(buildFrame());
Offset tileOffset = tester.getTopLeft(find.byType(ListTile));
Offset leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
Offset trailingOffset = tester.getTopRight(find.byKey(trailingKey));
// Leading and trailing widgets are positioned 16.0 pixels below the
// top of the tile widget.
const double titlePosition = 16.0;
expect(leadingOffset.dy - tileOffset.dy, titlePosition);
expect(trailingOffset.dy - tileOffset.dy, titlePosition);
// Test [ListTileTitleAlignment.threeLine] alignment.
await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.threeLine));
tileOffset = tester.getTopLeft(find.byType(ListTile));
leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
trailingOffset = tester.getTopRight(find.byKey(trailingKey));
// Leading and trailing widgets are vertically centered in the tile,
// If the [ListTile.isThreeLine] property is false.
const double centerPosition = (tileHeight / 2) - (leadingHeight / 2);
expect(leadingOffset.dy - tileOffset.dy, centerPosition);
expect(trailingOffset.dy - tileOffset.dy, centerPosition);
// Test [ListTileTitleAlignment.titleHeight] alignment.
await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.titleHeight));
tileOffset = tester.getTopLeft(find.byType(ListTile));
leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
trailingOffset = tester.getTopRight(find.byKey(trailingKey));
// Leading and trailing widgets are positioned 16.0 pixels below the
// top of the tile widget.
expect(leadingOffset.dy - tileOffset.dy, titlePosition);
expect(trailingOffset.dy - tileOffset.dy, titlePosition);
// Test [ListTileTitleAlignment.top] alignment.
await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.top));
tileOffset = tester.getTopLeft(find.byType(ListTile));
leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
trailingOffset = tester.getTopRight(find.byKey(trailingKey));
// Leading and trailing widgets are placed minVerticalPadding below
// the top of the tile widget.
const double topPosition = minVerticalPadding;
expect(leadingOffset.dy - tileOffset.dy, topPosition);
expect(trailingOffset.dy - tileOffset.dy, topPosition);
// Test [ListTileTitleAlignment.center] alignment.
await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.center));
tileOffset = tester.getTopLeft(find.byType(ListTile));
leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
trailingOffset = tester.getTopRight(find.byKey(trailingKey));
// Leading and trailing widgets are vertically centered in the tile.
expect(leadingOffset.dy - tileOffset.dy, centerPosition);
expect(trailingOffset.dy - tileOffset.dy, centerPosition);
// Test [ListTileTitleAlignment.bottom] alignment.
await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.bottom));
tileOffset = tester.getTopLeft(find.byType(ListTile));
leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
trailingOffset = tester.getTopRight(find.byKey(trailingKey));
// Leading and trailing widgets are placed minVerticalPadding above
// the bottom of the subtitle widget.
const double bottomPosition = tileHeight - minVerticalPadding - leadingHeight;
expect(leadingOffset.dy - tileOffset.dy, bottomPosition);
expect(trailingOffset.dy - tileOffset.dy, bottomPosition);
});
testWidgets("ListTile.isThreeLine updates ListTileTitleAlignment.threeLine's alignment", (WidgetTester tester) async {
final Key leadingKey = GlobalKey();
final Key trailingKey = GlobalKey();
const double leadingHeight = 24.0;
const double titleHeight = 50.0;
const double subtitleHeight = 50.0;
const double trailingHeight = 24.0;
const double minVerticalPadding = 10.0;
const double tileHeight = minVerticalPadding * 2 + titleHeight + subtitleHeight;
Widget buildFrame({ ListTileTitleAlignment? titleAlignment, bool isThreeLine = false }) {
return MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Material(
child: Center(
child: ListTile(
titleAlignment: titleAlignment,
minVerticalPadding: minVerticalPadding,
leading: SizedBox(key: leadingKey, width: 24.0, height: leadingHeight),
title: const SizedBox(width: 20.0, height: titleHeight),
subtitle: const SizedBox(width: 20.0, height: subtitleHeight),
trailing: SizedBox(key: trailingKey, width: 24.0, height: trailingHeight),
isThreeLine: isThreeLine,
),
),
),
);
}
// Set title alignment to threeLine.
await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.threeLine));
Offset tileOffset = tester.getTopLeft(find.byType(ListTile));
Offset leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
Offset trailingOffset = tester.getTopRight(find.byKey(trailingKey));
// If title alignment is threeLine and [ListTile.isThreeLine] is false,
// leading and trailing widgets are centered vertically in the tile.
const double leadingTrailingPosition = (tileHeight / 2) - (leadingHeight / 2);
expect(leadingOffset.dy - tileOffset.dy, leadingTrailingPosition);
expect(trailingOffset.dy - tileOffset.dy, leadingTrailingPosition);
// Set [ListTile.isThreeLine] to true to update the alignment.
await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.threeLine, isThreeLine: true));
tileOffset = tester.getTopLeft(find.byType(ListTile));
leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
trailingOffset = tester.getTopRight(find.byKey(trailingKey));
// The leading and trailing widgets are placed minVerticalPadding
// to the top of the tile widget.
expect(leadingOffset.dy - tileOffset.dy, minVerticalPadding);
expect(trailingOffset.dy - tileOffset.dy, minVerticalPadding);
});
});
}
......
......@@ -71,6 +71,7 @@ void main() {
expect(themeData.enableFeedback, null);
expect(themeData.mouseCursor, null);
expect(themeData.visualDensity, null);
expect(themeData.titleAlignment, null);
});
testWidgets('Default ListTileThemeData debugFillProperties', (WidgetTester tester) async {
......@@ -106,6 +107,7 @@ void main() {
enableFeedback: true,
mouseCursor: MaterialStateMouseCursor.clickable,
visualDensity: VisualDensity.comfortable,
titleAlignment: ListTileTitleAlignment.top,
).debugFillProperties(builder);
final List<String> description = builder.properties
......@@ -134,6 +136,7 @@ void main() {
'enableFeedback: true',
'mouseCursor: MaterialStateMouseCursor(clickable)',
'visualDensity: VisualDensity#00000(h: -1.0, v: -1.0)(horizontal: -1.0, vertical: -1.0)',
'titleAlignment: ListTileTitleAlignment.top',
]),
);
});
......@@ -215,6 +218,7 @@ void main() {
selectedColor: selectedColor,
iconColor: iconColor,
textColor: textColor,
minVerticalPadding: 25.0,
mouseCursor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return SystemMouseCursors.forbidden;
......@@ -223,6 +227,7 @@ void main() {
return SystemMouseCursors.click;
}),
visualDensity: VisualDensity.compact,
titleAlignment: ListTileTitleAlignment.bottom,
),
child: Builder(
builder: (BuildContext context) {
......@@ -311,7 +316,14 @@ void main() {
// VisualDensity is respected
final RenderBox box = tester.renderObject(find.byKey(listTileKey));
expect(box.size, equals(const Size(800, 64.0)));
expect(box.size, equals(const Size(800, 80.0)));
// titleAlignment is respected.
final Offset titleOffset = tester.getTopLeft(find.text('title'));
final Offset leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
final Offset trailingOffset = tester.getTopRight(find.byKey(trailingKey));
expect(leadingOffset.dy - titleOffset.dy, 6);
expect(trailingOffset.dy - titleOffset.dy, 6);
});
testWidgets('ListTileTheme colors are applied to leading and trailing text widgets', (WidgetTester tester) async {
......@@ -721,6 +733,7 @@ void main() {
minVerticalPadding: 300,
minLeadingWidth: 400,
enableFeedback: true,
titleAlignment: ListTileTitleAlignment.bottom,
);
final ListTileThemeData copy = original.copyWith(
......@@ -740,6 +753,7 @@ void main() {
minVerticalPadding: 700,
minLeadingWidth: 800,
enableFeedback: false,
titleAlignment: ListTileTitleAlignment.top,
);
expect(copy.dense, false);
......@@ -758,6 +772,43 @@ void main() {
expect(copy.minVerticalPadding, 700);
expect(copy.minLeadingWidth, 800);
expect(copy.enableFeedback, false);
expect(copy.titleAlignment, ListTileTitleAlignment.top);
});
testWidgets('ListTileTheme.titleAlignment is overridden by ListTile.titleAlignment', (WidgetTester tester) async {
final Key leadingKey = GlobalKey();
final Key trailingKey = GlobalKey();
const String titleText = '\nHeadline Text\n';
const String subtitleText = '\nSupporting Text\n';
Widget buildFrame({ ListTileTitleAlignment? alignment }) {
return MaterialApp(
theme: ThemeData(
useMaterial3: true,
listTileTheme: const ListTileThemeData(
titleAlignment: ListTileTitleAlignment.center,
),
),
home: Material(
child: Center(
child: ListTile(
titleAlignment: ListTileTitleAlignment.top,
leading: SizedBox(key: leadingKey, width: 24.0, height: 24.0),
title: const Text(titleText),
subtitle: const Text(subtitleText),
trailing: SizedBox(key: trailingKey, width: 24.0, height: 24.0),
),
),
),
);
}
await tester.pumpWidget(buildFrame());
final Offset tileOffset = tester.getTopLeft(find.byType(ListTile));
final Offset leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
final Offset trailingOffset = tester.getTopRight(find.byKey(trailingKey));
expect(leadingOffset.dy - tileOffset.dy, 8.0);
expect(trailingOffset.dy - tileOffset.dy, 8.0);
});
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment