Header for PaginatedDataTable (#4468)
Also renames ButtonTheme.footer to ButtonTheme.bar.
This commit is contained in:
parent
1fe57277cf
commit
f75fd5c389
@ -96,7 +96,7 @@ class TravelDestinationItem extends StatelessWidget {
|
||||
),
|
||||
// share, explore buttons
|
||||
// TODO(abarth): The theme and the bar should be part of card.
|
||||
new ButtonTheme.footer(
|
||||
new ButtonTheme.bar(
|
||||
child: new ButtonBar(
|
||||
alignment: MainAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
|
@ -91,6 +91,8 @@ class DesertDataSource extends DataTableSource {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
int _selectedCount = 0;
|
||||
|
||||
@override
|
||||
DataRow getRow(int index) {
|
||||
assert(index >= 0);
|
||||
@ -101,8 +103,12 @@ class DesertDataSource extends DataTableSource {
|
||||
index: index,
|
||||
selected: desert.selected,
|
||||
onSelectChanged: (bool value) {
|
||||
desert.selected = value;
|
||||
notifyListeners();
|
||||
if (desert.selected != value) {
|
||||
_selectedCount += value ? 1 : -1;
|
||||
assert(_selectedCount >= 0);
|
||||
desert.selected = value;
|
||||
notifyListeners();
|
||||
}
|
||||
},
|
||||
cells: <DataCell>[
|
||||
new DataCell(new Text('${desert.name}')),
|
||||
@ -122,6 +128,16 @@ class DesertDataSource extends DataTableSource {
|
||||
|
||||
@override
|
||||
bool get isRowCountApproximate => false;
|
||||
|
||||
@override
|
||||
int get selectedRowCount => _selectedCount;
|
||||
|
||||
void _selectAll(bool checked) {
|
||||
for (Desert desert in _deserts)
|
||||
desert.selected = checked;
|
||||
_selectedCount = checked ? _deserts.length : 0;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
class DataTableDemo extends StatefulWidget {
|
||||
@ -135,10 +151,10 @@ class _DataTableDemoState extends State<DataTableDemo> {
|
||||
int _rowsPerPage = PaginatedDataTable.defaultRowsPerPage;
|
||||
int _sortColumnIndex;
|
||||
bool _sortAscending = true;
|
||||
DesertDataSource _deserts = new DesertDataSource();
|
||||
DesertDataSource _desertsDataSource = new DesertDataSource();
|
||||
|
||||
void _sort/*<T>*/(Comparable<dynamic/*=T*/> getField(Desert d), int columnIndex, bool ascending) {
|
||||
_deserts._sort/*<T>*/(getField, ascending);
|
||||
_desertsDataSource._sort/*<T>*/(getField, ascending);
|
||||
setState(() {
|
||||
_sortColumnIndex = columnIndex;
|
||||
_sortAscending = ascending;
|
||||
@ -153,10 +169,12 @@ class _DataTableDemoState extends State<DataTableDemo> {
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
children: <Widget>[
|
||||
new PaginatedDataTable(
|
||||
header: new Text('Nutrition'),
|
||||
rowsPerPage: _rowsPerPage,
|
||||
onRowsPerPageChanged: (int value) { setState(() { _rowsPerPage = value; }); },
|
||||
sortColumnIndex: _sortColumnIndex,
|
||||
sortAscending: _sortAscending,
|
||||
onSelectAll: _desertsDataSource._selectAll,
|
||||
columns: <DataColumn>[
|
||||
new DataColumn(
|
||||
label: new Text('Dessert (100g serving)'),
|
||||
@ -200,7 +218,7 @@ class _DataTableDemoState extends State<DataTableDemo> {
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.iron, columnIndex, ascending)
|
||||
),
|
||||
],
|
||||
source: _deserts
|
||||
source: _desertsDataSource
|
||||
)
|
||||
]
|
||||
)
|
||||
|
@ -50,15 +50,21 @@ class ButtonTheme extends InheritedWidget {
|
||||
Widget child
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
/// Creates a button theme that is appropriate for footer buttons.
|
||||
/// Creates a button theme that is appropriate for button bars, as used in
|
||||
/// dialog footers and in the headers of data tables.
|
||||
///
|
||||
/// This theme is denser, with a smaller [minWidth] and [padding], than the
|
||||
/// default theme. Also, this theme uses [ButtonTextTheme.accent] rather than
|
||||
/// [ButtonTextTheme.normal].
|
||||
///
|
||||
/// For best effect, the label of the button at the edge of the container
|
||||
/// should have text that ends up wider than 64.0 pixels. This ensures that
|
||||
/// the alignment of the text matches the alignment of the edge of the
|
||||
/// container.
|
||||
///
|
||||
/// For example, buttons at the bottom of [Dialog] or [Card] widgets use this
|
||||
/// button theme.
|
||||
const ButtonTheme.footer({
|
||||
const ButtonTheme.bar({
|
||||
Key key,
|
||||
this.textTheme: ButtonTextTheme.accent,
|
||||
this.minWidth: 64.0,
|
||||
|
@ -51,7 +51,7 @@ class ButtonBar extends StatelessWidget {
|
||||
),
|
||||
child: new Row(
|
||||
mainAxisAlignment: alignment,
|
||||
children: children.map((Widget child) {
|
||||
children: children.map/*<Widget>*/((Widget child) {
|
||||
return new Padding(
|
||||
padding: new EdgeInsets.symmetric(horizontal: paddingUnit),
|
||||
child: child
|
||||
|
@ -54,4 +54,9 @@ abstract class DataTableSource extends ChangeNotifier {
|
||||
/// then later the exact number becomes available, then call
|
||||
/// [notifyListeners].
|
||||
bool get isRowCountApproximate;
|
||||
|
||||
/// Called to obtain the number of rows that are currently selected.
|
||||
///
|
||||
/// If the selected row count changes, call [notifyListeners].
|
||||
int get selectedRowCount;
|
||||
}
|
@ -96,7 +96,7 @@ class Dialog extends StatelessWidget {
|
||||
}
|
||||
|
||||
if (actions != null) {
|
||||
dialogBody.add(new ButtonTheme.footer(
|
||||
dialogBody.add(new ButtonTheme.bar(
|
||||
child: new ButtonBar(
|
||||
alignment: MainAxisAlignment.end,
|
||||
children: actions
|
||||
|
@ -4,9 +4,12 @@
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
import 'button.dart';
|
||||
import 'button_bar.dart';
|
||||
import 'card.dart';
|
||||
import 'data_table.dart';
|
||||
import 'data_table_source.dart';
|
||||
@ -23,6 +26,9 @@ import 'theme.dart';
|
||||
class PaginatedDataTable extends StatefulWidget {
|
||||
/// Creates a widget describing a paginated [DataTable] on a [Card].
|
||||
///
|
||||
/// The [header] should give the card's header, typically a [Text] widget. It
|
||||
/// must not be null.
|
||||
///
|
||||
/// The [columns] argument must be a list of as many [DataColumn] objects as
|
||||
/// the table is to have columns, ignoring the leading checkbox column if any.
|
||||
/// The [columns] argument must have a length greater than zero and cannot be
|
||||
@ -43,10 +49,12 @@ class PaginatedDataTable extends StatefulWidget {
|
||||
/// widget unless the data table really is to now show entirely different
|
||||
/// data from a new source.
|
||||
///
|
||||
/// The [rowsPerPage] and [availableRowsPerPage] must not be null (though they
|
||||
/// both have defaults, so don't have to be specified).
|
||||
/// The [rowsPerPage] and [availableRowsPerPage] must not be null (they
|
||||
/// both have defaults, though, so don't have to be specified).
|
||||
PaginatedDataTable({
|
||||
Key key,
|
||||
@required this.header,
|
||||
this.actions,
|
||||
this.columns,
|
||||
this.sortColumnIndex,
|
||||
this.sortAscending: true,
|
||||
@ -56,8 +64,9 @@ class PaginatedDataTable extends StatefulWidget {
|
||||
this.rowsPerPage: defaultRowsPerPage,
|
||||
this.availableRowsPerPage: const <int>[defaultRowsPerPage, defaultRowsPerPage * 2, defaultRowsPerPage * 5, defaultRowsPerPage * 10],
|
||||
this.onRowsPerPageChanged,
|
||||
this.source
|
||||
@required this.source
|
||||
}) : super(key: key) {
|
||||
assert(header != null);
|
||||
assert(columns != null);
|
||||
assert(columns.length > 0);
|
||||
assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length));
|
||||
@ -69,6 +78,24 @@ class PaginatedDataTable extends StatefulWidget {
|
||||
assert(source != null);
|
||||
}
|
||||
|
||||
/// The table card's header.
|
||||
///
|
||||
/// This is typically a [Text] widget, but can also be a [ButtonBar] with
|
||||
/// [FlatButton]s. Suitable defaults are automatically provided for the font,
|
||||
/// button color, button padding, and so forth.
|
||||
///
|
||||
/// If items in the table are selectable, then, when the selection is not
|
||||
/// empty, the header is replaced by a count of the selected items.
|
||||
final Widget header;
|
||||
|
||||
/// Icon buttons to show at the top right of the table.
|
||||
///
|
||||
/// Typically, the exact actions included in this list will vary based on
|
||||
/// whether any rows are selected or not.
|
||||
///
|
||||
/// These should be size 24.0 with default padding (8.0).
|
||||
final List<Widget> actions;
|
||||
|
||||
/// The configuration and labels for the columns in the table.
|
||||
final List<DataColumn> columns;
|
||||
|
||||
@ -142,6 +169,7 @@ class PaginatedDataTableState extends State<PaginatedDataTable> {
|
||||
int _firstRowIndex;
|
||||
int _rowCount;
|
||||
bool _rowCountApproximate;
|
||||
int _selectedRowCount;
|
||||
final Map<int, DataRow> _rows = <int, DataRow>{};
|
||||
|
||||
@override
|
||||
@ -172,6 +200,7 @@ class PaginatedDataTableState extends State<PaginatedDataTable> {
|
||||
setState(() {
|
||||
_rowCount = config.source.rowCount;
|
||||
_rowCountApproximate = config.source.isRowCountApproximate;
|
||||
_selectedRowCount = config.source.selectedRowCount;
|
||||
_rows.clear();
|
||||
});
|
||||
}
|
||||
@ -237,7 +266,42 @@ class PaginatedDataTableState extends State<PaginatedDataTable> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TextStyle textStyle = Theme.of(context).textTheme.caption;
|
||||
// TODO(ianh): This whole build function doesn't handle RTL yet.
|
||||
ThemeData themeData = Theme.of(context);
|
||||
// HEADER
|
||||
final List<Widget> headerWidgets = <Widget>[];
|
||||
double leftPadding = 24.0;
|
||||
if (_selectedRowCount == 0) {
|
||||
headerWidgets.add(new Flexible(child: config.header));
|
||||
if (config.header is ButtonBar) {
|
||||
// We adjust the padding when a button bar is present, because the
|
||||
// ButtonBar introduces 2 pixels of outside padding, plus 2 pixels
|
||||
// around each button on each side, and the button itself will have 8
|
||||
// pixels internally on each side, yet we want the left edge of the
|
||||
// inside of the button to line up with the 24.0 left inset.
|
||||
// TODO(ianh): Better magic. See https://github.com/flutter/flutter/issues/4460
|
||||
leftPadding = 12.0;
|
||||
}
|
||||
} else if (_selectedRowCount == 1) {
|
||||
// TODO(ianh): Real l10n.
|
||||
headerWidgets.add(new Flexible(child: new Text('1 item selected')));
|
||||
} else {
|
||||
headerWidgets.add(new Flexible(child: new Text('$_selectedRowCount items selected')));
|
||||
}
|
||||
if (config.actions != null) {
|
||||
headerWidgets.addAll(
|
||||
config.actions.map/*<Widget>*/((Widget widget) {
|
||||
return new Padding(
|
||||
// 8.0 is the default padding of an icon button
|
||||
padding: new EdgeInsets.only(left: 24.0 - 8.0 * 2.0),
|
||||
child: widget
|
||||
);
|
||||
}).toList()
|
||||
);
|
||||
}
|
||||
|
||||
// FOOTER
|
||||
final TextStyle footerTextStyle = themeData.textTheme.caption;
|
||||
final List<Widget> footerWidgets = <Widget>[];
|
||||
if (config.onRowsPerPageChanged != null) {
|
||||
List<Widget> availableRowsPerPage = config.availableRowsPerPage
|
||||
@ -256,7 +320,7 @@ class PaginatedDataTableState extends State<PaginatedDataTable> {
|
||||
items: availableRowsPerPage,
|
||||
value: config.rowsPerPage,
|
||||
onChanged: config.onRowsPerPageChanged,
|
||||
style: textStyle,
|
||||
style: footerTextStyle,
|
||||
iconSize: 24.0
|
||||
)
|
||||
),
|
||||
@ -285,21 +349,39 @@ class PaginatedDataTableState extends State<PaginatedDataTable> {
|
||||
),
|
||||
new Container(width: 14.0),
|
||||
]);
|
||||
|
||||
// CARD
|
||||
return new Card(
|
||||
// TODO(ianh): data table card headers
|
||||
/*
|
||||
- title, top left
|
||||
- 20px Roboto Regular, black87
|
||||
- persistent actions, top left
|
||||
- header when there's a selection
|
||||
- accent 50?
|
||||
- show number of selected items
|
||||
- different actions
|
||||
- actions, top right
|
||||
- 24px icons, black54
|
||||
*/
|
||||
child: new BlockBody(
|
||||
children: <Widget>[
|
||||
new DefaultTextStyle(
|
||||
// These typographic styles aren't quite the regular ones. We pick the closest ones from the regular
|
||||
// list and then tweak them appropriately.
|
||||
// See https://www.google.com/design/spec/components/data-tables.html#data-tables-tables-within-cards
|
||||
style: _selectedRowCount > 0 ? themeData.textTheme.subhead.copyWith(color: themeData.accentColor)
|
||||
: themeData.textTheme.title.copyWith(fontWeight: FontWeight.w400),
|
||||
child: new IconTheme(
|
||||
data: new IconThemeData(
|
||||
opacity: 0.54
|
||||
),
|
||||
child: new ButtonTheme.bar(
|
||||
child: new Container(
|
||||
height: 64.0,
|
||||
padding: new EdgeInsets.fromLTRB(leftPadding, 0.0, 14.0, 0.0),
|
||||
// TODO(ianh): This decoration will prevent ink splashes from being visible.
|
||||
// Instead, we should have a widget that prints the decoration on the material.
|
||||
// See https://github.com/flutter/flutter/issues/3782
|
||||
decoration: _selectedRowCount > 0 ? new BoxDecoration(
|
||||
backgroundColor: themeData.secondaryHeaderColor
|
||||
) : null,
|
||||
child: new Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: headerWidgets
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
new ScrollableViewport(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: new DataTable(
|
||||
@ -312,7 +394,7 @@ class PaginatedDataTableState extends State<PaginatedDataTable> {
|
||||
)
|
||||
),
|
||||
new DefaultTextStyle(
|
||||
style: textStyle,
|
||||
style: footerTextStyle,
|
||||
child: new IconTheme(
|
||||
data: new IconThemeData(
|
||||
opacity: 0.54
|
||||
|
@ -67,6 +67,7 @@ class ThemeData {
|
||||
Color unselectedWidgetColor,
|
||||
Color disabledColor,
|
||||
Color buttonColor,
|
||||
Color secondaryHeaderColor,
|
||||
Color textSelectionColor,
|
||||
Color textSelectionHandleColor,
|
||||
Color backgroundColor,
|
||||
@ -93,6 +94,7 @@ class ThemeData {
|
||||
unselectedWidgetColor ??= isDark ? Colors.white70 : Colors.black54;
|
||||
disabledColor ??= isDark ? Colors.white30 : Colors.black26;
|
||||
buttonColor ??= isDark ? primarySwatch[600] : Colors.grey[300];
|
||||
secondaryHeaderColor ??= primarySwatch[50]; // TODO(ianh): dark theme support (https://github.com/flutter/flutter/issues/3370)
|
||||
textSelectionColor ??= isDark ? accentColor : primarySwatch[200];
|
||||
textSelectionHandleColor ??= isDark ? Colors.tealAccent[400] : primarySwatch[300];
|
||||
backgroundColor ??= isDark ? Colors.grey[700] : primarySwatch[200];
|
||||
@ -117,6 +119,7 @@ class ThemeData {
|
||||
unselectedWidgetColor: unselectedWidgetColor,
|
||||
disabledColor: disabledColor,
|
||||
buttonColor: buttonColor,
|
||||
secondaryHeaderColor: secondaryHeaderColor,
|
||||
textSelectionColor: textSelectionColor,
|
||||
textSelectionHandleColor: textSelectionHandleColor,
|
||||
backgroundColor: backgroundColor,
|
||||
@ -150,6 +153,7 @@ class ThemeData {
|
||||
this.unselectedWidgetColor,
|
||||
this.disabledColor,
|
||||
this.buttonColor,
|
||||
this.secondaryHeaderColor,
|
||||
this.textSelectionColor,
|
||||
this.textSelectionHandleColor,
|
||||
this.backgroundColor,
|
||||
@ -174,6 +178,7 @@ class ThemeData {
|
||||
assert(unselectedWidgetColor != null);
|
||||
assert(disabledColor != null);
|
||||
assert(buttonColor != null);
|
||||
assert(secondaryHeaderColor != null);
|
||||
assert(textSelectionColor != null);
|
||||
assert(textSelectionHandleColor != null);
|
||||
assert(disabledColor != null);
|
||||
@ -257,6 +262,12 @@ class ThemeData {
|
||||
/// The default color of the [Material] used in [RaisedButton]s.
|
||||
final Color buttonColor;
|
||||
|
||||
/// The color of the header of a [PaginatedDataTable] when there are selected rows.
|
||||
// According to the spec for data tables:
|
||||
// https://material.google.com/components/data-tables.html#data-tables-tables-within-cards
|
||||
// ...this should be the "50-value of secondary app color".
|
||||
final Color secondaryHeaderColor;
|
||||
|
||||
/// The color of text selections in text fields, such as [Input].
|
||||
final Color textSelectionColor;
|
||||
|
||||
@ -301,6 +312,7 @@ class ThemeData {
|
||||
unselectedWidgetColor: Color.lerp(begin.unselectedWidgetColor, end.unselectedWidgetColor, t),
|
||||
disabledColor: Color.lerp(begin.disabledColor, end.disabledColor, t),
|
||||
buttonColor: Color.lerp(begin.buttonColor, end.buttonColor, t),
|
||||
secondaryHeaderColor: Color.lerp(begin.secondaryHeaderColor, end.secondaryHeaderColor, t),
|
||||
textSelectionColor: Color.lerp(begin.textSelectionColor, end.textSelectionColor, t),
|
||||
textSelectionHandleColor: Color.lerp(begin.textSelectionHandleColor, end.textSelectionHandleColor, t),
|
||||
backgroundColor: Color.lerp(begin.backgroundColor, end.backgroundColor, t),
|
||||
@ -332,6 +344,7 @@ class ThemeData {
|
||||
(otherData.unselectedWidgetColor == unselectedWidgetColor) &&
|
||||
(otherData.disabledColor == disabledColor) &&
|
||||
(otherData.buttonColor == buttonColor) &&
|
||||
(otherData.secondaryHeaderColor == secondaryHeaderColor) &&
|
||||
(otherData.textSelectionColor == textSelectionColor) &&
|
||||
(otherData.textSelectionHandleColor == textSelectionHandleColor) &&
|
||||
(otherData.backgroundColor == backgroundColor) &&
|
||||
@ -360,12 +373,13 @@ class ThemeData {
|
||||
unselectedWidgetColor,
|
||||
disabledColor,
|
||||
buttonColor,
|
||||
secondaryHeaderColor,
|
||||
textSelectionColor,
|
||||
textSelectionHandleColor,
|
||||
backgroundColor,
|
||||
accentColor,
|
||||
accentColorBrightness,
|
||||
hashValues( // Too many values.
|
||||
accentColorBrightness,
|
||||
indicatorColor,
|
||||
hintColor,
|
||||
errorColor,
|
||||
|
Loading…
x
Reference in New Issue
Block a user