Unverified Commit db5a62b0 authored by Per Classon's avatar Per Classon Committed by GitHub

Support customizing colors for rows in DataTable (#60764)

parent 70a3dc0c
...@@ -19,6 +19,7 @@ import 'dropdown.dart'; ...@@ -19,6 +19,7 @@ import 'dropdown.dart';
import 'icons.dart'; import 'icons.dart';
import 'ink_well.dart'; import 'ink_well.dart';
import 'material.dart'; import 'material.dart';
import 'material_state.dart';
import 'theme.dart'; import 'theme.dart';
import 'theme_data.dart'; import 'theme_data.dart';
import 'tooltip.dart'; import 'tooltip.dart';
...@@ -96,6 +97,7 @@ class DataRow { ...@@ -96,6 +97,7 @@ class DataRow {
this.key, this.key,
this.selected = false, this.selected = false,
this.onSelectChanged, this.onSelectChanged,
this.color,
@required this.cells, @required this.cells,
}) : assert(cells != null); }) : assert(cells != null);
...@@ -107,6 +109,7 @@ class DataRow { ...@@ -107,6 +109,7 @@ class DataRow {
int index, int index,
this.selected = false, this.selected = false,
this.onSelectChanged, this.onSelectChanged,
this.color,
@required this.cells, @required this.cells,
}) : assert(cells != null), }) : assert(cells != null),
key = ValueKey<int>(index); key = ValueKey<int>(index);
...@@ -150,13 +153,41 @@ class DataRow { ...@@ -150,13 +153,41 @@ class DataRow {
/// table. /// table.
final List<DataCell> cells; final List<DataCell> cells;
/// The color for the row.
///
/// By default, the color is transparent unless selected. Selected rows has
/// a grey translucent color.
///
/// The effective color can depend on the [MaterialState] state, if the
/// row is selected, pressed, hovered, focused, disabled or enabled. The
/// color is painted as an overlay to the row. To make sure that the row's
/// [InkWell] is visible (when pressed, hovered and focused), it is
/// recommended to use a translucent color.
///
/// ```dart
/// DataRow(
/// color: MaterialStateProperty.resolveWith<Color>(Set<MaterialState> states) {
/// if (states.contains(MaterialState.selected))
/// return Theme.of(context).colorScheme.primary.withOpacity(0.08);
/// return null; // Use the default value.
/// },
///)
/// ```
///
/// See also:
///
/// * The Material Design specification for overlay colors and how they
/// match a component's state:
/// <https://material.io/design/interaction/states.html#anatomy>.
final MaterialStateProperty<Color> color;
bool get _debugInteractive => onSelectChanged != null || cells.any((DataCell cell) => cell._debugInteractive); bool get _debugInteractive => onSelectChanged != null || cells.any((DataCell cell) => cell._debugInteractive);
} }
/// The data for a cell of a [DataTable]. /// The data for a cell of a [DataTable].
/// ///
/// One list of [DataCell] objects must be provided for each [DataRow] /// One list of [DataCell] objects must be provided for each [DataRow]
/// in the [DataTable], in the [new DataRow] constructor's `cells` /// in the [DataTable], in the new [DataRow] constructor's `cells`
/// argument. /// argument.
@immutable @immutable
class DataCell { class DataCell {
...@@ -292,6 +323,53 @@ class DataCell { ...@@ -292,6 +323,53 @@ class DataCell {
/// ///
/// {@end-tool} /// {@end-tool}
/// ///
///
/// {@tool dartpad --template=stateful_widget_scaffold}
///
/// This sample shows how to display a [DataTable] with alternate colors per
/// row, and a custom color for when the row is selected.
///
/// ```dart
/// static const int numItems = 10;
/// List<bool> selected = List<bool>.generate(numItems, (index) => false);
///
/// @override
/// Widget build(BuildContext context) {
/// return SizedBox(
/// width: double.infinity,
/// child: DataTable(
/// columns: const <DataColumn>[
/// DataColumn(
/// label: const Text('Number'),
/// ),
/// ],
/// rows: List<DataRow>.generate(
/// numItems,
/// (index) => DataRow(
/// color: MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) {
/// // All rows will have the same selected color.
/// if (states.contains(MaterialState.selected))
/// return Theme.of(context).colorScheme.primary.withOpacity(0.08);
/// // Even rows will have a grey color.
/// if (index % 2 == 0)
/// return Colors.grey.withOpacity(0.3);
/// return null; // Use default value for other states and odd rows.
/// }),
/// cells: [DataCell(Text('Row $index'))],
/// selected: selected[index],
/// onSelectChanged: (bool value) {
/// setState(() {
/// selected[index] = value;
/// });
/// },
/// ),
/// ),
/// ),
/// );
/// }
/// ```
/// {@end-tool}
///
/// See also: /// See also:
/// ///
/// * [DataColumn], which describes a column in the data table. /// * [DataColumn], which describes a column in the data table.
...@@ -481,10 +559,11 @@ class DataTable extends StatelessWidget { ...@@ -481,10 +559,11 @@ class DataTable extends StatelessWidget {
final double dividerThickness; final double dividerThickness;
Widget _buildCheckbox({ Widget _buildCheckbox({
Color color, Color activeColor,
bool checked, bool checked,
VoidCallback onRowTap, VoidCallback onRowTap,
ValueChanged<bool> onCheckboxChanged, ValueChanged<bool> onCheckboxChanged,
MaterialStateProperty<Color> overlayColor,
}) { }) {
Widget contents = Semantics( Widget contents = Semantics(
container: true, container: true,
...@@ -492,7 +571,7 @@ class DataTable extends StatelessWidget { ...@@ -492,7 +571,7 @@ class DataTable extends StatelessWidget {
padding: EdgeInsetsDirectional.only(start: horizontalMargin, end: horizontalMargin / 2.0), padding: EdgeInsetsDirectional.only(start: horizontalMargin, end: horizontalMargin / 2.0),
child: Center( child: Center(
child: Checkbox( child: Checkbox(
activeColor: color, activeColor: activeColor,
value: checked, value: checked,
onChanged: onCheckboxChanged, onChanged: onCheckboxChanged,
), ),
...@@ -503,6 +582,7 @@ class DataTable extends StatelessWidget { ...@@ -503,6 +582,7 @@ class DataTable extends StatelessWidget {
contents = TableRowInkWell( contents = TableRowInkWell(
onTap: onRowTap, onTap: onRowTap,
child: contents, child: contents,
overlayColor: overlayColor,
); );
} }
return TableCell( return TableCell(
...@@ -582,6 +662,7 @@ class DataTable extends StatelessWidget { ...@@ -582,6 +662,7 @@ class DataTable extends StatelessWidget {
bool showEditIcon, bool showEditIcon,
VoidCallback onTap, VoidCallback onTap,
VoidCallback onSelectChanged, VoidCallback onSelectChanged,
MaterialStateProperty<Color> overlayColor,
}) { }) {
final bool isLightTheme = Theme.of(context).brightness == Brightness.light; final bool isLightTheme = Theme.of(context).brightness == Brightness.light;
if (showEditIcon) { if (showEditIcon) {
...@@ -617,11 +698,13 @@ class DataTable extends StatelessWidget { ...@@ -617,11 +698,13 @@ class DataTable extends StatelessWidget {
label = InkWell( label = InkWell(
onTap: onTap, onTap: onTap,
child: label, child: label,
overlayColor: overlayColor,
); );
} else if (onSelectChanged != null) { } else if (onSelectChanged != null) {
label = TableRowInkWell( label = TableRowInkWell(
onTap: onSelectChanged, onTap: onSelectChanged,
child: label, child: label,
overlayColor: overlayColor,
); );
} }
return label; return label;
...@@ -632,26 +715,43 @@ class DataTable extends StatelessWidget { ...@@ -632,26 +715,43 @@ class DataTable extends StatelessWidget {
assert(!_debugInteractive || debugCheckHasMaterial(context)); assert(!_debugInteractive || debugCheckHasMaterial(context));
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
final BoxDecoration _kSelectedDecoration = BoxDecoration( final MaterialStateProperty<Color> defaultRowColor = MaterialStateProperty.resolveWith(
border: Border(bottom: Divider.createBorderSide(context, width: dividerThickness)), (Set<MaterialState> states) {
// The backgroundColor has to be transparent so you can see the ink on the material if (states.contains(MaterialState.selected)) {
color: (Theme.of(context).brightness == Brightness.light) ? _grey100Opacity : _grey300Opacity, // TODO(per): Add theming support for DataTable, https://github.com/flutter/flutter/issues/56079.
); // The color has to be transparent so you can see the ink on
final BoxDecoration _kUnselectedDecoration = BoxDecoration( // the [Material].
border: Border(bottom: Divider.createBorderSide(context, width: dividerThickness)), return (Theme.of(context).brightness == Brightness.light) ?
_grey100Opacity : _grey300Opacity;
}
return null;
},
); );
final bool anyRowSelectable = rows.any((DataRow row) => row.onSelectChanged != null);
final bool displayCheckboxColumn = showCheckboxColumn && rows.any((DataRow row) => row.onSelectChanged != null); final bool displayCheckboxColumn = showCheckboxColumn && anyRowSelectable;
final bool allChecked = displayCheckboxColumn && !rows.any((DataRow row) => row.onSelectChanged != null && !row.selected); final bool allChecked = displayCheckboxColumn && !rows.any((DataRow row) => row.onSelectChanged != null && !row.selected);
final List<TableColumnWidth> tableColumns = List<TableColumnWidth>(columns.length + (displayCheckboxColumn ? 1 : 0)); final List<TableColumnWidth> tableColumns = List<TableColumnWidth>(columns.length + (displayCheckboxColumn ? 1 : 0));
final List<TableRow> tableRows = List<TableRow>.generate( final List<TableRow> tableRows = List<TableRow>.generate(
rows.length + 1, // the +1 is for the header row rows.length + 1, // the +1 is for the header row
(int index) { (int index) {
final bool isSelected = index > 0 && rows[index - 1].selected;
final bool isDisabled = index > 0 && anyRowSelectable && rows[index - 1].onSelectChanged == null;
final Set<MaterialState> states = <MaterialState>{
if (isSelected)
MaterialState.selected,
if (isDisabled)
MaterialState.disabled,
};
final Color rowColor = index > 0 ? rows[index - 1].color?.resolve(states) : null;
return TableRow( return TableRow(
key: index == 0 ? _headingRowKey : rows[index - 1].key, key: index == 0 ? _headingRowKey : rows[index - 1].key,
decoration: index > 0 && rows[index - 1].selected ? _kSelectedDecoration decoration: BoxDecoration(
: _kUnselectedDecoration, border: Border(
bottom: Divider.createBorderSide(context, width: dividerThickness),
),
color: rowColor ?? defaultRowColor.resolve(states),
),
children: List<Widget>(tableColumns.length), children: List<Widget>(tableColumns.length),
); );
}, },
...@@ -663,17 +763,18 @@ class DataTable extends StatelessWidget { ...@@ -663,17 +763,18 @@ class DataTable extends StatelessWidget {
if (displayCheckboxColumn) { if (displayCheckboxColumn) {
tableColumns[0] = FixedColumnWidth(horizontalMargin + Checkbox.width + horizontalMargin / 2.0); tableColumns[0] = FixedColumnWidth(horizontalMargin + Checkbox.width + horizontalMargin / 2.0);
tableRows[0].children[0] = _buildCheckbox( tableRows[0].children[0] = _buildCheckbox(
color: theme.accentColor, activeColor: theme.accentColor,
checked: allChecked, checked: allChecked,
onCheckboxChanged: _handleSelectAll, onCheckboxChanged: _handleSelectAll,
); );
rowIndex = 1; rowIndex = 1;
for (final DataRow row in rows) { for (final DataRow row in rows) {
tableRows[rowIndex].children[0] = _buildCheckbox( tableRows[rowIndex].children[0] = _buildCheckbox(
color: theme.accentColor, activeColor: theme.accentColor,
checked: row.selected, checked: row.selected,
onRowTap: () => row.onSelectChanged != null ? row.onSelectChanged(!row.selected) : null , onRowTap: () => row.onSelectChanged != null ? row.onSelectChanged(!row.selected) : null ,
onCheckboxChanged: row.onSelectChanged, onCheckboxChanged: row.onSelectChanged,
overlayColor: row.color,
); );
rowIndex += 1; rowIndex += 1;
} }
...@@ -730,6 +831,7 @@ class DataTable extends StatelessWidget { ...@@ -730,6 +831,7 @@ class DataTable extends StatelessWidget {
showEditIcon: cell.showEditIcon, showEditIcon: cell.showEditIcon,
onTap: cell.onTap, onTap: cell.onTap,
onSelectChanged: () => row.onSelectChanged != null ? row.onSelectChanged(!row.selected) : null, onSelectChanged: () => row.onSelectChanged != null ? row.onSelectChanged(!row.selected) : null,
overlayColor: row.color,
); );
rowIndex += 1; rowIndex += 1;
} }
...@@ -765,6 +867,7 @@ class TableRowInkWell extends InkResponse { ...@@ -765,6 +867,7 @@ class TableRowInkWell extends InkResponse {
GestureTapCallback onDoubleTap, GestureTapCallback onDoubleTap,
GestureLongPressCallback onLongPress, GestureLongPressCallback onLongPress,
ValueChanged<bool> onHighlightChanged, ValueChanged<bool> onHighlightChanged,
MaterialStateProperty<Color> overlayColor,
}) : super( }) : super(
key: key, key: key,
child: child, child: child,
...@@ -774,6 +877,7 @@ class TableRowInkWell extends InkResponse { ...@@ -774,6 +877,7 @@ class TableRowInkWell extends InkResponse {
onHighlightChanged: onHighlightChanged, onHighlightChanged: onHighlightChanged,
containedInkWell: true, containedInkWell: true,
highlightShape: BoxShape.rectangle, highlightShape: BoxShape.rectangle,
overlayColor: overlayColor,
); );
@override @override
......
...@@ -503,7 +503,7 @@ class InkResponse extends StatelessWidget { ...@@ -503,7 +503,7 @@ class InkResponse extends StatelessWidget {
/// See also: /// See also:
/// ///
/// * The Material Design specification for overlay colors and how they /// * The Material Design specification for overlay colors and how they
/// to a component's state: /// match a component's state:
/// <https://material.io/design/interaction/states.html#anatomy>. /// <https://material.io/design/interaction/states.html#anatomy>.
final MaterialStateProperty<Color> overlayColor; final MaterialStateProperty<Color> overlayColor;
......
...@@ -8,6 +8,7 @@ import 'package:flutter/gestures.dart'; ...@@ -8,6 +8,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
import 'data_table_test_utils.dart'; import 'data_table_test_utils.dart';
void main() { void main() {
...@@ -1036,4 +1037,144 @@ void main() { ...@@ -1036,4 +1037,144 @@ void main() {
// after the view is destroyed, which causes exceptions. // after the view is destroyed, which causes exceptions.
await tester.pumpAndSettle(const Duration(seconds: 1)); await tester.pumpAndSettle(const Duration(seconds: 1));
}); });
testWidgets('DataRow renders custom colors when selected', (WidgetTester tester) async {
const Color selectedColor = Colors.green;
const Color defaultColor = Colors.red;
Widget buildTable({bool selected = false}) {
return Material(
child: DataTable(
columns: const <DataColumn>[
DataColumn(
label: Text('Column1'),
),
],
rows: <DataRow>[
DataRow(
selected: selected,
color: MaterialStateProperty.resolveWith<Color>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.selected))
return selectedColor;
return defaultColor;
},
),
cells: const <DataCell>[
DataCell(Text('Content1')),
],
),
],
),
);
}
BoxDecoration lastTableRowBoxDecoration() {
final Table table = tester.widget(find.byType(Table));
final TableRow tableRow = table.children.last;
return tableRow.decoration as BoxDecoration;
}
await tester.pumpWidget(MaterialApp(
home: buildTable(),
));
expect(lastTableRowBoxDecoration().color, defaultColor);
await tester.pumpWidget(MaterialApp(
home: buildTable(selected: true),
));
expect(lastTableRowBoxDecoration().color, selectedColor);
});
testWidgets('DataRow renders custom colors when disabled', (WidgetTester tester) async {
const Color disabledColor = Colors.grey;
const Color defaultColor = Colors.red;
Widget buildTable({bool disabled = false}) {
return Material(
child: DataTable(
columns: const <DataColumn>[
DataColumn(
label: Text('Column1'),
),
],
rows: <DataRow>[
DataRow(
cells: const <DataCell>[
DataCell(Text('Content1')),
],
onSelectChanged: (bool value) {},
),
DataRow(
color: MaterialStateProperty.resolveWith<Color>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled))
return disabledColor;
return defaultColor;
},
),
cells: const <DataCell>[
DataCell(Text('Content2')),
],
onSelectChanged: disabled ? null : (bool value) {},
),
],
),
);
}
BoxDecoration lastTableRowBoxDecoration() {
final Table table = tester.widget(find.byType(Table));
final TableRow tableRow = table.children.last;
return tableRow.decoration as BoxDecoration;
}
await tester.pumpWidget(MaterialApp(
home: buildTable(),
));
expect(lastTableRowBoxDecoration().color, defaultColor);
await tester.pumpWidget(MaterialApp(
home: buildTable(disabled: true),
));
expect(lastTableRowBoxDecoration().color, disabledColor);
});
testWidgets('DataRow renders custom colors when pressed', (WidgetTester tester) async {
const Color pressedColor = Color(0xff4caf50);
Widget buildTable() {
return DataTable(
columns: const <DataColumn>[
DataColumn(
label: Text('Column1'),
),
],
rows: <DataRow>[
DataRow(
color: MaterialStateProperty.resolveWith<Color>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed))
return pressedColor;
return Colors.transparent;
},
),
onSelectChanged: (bool value) {},
cells: const <DataCell>[
DataCell(Text('Content1')),
],
),
]
);
}
await tester.pumpWidget(MaterialApp(
home: Material(child: buildTable()),
));
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Content1')));
await tester.pump(const Duration(milliseconds: 200)); // splash is well underway
final RenderBox box = Material.of(tester.element(find.byType(InkWell))) as RenderBox;
expect(box, paints..circle(x: 64.0, y: 24.0, color: pressedColor));
await gesture.up();
});
} }
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