diff --git a/packages/flutter/lib/src/material/data_table.dart b/packages/flutter/lib/src/material/data_table.dart index 30ee7b565f3..c4ccb013f31 100644 --- a/packages/flutter/lib/src/material/data_table.dart +++ b/packages/flutter/lib/src/material/data_table.dart @@ -42,6 +42,7 @@ class DataColumn { this.numeric = false, this.onSort, this.mouseCursor, + this.headingRowAlignment, }); /// The column heading. @@ -97,6 +98,17 @@ class DataColumn { /// See also: /// * [MaterialStateMouseCursor], which can be used to create a [MouseCursor]. final MaterialStateProperty? mouseCursor; + + /// Defines the horizontal layout of the [label] and sort indicator in the + /// heading row. + /// + /// If [headingRowAlignment] value is [MainAxisAlignment.center] and [onSort] is + /// not null, then a [SizedBox] with a width of sort arrow icon size and sort + /// arrow padding will be placed before the [label] to ensure the label is + /// centered in the column. + /// + /// If null, then defaults to [MainAxisAlignment.start]. + final MainAxisAlignment? headingRowAlignment; } /// Row configuration and cell data for a [DataTable]. @@ -830,12 +842,16 @@ class DataTable extends StatelessWidget { required bool ascending, required MaterialStateProperty? overlayColor, required MouseCursor? mouseCursor, + required MainAxisAlignment headingRowAlignment, }) { final ThemeData themeData = Theme.of(context); final DataTableThemeData dataTableTheme = DataTableTheme.of(context); label = Row( textDirection: numeric ? TextDirection.rtl : null, + mainAxisAlignment: headingRowAlignment, children: [ + if (headingRowAlignment == MainAxisAlignment.center && onSort != null) + const SizedBox(width: _SortArrowState._arrowIconSize + _sortArrowPadding), label, if (onSort != null) ...[ @@ -1117,6 +1133,7 @@ class DataTable extends StatelessWidget { ascending: sortAscending, overlayColor: effectiveHeadingRowColor, mouseCursor: column.mouseCursor?.resolve(headerStates) ?? dataTableTheme.headingCellCursor?.resolve(headerStates), + headingRowAlignment: column.headingRowAlignment ?? dataTableTheme.headingRowAlignment ?? MainAxisAlignment.start, ); rowIndex = 1; for (final DataRow row in rows) { diff --git a/packages/flutter/lib/src/material/data_table_theme.dart b/packages/flutter/lib/src/material/data_table_theme.dart index fb2bb96b005..b2b94f778fd 100644 --- a/packages/flutter/lib/src/material/data_table_theme.dart +++ b/packages/flutter/lib/src/material/data_table_theme.dart @@ -57,6 +57,7 @@ class DataTableThemeData with Diagnosticable { this.checkboxHorizontalMargin, this.headingCellCursor, this.dataRowCursor, + this.headingRowAlignment, }) : assert(dataRowMinHeight == null || dataRowMaxHeight == null || dataRowMaxHeight >= dataRowMinHeight), assert(dataRowHeight == null || (dataRowMinHeight == null && dataRowMaxHeight == null), 'dataRowHeight ($dataRowHeight) must not be set if dataRowMinHeight ($dataRowMinHeight) or dataRowMaxHeight ($dataRowMaxHeight) are set.'), @@ -114,6 +115,9 @@ class DataTableThemeData with Diagnosticable { /// If specified, overrides the default value of [DataRow.mouseCursor]. final MaterialStateProperty? dataRowCursor; + /// If specified, overrides the default value of [DataColumn.headingRowAlignment]. + final MainAxisAlignment? headingRowAlignment; + /// Creates a copy of this object but with the given fields replaced with the /// new values. DataTableThemeData copyWith({ @@ -136,6 +140,7 @@ class DataTableThemeData with Diagnosticable { double? checkboxHorizontalMargin, MaterialStateProperty? headingCellCursor, MaterialStateProperty? dataRowCursor, + MainAxisAlignment? headingRowAlignment, }) { assert(dataRowHeight == null || (dataRowMinHeight == null && dataRowMaxHeight == null), 'dataRowHeight ($dataRowHeight) must not be set if dataRowMinHeight ($dataRowMinHeight) or dataRowMaxHeight ($dataRowMaxHeight) are set.'); @@ -157,6 +162,7 @@ class DataTableThemeData with Diagnosticable { checkboxHorizontalMargin: checkboxHorizontalMargin ?? this.checkboxHorizontalMargin, headingCellCursor: headingCellCursor ?? this.headingCellCursor, dataRowCursor: dataRowCursor ?? this.dataRowCursor, + headingRowAlignment: headingRowAlignment ?? this.headingRowAlignment, ); } @@ -182,6 +188,7 @@ class DataTableThemeData with Diagnosticable { checkboxHorizontalMargin: lerpDouble(a.checkboxHorizontalMargin, b.checkboxHorizontalMargin, t), headingCellCursor: t < 0.5 ? a.headingCellCursor : b.headingCellCursor, dataRowCursor: t < 0.5 ? a.dataRowCursor : b.dataRowCursor, + headingRowAlignment: t < 0.5 ? a.headingRowAlignment : b.headingRowAlignment, ); } @@ -201,6 +208,7 @@ class DataTableThemeData with Diagnosticable { checkboxHorizontalMargin, headingCellCursor, dataRowCursor, + headingRowAlignment, ); @override @@ -225,7 +233,8 @@ class DataTableThemeData with Diagnosticable { && other.dividerThickness == dividerThickness && other.checkboxHorizontalMargin == checkboxHorizontalMargin && other.headingCellCursor == headingCellCursor - && other.dataRowCursor == dataRowCursor; + && other.dataRowCursor == dataRowCursor + && other.headingRowAlignment == headingRowAlignment; } @override @@ -245,6 +254,7 @@ class DataTableThemeData with Diagnosticable { properties.add(DoubleProperty('checkboxHorizontalMargin', checkboxHorizontalMargin, defaultValue: null)); properties.add(DiagnosticsProperty?>('headingCellCursor', headingCellCursor, defaultValue: null)); properties.add(DiagnosticsProperty?>('dataRowCursor', dataRowCursor, defaultValue: null)); + properties.add(EnumProperty('headingRowAlignment', headingRowAlignment, defaultValue: null)); } } diff --git a/packages/flutter/test/material/data_table_test.dart b/packages/flutter/test/material/data_table_test.dart index 50afdbf4302..ab1683c7951 100644 --- a/packages/flutter/test/material/data_table_test.dart +++ b/packages/flutter/test/material/data_table_test.dart @@ -2363,6 +2363,60 @@ void main() { final TextStyle? dataTextStyle = _getTextRenderObject(tester, 'Data 1').text.style; expect(dataTextStyle, defaultTextStyle.style); }); + + // This is a regression test for https://github.com/flutter/flutter/issues/143340. + testWidgets('DataColumn label can be centered', (WidgetTester tester) async { + const double horizontalMargin = 24.0; + + Widget buildTable({ MainAxisAlignment? headingRowAlignment, bool sortEnabled = false }) { + return MaterialApp( + home: Material( + child: DataTable( + columns: [ + DataColumn( + headingRowAlignment: headingRowAlignment, + onSort: sortEnabled + ? (int columnIndex, bool ascending) { } + : null, + label: const Text('Header'), + ), + ], + rows: const [ + DataRow( + cells: [ + DataCell(Text('Data')), + ], + ), + ], + ), + ), + ); + } + + // Test mainAxisAlignment without sort arrow. + await tester.pumpWidget(buildTable()); + + Offset headerTopLeft = tester.getTopLeft(find.text('Header')); + expect(headerTopLeft.dx, equals(horizontalMargin)); + + // Test mainAxisAlignment.center without sort arrow. + await tester.pumpWidget(buildTable(headingRowAlignment: MainAxisAlignment.center)); + + Offset headerCenter = tester.getCenter(find.text('Header')); + expect(headerCenter.dx, equals(400)); + + // Test mainAxisAlignment with sort arrow. + await tester.pumpWidget(buildTable(sortEnabled: true)); + + headerTopLeft = tester.getTopLeft(find.text('Header')); + expect(headerTopLeft.dx, equals(horizontalMargin)); + + // Test mainAxisAlignment.center with sort arrow. + await tester.pumpWidget(buildTable(headingRowAlignment: MainAxisAlignment.center, sortEnabled: true)); + + headerCenter = tester.getCenter(find.text('Header')); + expect(headerCenter.dx, equals(400)); + }); } RenderParagraph _getTextRenderObject(WidgetTester tester, String text) { diff --git a/packages/flutter/test/material/data_table_theme_test.dart b/packages/flutter/test/material/data_table_theme_test.dart index d688aeadb7f..96c894d8323 100644 --- a/packages/flutter/test/material/data_table_theme_test.dart +++ b/packages/flutter/test/material/data_table_theme_test.dart @@ -45,6 +45,7 @@ void main() { expect(themeData.checkboxHorizontalMargin, null); expect(themeData.headingCellCursor, null); expect(themeData.dataRowCursor, null); + expect(themeData.headingRowAlignment, null); const DataTableTheme theme = DataTableTheme(data: DataTableThemeData(), child: SizedBox()); expect(theme.data.decoration, null); @@ -62,6 +63,7 @@ void main() { expect(theme.data.checkboxHorizontalMargin, null); expect(theme.data.headingCellCursor, null); expect(theme.data.dataRowCursor, null); + expect(theme.data.headingRowAlignment, null); }); testWidgets('Default DataTableThemeData debugFillProperties', (WidgetTester tester) async { @@ -97,6 +99,7 @@ void main() { checkboxHorizontalMargin: 6.0, headingCellCursor: const MaterialStatePropertyAll(SystemMouseCursors.grab), dataRowCursor: const MaterialStatePropertyAll(SystemMouseCursors.forbidden), + headingRowAlignment: MainAxisAlignment.center, ).debugFillProperties(builder); final List description = builder.properties @@ -118,6 +121,7 @@ void main() { expect(description[11], 'checkboxHorizontalMargin: 6.0'); expect(description[12], 'headingCellCursor: WidgetStatePropertyAll(SystemMouseCursor(grab))'); expect(description[13], 'dataRowCursor: WidgetStatePropertyAll(SystemMouseCursor(forbidden))'); + expect(description[14], 'headingRowAlignment: center'); }); testWidgets('DataTable is themeable', (WidgetTester tester) async { @@ -546,6 +550,67 @@ void main() { expect(tester.getSize(_findFirstContainerFor('Data')).height, localThemeDataRowHeight); }); + + // This is a regression test for https://github.com/flutter/flutter/issues/143340. + testWidgets('DataColumn label can be centered with DataTableTheme.headingRowAlignment', (WidgetTester tester) async { + const double horizontalMargin = 24.0; + + Widget buildTable({ MainAxisAlignment? headingRowAlignment, bool sortEnabled = false }) { + return MaterialApp( + theme: ThemeData( + dataTableTheme: DataTableThemeData( + headingRowAlignment: headingRowAlignment, + ), + ), + home: Material( + child: DataTable( + columns: [ + DataColumn( + onSort: sortEnabled + ? (int columnIndex, bool ascending) { } + : null, + label: const Text('Header'), + ), + ], + rows: const [ + DataRow( + cells: [ + DataCell(Text('Data')), + ], + ), + ], + ), + ), + ); + } + + // Test mainAxisAlignment without sort arrow. + await tester.pumpWidget(buildTable()); + + Offset headerTopLeft = tester.getTopLeft(find.text('Header')); + expect(headerTopLeft.dx, equals(horizontalMargin)); + + // Test mainAxisAlignment.center without sort arrow. + await tester.pumpWidget(buildTable(headingRowAlignment: MainAxisAlignment.center)); + await tester.pumpAndSettle(); + + Offset headerCenter = tester.getCenter(find.text('Header')); + expect(headerCenter.dx, equals(400)); + + // Test mainAxisAlignment with sort arrow. + await tester.pumpWidget(buildTable(sortEnabled: true)); + await tester.pumpAndSettle(); + + headerTopLeft = tester.getTopLeft(find.text('Header')); + expect(headerTopLeft.dx, equals(horizontalMargin)); + + // Test mainAxisAlignment.center with sort arrow. + await tester.pumpWidget(buildTable(headingRowAlignment: MainAxisAlignment.center, sortEnabled: true)); + await tester.pumpAndSettle(); + + headerCenter = tester.getCenter(find.text('Header')); + expect(headerCenter.dx, equals(400)); + }); } BoxDecoration _tableRowBoxDecoration({required WidgetTester tester, required int index}) {