diff --git a/packages/flutter/lib/src/material/checkbox_list_tile.dart b/packages/flutter/lib/src/material/checkbox_list_tile.dart index 3d3ca805b9f..372c468f56a 100644 --- a/packages/flutter/lib/src/material/checkbox_list_tile.dart +++ b/packages/flutter/lib/src/material/checkbox_list_tile.dart @@ -206,6 +206,7 @@ class CheckboxListTile extends StatelessWidget { this.enableFeedback, this.checkboxSemanticLabel, this.checkboxScaleFactor = 1.0, + this.titleAlignment, this.internalAddSemanticForOnTap = false, }) : _checkboxType = _CheckboxType.material, assert(tristate || value != null), @@ -252,6 +253,7 @@ class CheckboxListTile extends StatelessWidget { this.enableFeedback, this.checkboxSemanticLabel, this.checkboxScaleFactor = 1.0, + this.titleAlignment, this.internalAddSemanticForOnTap = false, }) : _checkboxType = _CheckboxType.adaptive, assert(tristate || value != null), @@ -468,6 +470,20 @@ class CheckboxListTile extends StatelessWidget { /// inoperative. final bool? enabled; + /// 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; + /// Whether to add button:true to the semantics if onTap is provided. /// This is a temporary flag to help changing the behavior of ListTile onTap semantics. /// @@ -583,6 +599,7 @@ class CheckboxListTile extends StatelessWidget { focusNode: focusNode, onFocusChange: onFocusChange, enableFeedback: enableFeedback, + titleAlignment: titleAlignment, internalAddSemanticForOnTap: internalAddSemanticForOnTap, ), ); diff --git a/packages/flutter/lib/src/material/radio_list_tile.dart b/packages/flutter/lib/src/material/radio_list_tile.dart index 1e55eee7afe..7da8f90fe98 100644 --- a/packages/flutter/lib/src/material/radio_list_tile.dart +++ b/packages/flutter/lib/src/material/radio_list_tile.dart @@ -201,6 +201,7 @@ class RadioListTile extends StatelessWidget { this.onFocusChange, this.enableFeedback, this.radioScaleFactor = 1.0, + this.titleAlignment, this.internalAddSemanticForOnTap = false, }) : _radioType = _RadioType.material, useCupertinoCheckmarkStyle = false, @@ -243,6 +244,7 @@ class RadioListTile extends StatelessWidget { this.enableFeedback, this.radioScaleFactor = 1.0, this.useCupertinoCheckmarkStyle = false, + this.titleAlignment, this.internalAddSemanticForOnTap = false, }) : _radioType = _RadioType.adaptive, assert(isThreeLine != true || subtitle != null); @@ -454,6 +456,20 @@ class RadioListTile extends StatelessWidget { final _RadioType _radioType; + /// 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; + /// Whether to add button:true to the semantics if onTap is provided. /// This is a temporary flag to help changing the behavior of ListTile onTap semantics. /// @@ -567,6 +583,7 @@ class RadioListTile extends StatelessWidget { focusNode: focusNode, onFocusChange: onFocusChange, enableFeedback: enableFeedback, + titleAlignment: titleAlignment, internalAddSemanticForOnTap: internalAddSemanticForOnTap, ), ); diff --git a/packages/flutter/test/material/checkbox_list_tile_test.dart b/packages/flutter/test/material/checkbox_list_tile_test.dart index 47200d0f087..e767917eea7 100644 --- a/packages/flutter/test/material/checkbox_list_tile_test.dart +++ b/packages/flutter/test/material/checkbox_list_tile_test.dart @@ -1569,6 +1569,278 @@ void main() { ); expectThreeLine(); }); + + testWidgets('titleAlignment position with title widget', (WidgetTester tester) async { + const Key secondaryKey = Key('secondary'); + const double titleHeight = 50.0; + const double secondaryHeight = 24.0; + // The default vertical padding for material 3 is 8.0. + const double minVerticalPadding = 8.0; + + Widget buildFrame({ListTileTitleAlignment? titleAlignment}) { + return MaterialApp( + home: Material( + child: Center( + child: CheckboxListTile( + titleAlignment: titleAlignment, + controlAffinity: ListTileControlAffinity.leading, + value: true, + onChanged: (bool? newValue) {}, + title: const SizedBox(width: 20.0, height: titleHeight), + secondary: const SizedBox(key: secondaryKey, width: 24.0, height: secondaryHeight), + ), + ), + ), + ); + } + + // 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()); + final double checkboxHeight = tester.getSize(find.byType(Checkbox)).height; + final double tileHeight = tester.getSize(find.byType(ListTile)).height; + expect( + find.byWidgetPredicate((Widget widget) { + return widget is ListTile && widget.titleAlignment == null; + }), + findsOne, + ); + Offset tileOffset = tester.getTopLeft(find.byType(ListTile)); + Offset checkboxOffset = tester.getTopLeft(find.byType(Checkbox)); + Offset secondaryOffset = tester.getTopRight(find.byKey(secondaryKey)); + + // Leading and trailing widgets are centered vertically in the tile. + final double centerPositionCheckbox = (tileHeight / 2) - (checkboxHeight / 2); + final double centerPositionSecondary = (tileHeight / 2) - (secondaryHeight / 2); + expect(checkboxOffset.dy - tileOffset.dy, centerPositionCheckbox); + expect(secondaryOffset.dy - tileOffset.dy, centerPositionSecondary); + + // Test [ListTileTitleAlignment.threeLine] alignment. + await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.threeLine)); + expect( + find.byWidgetPredicate((Widget widget) { + return widget is ListTile && widget.titleAlignment == ListTileTitleAlignment.threeLine; + }), + findsOne, + ); + tileOffset = tester.getTopLeft(find.byType(ListTile)); + checkboxOffset = tester.getTopLeft(find.byType(Checkbox)); + secondaryOffset = tester.getTopRight(find.byKey(secondaryKey)); + + // Leading and trailing widgets are centered vertically in the tile, + // If the [ListTile.isThreeLine] property is false. + expect(checkboxOffset.dy - tileOffset.dy, centerPositionCheckbox); + expect(secondaryOffset.dy - tileOffset.dy, centerPositionSecondary); + + // Test [ListTileTitleAlignment.titleHeight] alignment. + await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.titleHeight)); + expect( + find.byWidgetPredicate((Widget widget) { + return widget is ListTile && widget.titleAlignment == ListTileTitleAlignment.titleHeight; + }), + findsOne, + ); + tileOffset = tester.getTopLeft(find.byType(ListTile)); + checkboxOffset = tester.getTopLeft(find.byType(Checkbox)); + secondaryOffset = tester.getTopRight(find.byKey(secondaryKey)); + + expect(checkboxOffset.dy - tileOffset.dy, (tileHeight - checkboxHeight) / 2); + expect(secondaryOffset.dy - tileOffset.dy, centerPositionSecondary); + + // Test [ListTileTitleAlignment.top] alignment. + await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.top)); + expect( + find.byWidgetPredicate((Widget widget) { + return widget is ListTile && widget.titleAlignment == ListTileTitleAlignment.top; + }), + findsOne, + ); + tileOffset = tester.getTopLeft(find.byType(ListTile)); + checkboxOffset = tester.getTopLeft(find.byType(Checkbox)); + secondaryOffset = tester.getTopRight(find.byKey(secondaryKey)); + + // Leading and trailing widgets are placed minVerticalPadding below + // the top of the title widget. The default for material 3 is 8.0. + const double topPosition = minVerticalPadding; + expect(checkboxOffset.dy - tileOffset.dy, topPosition); + expect(secondaryOffset.dy - tileOffset.dy, topPosition); + + // Test [ListTileTitleAlignment.center] alignment. + await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.center)); + expect( + find.byWidgetPredicate((Widget widget) { + return widget is ListTile && widget.titleAlignment == ListTileTitleAlignment.center; + }), + findsOne, + ); + tileOffset = tester.getTopLeft(find.byType(ListTile)); + checkboxOffset = tester.getTopLeft(find.byType(Checkbox)); + secondaryOffset = tester.getTopRight(find.byKey(secondaryKey)); + + // Leading and trailing widgets are centered vertically in the tile. + expect(checkboxOffset.dy - tileOffset.dy, centerPositionCheckbox); + expect(secondaryOffset.dy - tileOffset.dy, centerPositionSecondary); + + // Test [ListTileTitleAlignment.bottom] alignment. + await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.bottom)); + expect( + find.byWidgetPredicate((Widget widget) { + return widget is ListTile && widget.titleAlignment == ListTileTitleAlignment.bottom; + }), + findsOne, + ); + tileOffset = tester.getTopLeft(find.byType(ListTile)); + checkboxOffset = tester.getTopLeft(find.byType(Checkbox)); + secondaryOffset = tester.getTopRight(find.byKey(secondaryKey)); + + // Leading and trailing widgets are placed minVerticalPadding above + // the bottom of the subtitle widget. + final double bottomPositionCheckbox = tileHeight - minVerticalPadding - checkboxHeight; + final double bottomPositionSecondary = tileHeight - minVerticalPadding - secondaryHeight; + expect(checkboxOffset.dy - tileOffset.dy, bottomPositionCheckbox); + expect(secondaryOffset.dy - tileOffset.dy, bottomPositionSecondary); + }); + + testWidgets('titleAlignment position with title and subtitle widgets', ( + WidgetTester tester, + ) async { + const Key secondaryKey = Key('secondary'); + const double titleHeight = 50.0; + const double subtitleHeight = 50.0; + const double secondaryHeight = 24.0; + const double verticalPadding = 8.0; + + Widget buildFrame({ListTileTitleAlignment? titleAlignment}) { + return MaterialApp( + home: Material( + child: Center( + child: CheckboxListTile( + titleAlignment: titleAlignment, + controlAffinity: ListTileControlAffinity.leading, + title: const SizedBox(width: 20.0, height: titleHeight), + subtitle: const SizedBox(width: 20.0, height: subtitleHeight), + secondary: const SizedBox(key: secondaryKey, width: 24.0, height: secondaryHeight), + value: true, + onChanged: (bool? newValue) {}, + ), + ), + ), + ); + } + + // 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()); + final double tileHeight = tester.getSize(find.byType(ListTile)).height; + final double checkboxHeight = tester.getSize(find.byType(Checkbox)).height; + expect( + find.byWidgetPredicate((Widget widget) { + return widget is ListTile && widget.titleAlignment == null; + }), + findsOne, + ); + Offset tileOffset = tester.getTopLeft(find.byType(ListTile)); + Offset checkboxOffset = tester.getTopLeft(find.byType(Checkbox)); + Offset secondaryOffset = tester.getTopRight(find.byKey(secondaryKey)); + + // Leading and trailing widgets are centered vertically in the tile. + final double centerPositionOffsetCheckbox = (tileHeight / 2) - (checkboxHeight / 2); + final double centerPositionOffsetSecondary = (tileHeight / 2) - (secondaryHeight / 2); + expect(checkboxOffset.dy - tileOffset.dy, centerPositionOffsetCheckbox); + expect(secondaryOffset.dy - tileOffset.dy, centerPositionOffsetSecondary); + + // Test [ListTileTitleAlignment.threeLine] alignment. + await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.threeLine)); + expect( + find.byWidgetPredicate((Widget widget) { + return widget is ListTile && widget.titleAlignment == ListTileTitleAlignment.threeLine; + }), + findsOne, + ); + tileOffset = tester.getTopLeft(find.byType(ListTile)); + checkboxOffset = tester.getTopLeft(find.byType(Checkbox)); + secondaryOffset = tester.getTopRight(find.byKey(secondaryKey)); + + // Leading and trailing widgets are centered vertically in the tile, + // If the [ListTile.isThreeLine] property is false. + expect(checkboxOffset.dy - tileOffset.dy, centerPositionOffsetCheckbox); + expect(secondaryOffset.dy - tileOffset.dy, centerPositionOffsetSecondary); + + // Test [ListTileTitleAlignment.titleHeight] alignment. + await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.titleHeight)); + expect( + find.byWidgetPredicate((Widget widget) { + return widget is ListTile && widget.titleAlignment == ListTileTitleAlignment.titleHeight; + }), + findsOne, + ); + tileOffset = tester.getTopLeft(find.byType(ListTile)); + checkboxOffset = tester.getTopLeft(find.byType(Checkbox)); + secondaryOffset = tester.getTopRight(find.byKey(secondaryKey)); + + // Leading and trailing widgets are positioned 16.0 pixels below the + // top of the title widget. + const double titlePosition = 16.0; + expect(checkboxOffset.dy - tileOffset.dy, titlePosition); + expect(secondaryOffset.dy - tileOffset.dy, titlePosition); + + // Test [ListTileTitleAlignment.top] alignment. + await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.top)); + expect( + find.byWidgetPredicate((Widget widget) { + return widget is ListTile && widget.titleAlignment == ListTileTitleAlignment.top; + }), + findsOne, + ); + tileOffset = tester.getTopLeft(find.byType(ListTile)); + checkboxOffset = tester.getTopLeft(find.byType(Checkbox)); + secondaryOffset = tester.getTopRight(find.byKey(secondaryKey)); + + // Leading and trailing widgets are placed minVerticalPadding below + // the top of the title widget. + const double topPosition = verticalPadding; + expect(checkboxOffset.dy - tileOffset.dy, topPosition); + expect(secondaryOffset.dy - tileOffset.dy, topPosition); + + // Test [ListTileTitleAlignment.center] alignment. + await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.center)); + expect( + find.byWidgetPredicate((Widget widget) { + return widget is ListTile && widget.titleAlignment == ListTileTitleAlignment.center; + }), + findsOne, + ); + tileOffset = tester.getTopLeft(find.byType(ListTile)); + checkboxOffset = tester.getTopLeft(find.byType(Checkbox)); + secondaryOffset = tester.getTopRight(find.byKey(secondaryKey)); + + // Leading and trailing widgets are centered vertically in the tile. + expect(checkboxOffset.dy - tileOffset.dy, centerPositionOffsetCheckbox); + expect(secondaryOffset.dy - tileOffset.dy, centerPositionOffsetSecondary); + + // Test [ListTileTitleAlignment.bottom] alignment. + await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.bottom)); + expect( + find.byWidgetPredicate((Widget widget) { + return widget is ListTile && widget.titleAlignment == ListTileTitleAlignment.bottom; + }), + findsOne, + ); + tileOffset = tester.getTopLeft(find.byType(ListTile)); + checkboxOffset = tester.getTopLeft(find.byType(Checkbox)); + secondaryOffset = tester.getTopRight(find.byKey(secondaryKey)); + + // Leading and trailing widgets are placed minVerticalPadding above + // the bottom of the subtitle widget. + final double bottomPositionCheckbox = tileHeight - verticalPadding - checkboxHeight; + final double bottomPositionSecondary = tileHeight - verticalPadding - secondaryHeight; + expect(checkboxOffset.dy - tileOffset.dy, bottomPositionCheckbox); + expect(secondaryOffset.dy - tileOffset.dy, bottomPositionSecondary); + }); } class _SelectedGrabMouseCursor extends MaterialStateMouseCursor { diff --git a/packages/flutter/test/material/radio_list_tile_test.dart b/packages/flutter/test/material/radio_list_tile_test.dart index 54ec1aca070..dd75cc7f0ed 100644 --- a/packages/flutter/test/material/radio_list_tile_test.dart +++ b/packages/flutter/test/material/radio_list_tile_test.dart @@ -1880,4 +1880,278 @@ void main() { ); expectThreeLine(); }); + + testWidgets('titleAlignment position with title widget', (WidgetTester tester) async { + const Key secondaryKey = Key('secondary'); + const double titleHeight = 50.0; + const double secondaryHeight = 24.0; + // The default vertical padding for material 3 is 8.0. + const double minVerticalPadding = 8.0; + + Widget buildFrame({ListTileTitleAlignment? titleAlignment}) { + return MaterialApp( + home: Material( + child: Center( + child: RadioListTile( + titleAlignment: titleAlignment, + controlAffinity: ListTileControlAffinity.leading, + value: true, + groupValue: true, + onChanged: (bool? newValue) {}, + title: const SizedBox(width: 20.0, height: titleHeight), + secondary: const SizedBox(key: secondaryKey, width: 24.0, height: secondaryHeight), + ), + ), + ), + ); + } + + // 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()); + final double radioHeight = tester.getSize(find.byType(Radio)).height; + final double tileHeight = tester.getSize(find.byType(ListTile)).height; + expect( + find.byWidgetPredicate((Widget widget) { + return widget is ListTile && widget.titleAlignment == null; + }), + findsOne, + ); + Offset tileOffset = tester.getTopLeft(find.byType(ListTile)); + Offset radioOffset = tester.getTopLeft(find.byType(Radio)); + Offset secondaryOffset = tester.getTopRight(find.byKey(secondaryKey)); + + // Leading and trailing widgets are centered vertically in the tile. + final double centerPositionRadio = (tileHeight / 2) - (radioHeight / 2); + final double centerPositionSecondary = (tileHeight / 2) - (secondaryHeight / 2); + expect(radioOffset.dy - tileOffset.dy, centerPositionRadio); + expect(secondaryOffset.dy - tileOffset.dy, centerPositionSecondary); + + // Test [ListTileTitleAlignment.threeLine] alignment. + await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.threeLine)); + expect( + find.byWidgetPredicate((Widget widget) { + return widget is ListTile && widget.titleAlignment == ListTileTitleAlignment.threeLine; + }), + findsOne, + ); + tileOffset = tester.getTopLeft(find.byType(ListTile)); + radioOffset = tester.getTopLeft(find.byType(Radio)); + secondaryOffset = tester.getTopRight(find.byKey(secondaryKey)); + + // Leading and trailing widgets are centered vertically in the tile, + // If the [ListTile.isThreeLine] property is false. + expect(radioOffset.dy - tileOffset.dy, centerPositionRadio); + expect(secondaryOffset.dy - tileOffset.dy, centerPositionSecondary); + + // Test [ListTileTitleAlignment.titleHeight] alignment. + await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.titleHeight)); + expect( + find.byWidgetPredicate((Widget widget) { + return widget is ListTile && widget.titleAlignment == ListTileTitleAlignment.titleHeight; + }), + findsOne, + ); + tileOffset = tester.getTopLeft(find.byType(ListTile)); + radioOffset = tester.getTopLeft(find.byType(Radio)); + secondaryOffset = tester.getTopRight(find.byKey(secondaryKey)); + + expect(radioOffset.dy - tileOffset.dy, (tileHeight - radioHeight) / 2); + expect(secondaryOffset.dy - tileOffset.dy, centerPositionSecondary); + + // Test [ListTileTitleAlignment.top] alignment. + await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.top)); + expect( + find.byWidgetPredicate((Widget widget) { + return widget is ListTile && widget.titleAlignment == ListTileTitleAlignment.top; + }), + findsOne, + ); + tileOffset = tester.getTopLeft(find.byType(ListTile)); + radioOffset = tester.getTopLeft(find.byType(Radio)); + secondaryOffset = tester.getTopRight(find.byKey(secondaryKey)); + + // Leading and trailing widgets are placed minVerticalPadding below + // the top of the title widget. The default for material 3 is 8.0. + const double topPosition = minVerticalPadding; + expect(radioOffset.dy - tileOffset.dy, topPosition); + expect(secondaryOffset.dy - tileOffset.dy, topPosition); + + // Test [ListTileTitleAlignment.center] alignment. + await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.center)); + expect( + find.byWidgetPredicate((Widget widget) { + return widget is ListTile && widget.titleAlignment == ListTileTitleAlignment.center; + }), + findsOne, + ); + tileOffset = tester.getTopLeft(find.byType(ListTile)); + radioOffset = tester.getTopLeft(find.byType(Radio)); + secondaryOffset = tester.getTopRight(find.byKey(secondaryKey)); + + // Leading and trailing widgets are centered vertically in the tile. + expect(radioOffset.dy - tileOffset.dy, centerPositionRadio); + expect(secondaryOffset.dy - tileOffset.dy, centerPositionSecondary); + + // Test [ListTileTitleAlignment.bottom] alignment. + await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.bottom)); + expect( + find.byWidgetPredicate((Widget widget) { + return widget is ListTile && widget.titleAlignment == ListTileTitleAlignment.bottom; + }), + findsOne, + ); + tileOffset = tester.getTopLeft(find.byType(ListTile)); + radioOffset = tester.getTopLeft(find.byType(Radio)); + secondaryOffset = tester.getTopRight(find.byKey(secondaryKey)); + + // Leading and trailing widgets are placed minVerticalPadding above + // the bottom of the subtitle widget. + final double bottomPositionRadio = tileHeight - minVerticalPadding - radioHeight; + final double bottomPositionSecondary = tileHeight - minVerticalPadding - secondaryHeight; + expect(radioOffset.dy - tileOffset.dy, bottomPositionRadio); + expect(secondaryOffset.dy - tileOffset.dy, bottomPositionSecondary); + }); + + testWidgets('titleAlignment position with title and subtitle widgets', ( + WidgetTester tester, + ) async { + const Key secondaryKey = Key('secondary'); + const double titleHeight = 50.0; + const double subtitleHeight = 50.0; + const double secondaryHeight = 24.0; + const double verticalPadding = 8.0; + + Widget buildFrame({ListTileTitleAlignment? titleAlignment}) { + return MaterialApp( + home: Material( + child: Center( + child: RadioListTile( + titleAlignment: titleAlignment, + controlAffinity: ListTileControlAffinity.leading, + title: const SizedBox(width: 20.0, height: titleHeight), + subtitle: const SizedBox(width: 20.0, height: subtitleHeight), + secondary: const SizedBox(key: secondaryKey, width: 24.0, height: secondaryHeight), + value: true, + groupValue: true, + onChanged: (bool? newValue) {}, + ), + ), + ), + ); + } + + // 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()); + final double tileHeight = tester.getSize(find.byType(ListTile)).height; + final double radioHeight = tester.getSize(find.byType(Radio)).height; + expect( + find.byWidgetPredicate((Widget widget) { + return widget is ListTile && widget.titleAlignment == null; + }), + findsOne, + ); + Offset tileOffset = tester.getTopLeft(find.byType(ListTile)); + Offset radioOffset = tester.getTopLeft(find.byType(Radio)); + Offset secondaryOffset = tester.getTopRight(find.byKey(secondaryKey)); + + // Leading and trailing widgets are centered vertically in the tile. + final double centerPositionOffsetRadio = (tileHeight / 2) - (radioHeight / 2); + final double centerPositionOffsetSecondary = (tileHeight / 2) - (secondaryHeight / 2); + expect(radioOffset.dy - tileOffset.dy, centerPositionOffsetRadio); + expect(secondaryOffset.dy - tileOffset.dy, centerPositionOffsetSecondary); + + // Test [ListTileTitleAlignment.threeLine] alignment. + await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.threeLine)); + expect( + find.byWidgetPredicate((Widget widget) { + return widget is ListTile && widget.titleAlignment == ListTileTitleAlignment.threeLine; + }), + findsOne, + ); + tileOffset = tester.getTopLeft(find.byType(ListTile)); + radioOffset = tester.getTopLeft(find.byType(Radio)); + secondaryOffset = tester.getTopRight(find.byKey(secondaryKey)); + + // Leading and trailing widgets are centered vertically in the tile, + // If the [ListTile.isThreeLine] property is false. + expect(radioOffset.dy - tileOffset.dy, centerPositionOffsetRadio); + expect(secondaryOffset.dy - tileOffset.dy, centerPositionOffsetSecondary); + + // Test [ListTileTitleAlignment.titleHeight] alignment. + await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.titleHeight)); + expect( + find.byWidgetPredicate((Widget widget) { + return widget is ListTile && widget.titleAlignment == ListTileTitleAlignment.titleHeight; + }), + findsOne, + ); + tileOffset = tester.getTopLeft(find.byType(ListTile)); + radioOffset = tester.getTopLeft(find.byType(Radio)); + secondaryOffset = tester.getTopRight(find.byKey(secondaryKey)); + + // Leading and trailing widgets are positioned 16.0 pixels below the + // top of the title widget. + const double titlePosition = 16.0; + expect(radioOffset.dy - tileOffset.dy, titlePosition); + expect(secondaryOffset.dy - tileOffset.dy, titlePosition); + + // Test [ListTileTitleAlignment.top] alignment. + await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.top)); + expect( + find.byWidgetPredicate((Widget widget) { + return widget is ListTile && widget.titleAlignment == ListTileTitleAlignment.top; + }), + findsOne, + ); + tileOffset = tester.getTopLeft(find.byType(ListTile)); + radioOffset = tester.getTopLeft(find.byType(Radio)); + secondaryOffset = tester.getTopRight(find.byKey(secondaryKey)); + + // Leading and trailing widgets are placed minVerticalPadding below + // the top of the title widget. + const double topPosition = verticalPadding; + expect(radioOffset.dy - tileOffset.dy, topPosition); + expect(secondaryOffset.dy - tileOffset.dy, topPosition); + + // Test [ListTileTitleAlignment.center] alignment. + await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.center)); + expect( + find.byWidgetPredicate((Widget widget) { + return widget is ListTile && widget.titleAlignment == ListTileTitleAlignment.center; + }), + findsOne, + ); + tileOffset = tester.getTopLeft(find.byType(ListTile)); + radioOffset = tester.getTopLeft(find.byType(Radio)); + secondaryOffset = tester.getTopRight(find.byKey(secondaryKey)); + + // Leading and trailing widgets are centered vertically in the tile. + expect(radioOffset.dy - tileOffset.dy, centerPositionOffsetRadio); + expect(secondaryOffset.dy - tileOffset.dy, centerPositionOffsetSecondary); + + // Test [ListTileTitleAlignment.bottom] alignment. + await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.bottom)); + expect( + find.byWidgetPredicate((Widget widget) { + return widget is ListTile && widget.titleAlignment == ListTileTitleAlignment.bottom; + }), + findsOne, + ); + tileOffset = tester.getTopLeft(find.byType(ListTile)); + radioOffset = tester.getTopLeft(find.byType(Radio)); + secondaryOffset = tester.getTopRight(find.byKey(secondaryKey)); + + // Leading and trailing widgets are placed minVerticalPadding above + // the bottom of the subtitle widget. + final double bottomPositionRadio = tileHeight - verticalPadding - radioHeight; + final double bottomPositionSecondary = tileHeight - verticalPadding - secondaryHeight; + expect(radioOffset.dy - tileOffset.dy, bottomPositionRadio); + expect(secondaryOffset.dy - tileOffset.dy, bottomPositionSecondary); + }); }