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 { ...@@ -63,6 +63,49 @@ enum ListTileControlAffinity {
platform, 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 single fixed-height row that typically contains some text as well as
/// a leading or trailing icon. /// a leading or trailing icon.
/// ///
...@@ -156,6 +199,14 @@ enum ListTileControlAffinity { ...@@ -156,6 +199,14 @@ enum ListTileControlAffinity {
/// ** See code in examples/api/lib/material/list_tile/list_tile.3.dart ** /// ** See code in examples/api/lib/material/list_tile/list_tile.3.dart **
/// {@end-tool} /// {@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} /// {@tool snippet}
/// To use a [ListTile] within a [Row], it needs to be wrapped in an /// To use a [ListTile] within a [Row], it needs to be wrapped in an
/// [Expanded] widget. [ListTile] requires fixed width constraints, /// [Expanded] widget. [ListTile] requires fixed width constraints,
...@@ -317,6 +368,7 @@ class ListTile extends StatelessWidget { ...@@ -317,6 +368,7 @@ class ListTile extends StatelessWidget {
this.horizontalTitleGap, this.horizontalTitleGap,
this.minVerticalPadding, this.minVerticalPadding,
this.minLeadingWidth, this.minLeadingWidth,
this.titleAlignment,
}) : assert(!isThreeLine || subtitle != null); }) : assert(!isThreeLine || subtitle != null);
/// A widget to display before the title. /// A widget to display before the title.
...@@ -616,6 +668,20 @@ class ListTile extends StatelessWidget { ...@@ -616,6 +668,20 @@ class ListTile extends StatelessWidget {
/// that is also null, then a default value of 40 is used. /// that is also null, then a default value of 40 is used.
final double? minLeadingWidth; 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 /// Add a one pixel border in between each tile. If color isn't specified the
/// [ThemeData.dividerColor] of the context's [Theme] is used. /// [ThemeData.dividerColor] of the context's [Theme] is used.
/// ///
...@@ -761,6 +827,7 @@ class ListTile extends StatelessWidget { ...@@ -761,6 +827,7 @@ class ListTile extends StatelessWidget {
final EdgeInsets resolvedContentPadding = contentPadding?.resolve(textDirection) final EdgeInsets resolvedContentPadding = contentPadding?.resolve(textDirection)
?? tileTheme.contentPadding?.resolve(textDirection) ?? tileTheme.contentPadding?.resolve(textDirection)
?? defaults.contentPadding!.resolve(textDirection); ?? defaults.contentPadding!.resolve(textDirection);
// Show basic cursor when ListTile isn't enabled or gesture callbacks are null. // Show basic cursor when ListTile isn't enabled or gesture callbacks are null.
final Set<MaterialState> mouseStates = <MaterialState>{ final Set<MaterialState> mouseStates = <MaterialState>{
if (!enabled || (onTap == null && onLongPress == null)) MaterialState.disabled, if (!enabled || (onTap == null && onLongPress == null)) MaterialState.disabled,
...@@ -769,6 +836,10 @@ class ListTile extends StatelessWidget { ...@@ -769,6 +836,10 @@ class ListTile extends StatelessWidget {
?? tileTheme.mouseCursor?.resolve(mouseStates) ?? tileTheme.mouseCursor?.resolve(mouseStates)
?? MaterialStateMouseCursor.clickable.resolve(mouseStates); ?? MaterialStateMouseCursor.clickable.resolve(mouseStates);
final ListTileTitleAlignment effectiveTitleAlignment = titleAlignment
?? tileTheme.titleAlignment
?? (theme.useMaterial3 ? ListTileTitleAlignment.threeLine : ListTileTitleAlignment.titleHeight);
return InkWell( return InkWell(
customBorder: shape ?? tileTheme.shape, customBorder: shape ?? tileTheme.shape,
onTap: enabled ? onTap : null, onTap: enabled ? onTap : null,
...@@ -812,7 +883,7 @@ class ListTile extends StatelessWidget { ...@@ -812,7 +883,7 @@ class ListTile extends StatelessWidget {
horizontalTitleGap: horizontalTitleGap ?? tileTheme.horizontalTitleGap ?? 16, horizontalTitleGap: horizontalTitleGap ?? tileTheme.horizontalTitleGap ?? 16,
minVerticalPadding: minVerticalPadding ?? tileTheme.minVerticalPadding ?? defaults.minVerticalPadding!, minVerticalPadding: minVerticalPadding ?? tileTheme.minVerticalPadding ?? defaults.minVerticalPadding!,
minLeadingWidth: minLeadingWidth ?? tileTheme.minLeadingWidth ?? defaults.minLeadingWidth!, minLeadingWidth: minLeadingWidth ?? tileTheme.minLeadingWidth ?? defaults.minLeadingWidth!,
material3: theme.useMaterial3, titleAlignment: effectiveTitleAlignment,
), ),
), ),
), ),
...@@ -856,6 +927,7 @@ class ListTile extends StatelessWidget { ...@@ -856,6 +927,7 @@ class ListTile extends StatelessWidget {
properties.add(DoubleProperty('horizontalTitleGap', horizontalTitleGap, defaultValue: null)); properties.add(DoubleProperty('horizontalTitleGap', horizontalTitleGap, defaultValue: null));
properties.add(DoubleProperty('minVerticalPadding', minVerticalPadding, defaultValue: null)); properties.add(DoubleProperty('minVerticalPadding', minVerticalPadding, defaultValue: null));
properties.add(DoubleProperty('minLeadingWidth', minLeadingWidth, 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 ...@@ -910,7 +982,7 @@ class _ListTile extends RenderObjectWidget with SlottedMultiChildRenderObjectWid
required this.minVerticalPadding, required this.minVerticalPadding,
required this.minLeadingWidth, required this.minLeadingWidth,
this.subtitleBaselineType, this.subtitleBaselineType,
required this.material3, required this.titleAlignment,
}); });
final Widget? leading; final Widget? leading;
...@@ -926,7 +998,7 @@ class _ListTile extends RenderObjectWidget with SlottedMultiChildRenderObjectWid ...@@ -926,7 +998,7 @@ class _ListTile extends RenderObjectWidget with SlottedMultiChildRenderObjectWid
final double horizontalTitleGap; final double horizontalTitleGap;
final double minVerticalPadding; final double minVerticalPadding;
final double minLeadingWidth; final double minLeadingWidth;
final bool material3; final ListTileTitleAlignment titleAlignment;
@override @override
Iterable<_ListTileSlot> get slots => _ListTileSlot.values; Iterable<_ListTileSlot> get slots => _ListTileSlot.values;
...@@ -957,7 +1029,7 @@ class _ListTile extends RenderObjectWidget with SlottedMultiChildRenderObjectWid ...@@ -957,7 +1029,7 @@ class _ListTile extends RenderObjectWidget with SlottedMultiChildRenderObjectWid
horizontalTitleGap: horizontalTitleGap, horizontalTitleGap: horizontalTitleGap,
minVerticalPadding: minVerticalPadding, minVerticalPadding: minVerticalPadding,
minLeadingWidth: minLeadingWidth, minLeadingWidth: minLeadingWidth,
material3: material3, titleAlignment: titleAlignment,
); );
} }
...@@ -973,7 +1045,7 @@ class _ListTile extends RenderObjectWidget with SlottedMultiChildRenderObjectWid ...@@ -973,7 +1045,7 @@ class _ListTile extends RenderObjectWidget with SlottedMultiChildRenderObjectWid
..horizontalTitleGap = horizontalTitleGap ..horizontalTitleGap = horizontalTitleGap
..minLeadingWidth = minLeadingWidth ..minLeadingWidth = minLeadingWidth
..minVerticalPadding = minVerticalPadding ..minVerticalPadding = minVerticalPadding
..material3 = material3; ..titleAlignment = titleAlignment;
} }
} }
...@@ -988,7 +1060,7 @@ class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_ ...@@ -988,7 +1060,7 @@ class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_
required double horizontalTitleGap, required double horizontalTitleGap,
required double minVerticalPadding, required double minVerticalPadding,
required double minLeadingWidth, required double minLeadingWidth,
required bool material3, required ListTileTitleAlignment titleAlignment,
}) : _isDense = isDense, }) : _isDense = isDense,
_visualDensity = visualDensity, _visualDensity = visualDensity,
_isThreeLine = isThreeLine, _isThreeLine = isThreeLine,
...@@ -998,7 +1070,7 @@ class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_ ...@@ -998,7 +1070,7 @@ class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_
_horizontalTitleGap = horizontalTitleGap, _horizontalTitleGap = horizontalTitleGap,
_minVerticalPadding = minVerticalPadding, _minVerticalPadding = minVerticalPadding,
_minLeadingWidth = minLeadingWidth, _minLeadingWidth = minLeadingWidth,
_material3 = material3; _titleAlignment = titleAlignment;
RenderBox? get leading => childForSlot(_ListTileSlot.leading); RenderBox? get leading => childForSlot(_ListTileSlot.leading);
RenderBox? get title => childForSlot(_ListTileSlot.title); RenderBox? get title => childForSlot(_ListTileSlot.title);
...@@ -1114,13 +1186,13 @@ class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_ ...@@ -1114,13 +1186,13 @@ class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_
markNeedsLayout(); markNeedsLayout();
} }
bool get material3 => _material3; ListTileTitleAlignment get titleAlignment => _titleAlignment;
bool _material3; ListTileTitleAlignment _titleAlignment;
set material3(bool value) { set titleAlignment(ListTileTitleAlignment value) {
if (_material3 == value) { if (_titleAlignment == value) {
return; return;
} }
_material3 = value; _titleAlignment = value;
markNeedsLayout(); markNeedsLayout();
} }
...@@ -1314,30 +1386,51 @@ class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_ ...@@ -1314,30 +1386,51 @@ class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_
final double leadingY; final double leadingY;
final double trailingY; final double trailingY;
if (material3) {
if (isThreeLine) { switch (titleAlignment) {
case ListTileTitleAlignment.threeLine: {
if (isThreeLine) {
leadingY = _minVerticalPadding;
trailingY = _minVerticalPadding;
} else {
leadingY = (tileHeight - leadingSize.height) / 2.0;
trailingY = (tileHeight - trailingSize.height) / 2.0;
}
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://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.
// - For smaller tiles, trailing should always be centered. Leading can be
// centered or closer to the top. It should never be further than 16dp
// to the top.
if (tileHeight > 72.0) {
leadingY = 16.0;
trailingY = 16.0;
} else {
leadingY = math.min((tileHeight - leadingSize.height) / 2.0, 16.0);
trailingY = (tileHeight - trailingSize.height) / 2.0;
}
break;
}
case ListTileTitleAlignment.top: {
leadingY = _minVerticalPadding; leadingY = _minVerticalPadding;
trailingY = _minVerticalPadding; trailingY = _minVerticalPadding;
} else { break;
}
case ListTileTitleAlignment.center: {
leadingY = (tileHeight - leadingSize.height) / 2.0; leadingY = (tileHeight - leadingSize.height) / 2.0;
trailingY = (tileHeight - trailingSize.height) / 2.0; trailingY = (tileHeight - trailingSize.height) / 2.0;
break;
} }
} else { case ListTileTitleAlignment.bottom: {
// This attempts to implement the redlines for the vertical position of the leadingY = tileHeight - leadingSize.height - _minVerticalPadding;
// leading and trailing icons on the spec page: trailingY = tileHeight - trailingSize.height - _minVerticalPadding;
// https://material.io/design/components/lists.html#specs break;
// 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.
// - For smaller tiles, trailing should always be centered. Leading can be
// centered or closer to the top. It should never be further than 16dp
// to the top.
if (tileHeight > 72.0) {
leadingY = 16.0;
trailingY = 16.0;
} else {
leadingY = math.min((tileHeight - leadingSize.height) / 2.0, 16.0);
trailingY = (tileHeight - trailingSize.height) / 2.0;
} }
} }
......
...@@ -63,6 +63,7 @@ class ListTileThemeData with Diagnosticable { ...@@ -63,6 +63,7 @@ class ListTileThemeData with Diagnosticable {
this.enableFeedback, this.enableFeedback,
this.mouseCursor, this.mouseCursor,
this.visualDensity, this.visualDensity,
this.titleAlignment,
}); });
/// Overrides the default value of [ListTile.dense]. /// Overrides the default value of [ListTile.dense].
...@@ -119,6 +120,9 @@ class ListTileThemeData with Diagnosticable { ...@@ -119,6 +120,9 @@ class ListTileThemeData with Diagnosticable {
/// If specified, overrides the default value of [ListTile.visualDensity]. /// If specified, overrides the default value of [ListTile.visualDensity].
final VisualDensity? 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 /// Creates a copy of this object with the given fields replaced with the
/// new values. /// new values.
ListTileThemeData copyWith({ ListTileThemeData copyWith({
...@@ -141,6 +145,7 @@ class ListTileThemeData with Diagnosticable { ...@@ -141,6 +145,7 @@ class ListTileThemeData with Diagnosticable {
MaterialStateProperty<MouseCursor?>? mouseCursor, MaterialStateProperty<MouseCursor?>? mouseCursor,
bool? isThreeLine, bool? isThreeLine,
VisualDensity? visualDensity, VisualDensity? visualDensity,
ListTileTitleAlignment? titleAlignment,
}) { }) {
return ListTileThemeData( return ListTileThemeData(
dense: dense ?? this.dense, dense: dense ?? this.dense,
...@@ -161,6 +166,7 @@ class ListTileThemeData with Diagnosticable { ...@@ -161,6 +166,7 @@ class ListTileThemeData with Diagnosticable {
enableFeedback: enableFeedback ?? this.enableFeedback, enableFeedback: enableFeedback ?? this.enableFeedback,
mouseCursor: mouseCursor ?? this.mouseCursor, mouseCursor: mouseCursor ?? this.mouseCursor,
visualDensity: visualDensity ?? this.visualDensity, visualDensity: visualDensity ?? this.visualDensity,
titleAlignment: titleAlignment ?? this.titleAlignment,
); );
} }
...@@ -188,6 +194,7 @@ class ListTileThemeData with Diagnosticable { ...@@ -188,6 +194,7 @@ class ListTileThemeData with Diagnosticable {
enableFeedback: t < 0.5 ? a?.enableFeedback : b?.enableFeedback, enableFeedback: t < 0.5 ? a?.enableFeedback : b?.enableFeedback,
mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor, mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor,
visualDensity: t < 0.5 ? a?.visualDensity : b?.visualDensity, visualDensity: t < 0.5 ? a?.visualDensity : b?.visualDensity,
titleAlignment: t < 0.5 ? a?.titleAlignment : b?.titleAlignment,
); );
} }
...@@ -211,6 +218,7 @@ class ListTileThemeData with Diagnosticable { ...@@ -211,6 +218,7 @@ class ListTileThemeData with Diagnosticable {
enableFeedback, enableFeedback,
mouseCursor, mouseCursor,
visualDensity, visualDensity,
titleAlignment,
); );
@override @override
...@@ -239,7 +247,8 @@ class ListTileThemeData with Diagnosticable { ...@@ -239,7 +247,8 @@ class ListTileThemeData with Diagnosticable {
&& other.minLeadingWidth == minLeadingWidth && other.minLeadingWidth == minLeadingWidth
&& other.enableFeedback == enableFeedback && other.enableFeedback == enableFeedback
&& other.mouseCursor == mouseCursor && other.mouseCursor == mouseCursor
&& other.visualDensity == visualDensity; && other.visualDensity == visualDensity
&& other.titleAlignment == titleAlignment;
} }
@override @override
...@@ -263,6 +272,7 @@ class ListTileThemeData with Diagnosticable { ...@@ -263,6 +272,7 @@ class ListTileThemeData with Diagnosticable {
properties.add(DiagnosticsProperty<bool>('enableFeedback', enableFeedback, defaultValue: null)); properties.add(DiagnosticsProperty<bool>('enableFeedback', enableFeedback, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: null)); properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: null));
properties.add(DiagnosticsProperty<VisualDensity>('visualDensity', visualDensity, 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 { ...@@ -477,6 +487,7 @@ class ListTileTheme extends InheritedTheme {
double? horizontalTitleGap, double? horizontalTitleGap,
double? minVerticalPadding, double? minVerticalPadding,
double? minLeadingWidth, double? minLeadingWidth,
ListTileTitleAlignment? titleAlignment,
required Widget child, required Widget child,
}) { }) {
return Builder( return Builder(
...@@ -498,6 +509,7 @@ class ListTileTheme extends InheritedTheme { ...@@ -498,6 +509,7 @@ class ListTileTheme extends InheritedTheme {
horizontalTitleGap: horizontalTitleGap ?? parent.horizontalTitleGap, horizontalTitleGap: horizontalTitleGap ?? parent.horizontalTitleGap,
minVerticalPadding: minVerticalPadding ?? parent.minVerticalPadding, minVerticalPadding: minVerticalPadding ?? parent.minVerticalPadding,
minLeadingWidth: minLeadingWidth ?? parent.minLeadingWidth, minLeadingWidth: minLeadingWidth ?? parent.minLeadingWidth,
titleAlignment: titleAlignment ?? parent.titleAlignment,
), ),
child: child, child: child,
); );
......
...@@ -1998,6 +1998,7 @@ void main() { ...@@ -1998,6 +1998,7 @@ void main() {
horizontalTitleGap: 4.0, horizontalTitleGap: 4.0,
minVerticalPadding: 2.0, minVerticalPadding: 2.0,
minLeadingWidth: 6.0, minLeadingWidth: 6.0,
titleAlignment: ListTileTitleAlignment.bottom,
).debugFillProperties(builder); ).debugFillProperties(builder);
final List<String> description = builder.properties final List<String> description = builder.properties
...@@ -2035,6 +2036,7 @@ void main() { ...@@ -2035,6 +2036,7 @@ void main() {
'horizontalTitleGap: 4.0', 'horizontalTitleGap: 4.0',
'minVerticalPadding: 2.0', 'minVerticalPadding: 2.0',
'minLeadingWidth: 6.0', 'minLeadingWidth: 6.0',
'titleAlignment: ListTileTitleAlignment.bottom',
]), ]),
); );
}); });
...@@ -2222,6 +2224,260 @@ void main() { ...@@ -2222,6 +2224,260 @@ void main() {
expect(tester.takeException(), isNull); 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', () { group('Material 2', () {
// Tests that are only relevant for Material 2. Once ThemeData.useMaterial3 // Tests that are only relevant for Material 2. Once ThemeData.useMaterial3
// is turned on by default, these tests can be removed. // is turned on by default, these tests can be removed.
...@@ -3462,6 +3718,258 @@ void main() { ...@@ -3462,6 +3718,258 @@ void main() {
expect(find.byType(Material), paints..rect(color: defaultColor)); 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() { ...@@ -71,6 +71,7 @@ void main() {
expect(themeData.enableFeedback, null); expect(themeData.enableFeedback, null);
expect(themeData.mouseCursor, null); expect(themeData.mouseCursor, null);
expect(themeData.visualDensity, null); expect(themeData.visualDensity, null);
expect(themeData.titleAlignment, null);
}); });
testWidgets('Default ListTileThemeData debugFillProperties', (WidgetTester tester) async { testWidgets('Default ListTileThemeData debugFillProperties', (WidgetTester tester) async {
...@@ -106,6 +107,7 @@ void main() { ...@@ -106,6 +107,7 @@ void main() {
enableFeedback: true, enableFeedback: true,
mouseCursor: MaterialStateMouseCursor.clickable, mouseCursor: MaterialStateMouseCursor.clickable,
visualDensity: VisualDensity.comfortable, visualDensity: VisualDensity.comfortable,
titleAlignment: ListTileTitleAlignment.top,
).debugFillProperties(builder); ).debugFillProperties(builder);
final List<String> description = builder.properties final List<String> description = builder.properties
...@@ -134,6 +136,7 @@ void main() { ...@@ -134,6 +136,7 @@ void main() {
'enableFeedback: true', 'enableFeedback: true',
'mouseCursor: MaterialStateMouseCursor(clickable)', 'mouseCursor: MaterialStateMouseCursor(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',
]), ]),
); );
}); });
...@@ -215,6 +218,7 @@ void main() { ...@@ -215,6 +218,7 @@ void main() {
selectedColor: selectedColor, selectedColor: selectedColor,
iconColor: iconColor, iconColor: iconColor,
textColor: textColor, textColor: textColor,
minVerticalPadding: 25.0,
mouseCursor: MaterialStateProperty.resolveWith((Set<MaterialState> states) { mouseCursor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) { if (states.contains(MaterialState.disabled)) {
return SystemMouseCursors.forbidden; return SystemMouseCursors.forbidden;
...@@ -223,6 +227,7 @@ void main() { ...@@ -223,6 +227,7 @@ void main() {
return SystemMouseCursors.click; return SystemMouseCursors.click;
}), }),
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
titleAlignment: ListTileTitleAlignment.bottom,
), ),
child: Builder( child: Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
...@@ -311,7 +316,14 @@ void main() { ...@@ -311,7 +316,14 @@ void main() {
// VisualDensity is respected // VisualDensity is respected
final RenderBox box = tester.renderObject(find.byKey(listTileKey)); 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 { testWidgets('ListTileTheme colors are applied to leading and trailing text widgets', (WidgetTester tester) async {
...@@ -721,6 +733,7 @@ void main() { ...@@ -721,6 +733,7 @@ void main() {
minVerticalPadding: 300, minVerticalPadding: 300,
minLeadingWidth: 400, minLeadingWidth: 400,
enableFeedback: true, enableFeedback: true,
titleAlignment: ListTileTitleAlignment.bottom,
); );
final ListTileThemeData copy = original.copyWith( final ListTileThemeData copy = original.copyWith(
...@@ -740,6 +753,7 @@ void main() { ...@@ -740,6 +753,7 @@ void main() {
minVerticalPadding: 700, minVerticalPadding: 700,
minLeadingWidth: 800, minLeadingWidth: 800,
enableFeedback: false, enableFeedback: false,
titleAlignment: ListTileTitleAlignment.top,
); );
expect(copy.dense, false); expect(copy.dense, false);
...@@ -758,6 +772,43 @@ void main() { ...@@ -758,6 +772,43 @@ void main() {
expect(copy.minVerticalPadding, 700); expect(copy.minVerticalPadding, 700);
expect(copy.minLeadingWidth, 800); expect(copy.minLeadingWidth, 800);
expect(copy.enableFeedback, false); 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