Provide UI to paginate PaginatedDataTable (#4382)
Also: * Make PaginatedDataTable able to scroll itself horizontally. * Make drop down buttons support having an explicit text style and icon size given. * Fix a bug with drop-down buttons asserting when opened partly off-screen. * Make sure to pop the drop-down button's route if the drop-down button is discarded while the route is up. * Remove extraneous padding on drop-down buttons. (Couldn't figure out why it was there, and it breaks alignment when a drop-down is mixed with other text.) * Some docs improvements. * Add Route.isActive * Add a setState() method to ModalRoutes.
This commit is contained in:
parent
5e6baf4a26
commit
68f92d4f34
@ -121,7 +121,7 @@ class DesertDataSource extends DataTableSource {
|
||||
int get rowCount => _deserts.length;
|
||||
|
||||
@override
|
||||
bool get isRowCountApproximate => true;
|
||||
bool get isRowCountApproximate => false;
|
||||
}
|
||||
|
||||
class DataTableDemo extends StatefulWidget {
|
||||
@ -132,6 +132,7 @@ class DataTableDemo extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _DataTableDemoState extends State<DataTableDemo> {
|
||||
int _rowsPerPage = PaginatedDataTable.defaultRowsPerPage;
|
||||
int _sortColumnIndex;
|
||||
bool _sortAscending = true;
|
||||
DesertDataSource _deserts = new DesertDataSource();
|
||||
@ -149,63 +150,57 @@ class _DataTableDemoState extends State<DataTableDemo> {
|
||||
return new Scaffold(
|
||||
appBar: new AppBar(title: new Text('Data tables')),
|
||||
body: new Block(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
children: <Widget>[
|
||||
new IntrinsicHeight(
|
||||
child: new Block(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: <Widget>[
|
||||
new PaginatedDataTable(
|
||||
rowsPerPage: 10,
|
||||
sortColumnIndex: _sortColumnIndex,
|
||||
sortAscending: _sortAscending,
|
||||
columns: <DataColumn>[
|
||||
new DataColumn(
|
||||
label: new Text('Dessert (100g serving)'),
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<String>*/((Desert d) => d.name, columnIndex, ascending)
|
||||
),
|
||||
new DataColumn(
|
||||
label: new Text('Calories'),
|
||||
tooltip: 'The total amount of food energy in the given serving size.',
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.calories, columnIndex, ascending)
|
||||
),
|
||||
new DataColumn(
|
||||
label: new Text('Fat (g)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.fat, columnIndex, ascending)
|
||||
),
|
||||
new DataColumn(
|
||||
label: new Text('Carbs (g)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.carbs, columnIndex, ascending)
|
||||
),
|
||||
new DataColumn(
|
||||
label: new Text('Protein (g)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.protein, columnIndex, ascending)
|
||||
),
|
||||
new DataColumn(
|
||||
label: new Text('Sodium (mg)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.sodium, columnIndex, ascending)
|
||||
),
|
||||
new DataColumn(
|
||||
label: new Text('Calcium (%)'),
|
||||
tooltip: 'The amount of calcium as a percentage of the recommended daily amount.',
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.calcium, columnIndex, ascending)
|
||||
),
|
||||
new DataColumn(
|
||||
label: new Text('Iron (%)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.iron, columnIndex, ascending)
|
||||
),
|
||||
],
|
||||
source: _deserts
|
||||
)
|
||||
]
|
||||
)
|
||||
new PaginatedDataTable(
|
||||
rowsPerPage: _rowsPerPage,
|
||||
onRowsPerPageChanged: (int value) { setState(() { _rowsPerPage = value; }); },
|
||||
sortColumnIndex: _sortColumnIndex,
|
||||
sortAscending: _sortAscending,
|
||||
columns: <DataColumn>[
|
||||
new DataColumn(
|
||||
label: new Text('Dessert (100g serving)'),
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<String>*/((Desert d) => d.name, columnIndex, ascending)
|
||||
),
|
||||
new DataColumn(
|
||||
label: new Text('Calories'),
|
||||
tooltip: 'The total amount of food energy in the given serving size.',
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.calories, columnIndex, ascending)
|
||||
),
|
||||
new DataColumn(
|
||||
label: new Text('Fat (g)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.fat, columnIndex, ascending)
|
||||
),
|
||||
new DataColumn(
|
||||
label: new Text('Carbs (g)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.carbs, columnIndex, ascending)
|
||||
),
|
||||
new DataColumn(
|
||||
label: new Text('Protein (g)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.protein, columnIndex, ascending)
|
||||
),
|
||||
new DataColumn(
|
||||
label: new Text('Sodium (mg)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.sodium, columnIndex, ascending)
|
||||
),
|
||||
new DataColumn(
|
||||
label: new Text('Calcium (%)'),
|
||||
tooltip: 'The amount of calcium as a percentage of the recommended daily amount.',
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.calcium, columnIndex, ascending)
|
||||
),
|
||||
new DataColumn(
|
||||
label: new Text('Iron (%)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.iron, columnIndex, ascending)
|
||||
),
|
||||
],
|
||||
source: _deserts
|
||||
)
|
||||
]
|
||||
)
|
||||
|
@ -16,7 +16,6 @@ import 'theme.dart';
|
||||
import 'material.dart';
|
||||
|
||||
const Duration _kDropDownMenuDuration = const Duration(milliseconds: 300);
|
||||
const double _kTopMargin = 6.0;
|
||||
const double _kMenuItemHeight = 48.0;
|
||||
const EdgeInsets _kMenuVerticalPadding = const EdgeInsets.symmetric(vertical: 8.0);
|
||||
const EdgeInsets _kMenuHorizontalPadding = const EdgeInsets.symmetric(horizontal: 36.0);
|
||||
@ -136,6 +135,7 @@ class _DropDownMenu<T> extends StatusTransitionWidget {
|
||||
),
|
||||
child: new Material(
|
||||
type: MaterialType.transparency,
|
||||
textStyle: route.style,
|
||||
child: new ScrollableList(
|
||||
padding: _kMenuVerticalPadding,
|
||||
itemExtent: _kMenuItemHeight,
|
||||
@ -182,8 +182,17 @@ class _DropDownMenuRouteLayout extends SingleChildLayoutDelegate {
|
||||
bottom = math.max(buttonTop + _kMenuItemHeight, bottomPreferredLimit);
|
||||
top = bottom - childSize.height;
|
||||
}
|
||||
assert(top >= 0.0);
|
||||
assert(top + childSize.height <= size.height);
|
||||
assert(() {
|
||||
final Rect container = Point.origin & size;
|
||||
if (container.intersect(buttonRect) == buttonRect) {
|
||||
// If the button was entirely on-screen, then verify
|
||||
// that the menu is also on-screen.
|
||||
// If the button was a bit off-screen, then, oh well.
|
||||
assert(top >= 0.0);
|
||||
assert(top + childSize.height <= size.height);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return new Offset(buttonRect.left, top);
|
||||
}
|
||||
|
||||
@ -220,14 +229,28 @@ class _DropDownRoute<T> extends PopupRoute<_DropDownRouteResult<T>> {
|
||||
this.items,
|
||||
this.buttonRect,
|
||||
this.selectedIndex,
|
||||
this.elevation: 8
|
||||
}) : super(completer: completer);
|
||||
this.elevation: 8,
|
||||
TextStyle style
|
||||
}) : _style = style, super(completer: completer) {
|
||||
assert(style != null);
|
||||
}
|
||||
|
||||
final List<DropDownMenuItem<T>> items;
|
||||
final Rect buttonRect;
|
||||
final int selectedIndex;
|
||||
final int elevation;
|
||||
|
||||
TextStyle get style => _style;
|
||||
TextStyle _style;
|
||||
set style (TextStyle value) {
|
||||
assert(value != null);
|
||||
if (_style == value)
|
||||
return;
|
||||
setState(() {
|
||||
_style = value;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Duration get transitionDuration => _kDropDownMenuDuration;
|
||||
|
||||
@ -277,13 +300,10 @@ class DropDownMenuItem<T> extends StatelessWidget {
|
||||
return new Container(
|
||||
height: _kMenuItemHeight,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: new DefaultTextStyle(
|
||||
style: Theme.of(context).textTheme.subhead,
|
||||
child: new Baseline(
|
||||
baselineType: TextBaseline.alphabetic,
|
||||
baseline: _kMenuItemHeight - _kBaselineOffsetFromBottom,
|
||||
child: child
|
||||
)
|
||||
child: new Baseline(
|
||||
baselineType: TextBaseline.alphabetic,
|
||||
baseline: _kMenuItemHeight - _kBaselineOffsetFromBottom,
|
||||
child: child
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -332,12 +352,17 @@ class DropDownButton<T> extends StatefulWidget {
|
||||
/// Creates a drop down button.
|
||||
///
|
||||
/// The [items] must have distinct values and [value] must be among them.
|
||||
///
|
||||
/// The [elevation] and [iconSize] arguments must not be null (they both have
|
||||
/// defaults, so do not need to be specified).
|
||||
DropDownButton({
|
||||
Key key,
|
||||
this.items,
|
||||
this.value,
|
||||
this.onChanged,
|
||||
this.elevation: 8
|
||||
this.elevation: 8,
|
||||
this.style,
|
||||
this.iconSize: 36.0
|
||||
}) : super(key: key) {
|
||||
assert(items != null);
|
||||
assert(items.where((DropDownMenuItem<T> item) => item.value == value).length == 1);
|
||||
@ -357,6 +382,18 @@ class DropDownButton<T> extends StatefulWidget {
|
||||
/// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12, 16, 24
|
||||
final int elevation;
|
||||
|
||||
/// The text style to use for text in the drop down button and the drop down
|
||||
/// menu that appears when you tap the button.
|
||||
///
|
||||
/// Defaults to the [TextTheme.subhead] value of the current
|
||||
/// [ThemeData.textTheme] of the current [Theme].
|
||||
final TextStyle style;
|
||||
|
||||
/// The size to use for the drop-down button's down arrow icon button.
|
||||
///
|
||||
/// Defaults to 36.0.
|
||||
final double iconSize;
|
||||
|
||||
@override
|
||||
_DropDownButtonState<T> createState() => new _DropDownButtonState<T>();
|
||||
}
|
||||
@ -388,18 +425,26 @@ class _DropDownButtonState<T> extends State<DropDownButton<T>> {
|
||||
}
|
||||
}
|
||||
|
||||
TextStyle get _textStyle => config.style ?? Theme.of(context).textTheme.subhead;
|
||||
|
||||
_DropDownRoute<T> _currentRoute;
|
||||
|
||||
void _handleTap() {
|
||||
assert(_currentRoute == null);
|
||||
final RenderBox itemBox = _itemKey.currentContext.findRenderObject();
|
||||
final Rect itemRect = itemBox.localToGlobal(Point.origin) & itemBox.size;
|
||||
final Completer<_DropDownRouteResult<T>> completer = new Completer<_DropDownRouteResult<T>>();
|
||||
Navigator.push(context, new _DropDownRoute<T>(
|
||||
_currentRoute = new _DropDownRoute<T>(
|
||||
completer: completer,
|
||||
items: config.items,
|
||||
buttonRect: _kMenuHorizontalPadding.inflateRect(itemRect),
|
||||
selectedIndex: _selectedIndex,
|
||||
elevation: config.elevation
|
||||
));
|
||||
elevation: config.elevation,
|
||||
style: _textStyle
|
||||
);
|
||||
Navigator.push(context, _currentRoute);
|
||||
completer.future.then((_DropDownRouteResult<T> newValue) {
|
||||
_currentRoute = null;
|
||||
if (!mounted || newValue == null)
|
||||
return;
|
||||
if (config.onChanged != null)
|
||||
@ -410,28 +455,28 @@ class _DropDownButtonState<T> extends State<DropDownButton<T>> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasMaterial(context));
|
||||
Widget result = new Row(
|
||||
mainAxisAlignment: MainAxisAlignment.collapse,
|
||||
children: <Widget>[
|
||||
// We use an IndexedStack to make sure we have enough width to show any
|
||||
// possible item as the selected item without changing size.
|
||||
new IndexedStack(
|
||||
children: config.items,
|
||||
key: _itemKey,
|
||||
index: _selectedIndex,
|
||||
alignment: FractionalOffset.centerLeft
|
||||
),
|
||||
new Icon(icon: Icons.arrow_drop_down, size: 36.0)
|
||||
]
|
||||
final TextStyle style = _textStyle;
|
||||
if (_currentRoute != null)
|
||||
_currentRoute.style = style;
|
||||
Widget result = new DefaultTextStyle(
|
||||
style: style,
|
||||
child: new Row(
|
||||
mainAxisAlignment: MainAxisAlignment.collapse,
|
||||
children: <Widget>[
|
||||
// We use an IndexedStack to make sure we have enough width to show any
|
||||
// possible item as the selected item without changing size.
|
||||
new IndexedStack(
|
||||
key: _itemKey,
|
||||
index: _selectedIndex,
|
||||
alignment: FractionalOffset.centerLeft,
|
||||
children: config.items
|
||||
),
|
||||
new Icon(icon: Icons.arrow_drop_down, size: config.iconSize)
|
||||
]
|
||||
)
|
||||
);
|
||||
if (DropDownButtonHideUnderline.at(context)) {
|
||||
result = new Padding(
|
||||
padding: const EdgeInsets.only(top: _kTopMargin, bottom: _kBottomBorderHeight),
|
||||
child: result
|
||||
);
|
||||
} else {
|
||||
if (!DropDownButtonHideUnderline.at(context)) {
|
||||
result = new Container(
|
||||
padding: const EdgeInsets.only(top: _kTopMargin),
|
||||
decoration: const BoxDecoration(border: _kDropDownUnderline),
|
||||
child: result
|
||||
);
|
||||
|
@ -3,6 +3,7 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'debug.dart';
|
||||
import 'icon.dart';
|
||||
@ -35,12 +36,18 @@ class IconButton extends StatelessWidget {
|
||||
/// be used in many other places as well.
|
||||
///
|
||||
/// Requires one of its ancestors to be a [Material] widget.
|
||||
///
|
||||
/// The [size], [padding], and [alignment] arguments must not be null (though
|
||||
/// they each have default values).
|
||||
///
|
||||
/// The [icon] argument must be specified. See [Icons] for a list of icons to
|
||||
/// use for this argument.
|
||||
const IconButton({
|
||||
Key key,
|
||||
this.size: 24.0,
|
||||
this.padding: const EdgeInsets.all(8.0),
|
||||
this.alignment: FractionalOffset.center,
|
||||
this.icon,
|
||||
@required this.icon,
|
||||
this.color,
|
||||
this.disabledColor,
|
||||
this.onPressed,
|
||||
@ -48,16 +55,24 @@ class IconButton extends StatelessWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
/// The size of the icon inside the button.
|
||||
///
|
||||
/// This property must not be null. It defaults to 24.0.
|
||||
final double size;
|
||||
|
||||
/// The padding around the button's icon. The entire padded icon will react
|
||||
/// to input gestures.
|
||||
///
|
||||
/// This property must not be null. It defaults to 8.0 padding on all sides.
|
||||
final EdgeInsets padding;
|
||||
|
||||
/// Defines how the icon is positioned within the IconButton.
|
||||
///
|
||||
/// This property must not be null. It defaults to [FractionalOffset.center].
|
||||
final FractionalOffset alignment;
|
||||
|
||||
/// The icon to display inside the button.
|
||||
/// The icon to display inside the button, from the list in [Icons].
|
||||
///
|
||||
/// This property must not be null.
|
||||
final IconData icon;
|
||||
|
||||
/// The color to use for the icon inside the button, if the icon is enabled.
|
||||
|
@ -2,12 +2,21 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
import 'card.dart';
|
||||
import 'data_table.dart';
|
||||
import 'data_table_source.dart';
|
||||
import 'drop_down.dart';
|
||||
import 'icon_button.dart';
|
||||
import 'icon_theme.dart';
|
||||
import 'icon_theme_data.dart';
|
||||
import 'icons.dart';
|
||||
import 'progress_indicator.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
/// A wrapper for [DataTable] that obtains data lazily from a [DataTableSource]
|
||||
/// and displays it one page at a time. The widget is presented as a [Card].
|
||||
@ -33,6 +42,9 @@ class PaginatedDataTable extends StatefulWidget {
|
||||
/// [DataTableSource] with each new instance of the [PaginatedDataTable]
|
||||
/// 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).
|
||||
PaginatedDataTable({
|
||||
Key key,
|
||||
this.columns,
|
||||
@ -41,7 +53,8 @@ class PaginatedDataTable extends StatefulWidget {
|
||||
this.onSelectAll,
|
||||
this.initialFirstRowIndex: 0,
|
||||
this.onPageChanged,
|
||||
this.rowsPerPage: 10,
|
||||
this.rowsPerPage: defaultRowsPerPage,
|
||||
this.availableRowsPerPage: const <int>[defaultRowsPerPage, defaultRowsPerPage * 2, defaultRowsPerPage * 5, defaultRowsPerPage * 10],
|
||||
this.onRowsPerPageChanged,
|
||||
this.source
|
||||
}) : super(key: key) {
|
||||
@ -49,6 +62,10 @@ class PaginatedDataTable extends StatefulWidget {
|
||||
assert(columns.length > 0);
|
||||
assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length));
|
||||
assert(sortAscending != null);
|
||||
assert(rowsPerPage != null);
|
||||
assert(rowsPerPage > 0);
|
||||
assert(availableRowsPerPage != null);
|
||||
assert(availableRowsPerPage.contains(rowsPerPage));
|
||||
assert(source != null);
|
||||
}
|
||||
|
||||
@ -76,13 +93,31 @@ class PaginatedDataTable extends StatefulWidget {
|
||||
final int initialFirstRowIndex;
|
||||
|
||||
/// Invoked when the user switches to another page.
|
||||
///
|
||||
/// The value is the index of the first row on the currently displayed page.
|
||||
final ValueChanged<int> onPageChanged;
|
||||
|
||||
/// The number of rows to show on each page.
|
||||
///
|
||||
/// See also [onRowsPerPageChanged].
|
||||
/// See also:
|
||||
///
|
||||
/// * [onRowsPerPageChanged]
|
||||
/// * [defaultRowsPerPage]
|
||||
final int rowsPerPage;
|
||||
|
||||
/// The default value for [rowsPerPage].
|
||||
///
|
||||
/// Useful when initializing the field that will hold the current
|
||||
/// [rowsPerPage], when implemented [onRowsPerPageChanged].
|
||||
static const int defaultRowsPerPage = 10;
|
||||
|
||||
/// The options to offer for the rowsPerPage.
|
||||
///
|
||||
/// The current [rowsPerPage] must be a value in this list.
|
||||
///
|
||||
/// The values in this list should be sorted in ascending order.
|
||||
final List<int> availableRowsPerPage;
|
||||
|
||||
/// Invoked when the user selects a different number of rows per page.
|
||||
///
|
||||
/// If this is null, then the value given by [rowsPerPage] will be used
|
||||
@ -142,11 +177,15 @@ class PaginatedDataTableState extends State<PaginatedDataTable> {
|
||||
}
|
||||
|
||||
/// Ensures that the given row is visible.
|
||||
void pageTo(double rowIndex) {
|
||||
void pageTo(int rowIndex) {
|
||||
final int oldFirstRowIndex = _firstRowIndex;
|
||||
setState(() {
|
||||
final int rowsPerPage = config.rowsPerPage;
|
||||
_firstRowIndex = (rowIndex ~/ rowsPerPage) * rowsPerPage;
|
||||
});
|
||||
if ((config.onPageChanged != null) &&
|
||||
(oldFirstRowIndex != _firstRowIndex))
|
||||
config.onPageChanged(_firstRowIndex);
|
||||
}
|
||||
|
||||
DataRow _getBlankRowFor(int index) {
|
||||
@ -198,40 +237,97 @@ class PaginatedDataTableState extends State<PaginatedDataTable> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<DataRow> rows = _getRows(_firstRowIndex, config.rowsPerPage);
|
||||
Widget table = new DataTable(
|
||||
key: _tableKey,
|
||||
columns: config.columns,
|
||||
sortColumnIndex: config.sortColumnIndex,
|
||||
sortAscending: config.sortAscending,
|
||||
onSelectAll: config.onSelectAll,
|
||||
rows: rows
|
||||
);
|
||||
final TextStyle textStyle = Theme.of(context).textTheme.caption;
|
||||
final List<Widget> footerWidgets = <Widget>[];
|
||||
if (config.onRowsPerPageChanged != null) {
|
||||
List<Widget> availableRowsPerPage = config.availableRowsPerPage
|
||||
.where((int value) => value <= _rowCount)
|
||||
.map/*<DropDownMenuItem<int>>*/((int value) {
|
||||
return new DropDownMenuItem<int>(
|
||||
value: value,
|
||||
child: new Text('$value')
|
||||
);
|
||||
})
|
||||
.toList();
|
||||
footerWidgets.addAll(<Widget>[
|
||||
new Text('Rows per page:'),
|
||||
new DropDownButtonHideUnderline(
|
||||
child: new DropDownButton<int>(
|
||||
items: availableRowsPerPage,
|
||||
value: config.rowsPerPage,
|
||||
onChanged: config.onRowsPerPageChanged,
|
||||
style: textStyle,
|
||||
iconSize: 24.0
|
||||
)
|
||||
),
|
||||
]);
|
||||
}
|
||||
footerWidgets.addAll(<Widget>[
|
||||
new Container(width: 32.0),
|
||||
new Text(
|
||||
'${_firstRowIndex + 1}\u2013${_firstRowIndex + config.rowsPerPage} ${ _rowCountApproximate ? "of about" : "of" } $_rowCount'
|
||||
),
|
||||
new Container(width: 32.0),
|
||||
new IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: Icons.chevron_left,
|
||||
onPressed: _firstRowIndex <= 0 ? null : () {
|
||||
pageTo(math.max(_firstRowIndex - config.rowsPerPage, 0));
|
||||
}
|
||||
),
|
||||
new Container(width: 24.0),
|
||||
new IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: Icons.chevron_right,
|
||||
onPressed: (!_rowCountApproximate && (_firstRowIndex + config.rowsPerPage >= _rowCount)) ? null : () {
|
||||
pageTo(_firstRowIndex + config.rowsPerPage);
|
||||
}
|
||||
),
|
||||
new Container(width: 14.0),
|
||||
]);
|
||||
return new Card(
|
||||
// TODO(ianh): data table card headers
|
||||
child: table
|
||||
// TODO(ianh): data table card footers: prev/next page, rows per page, etc
|
||||
/*
|
||||
- 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 ScrollableViewport(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: new DataTable(
|
||||
key: _tableKey,
|
||||
columns: config.columns,
|
||||
sortColumnIndex: config.sortColumnIndex,
|
||||
sortAscending: config.sortAscending,
|
||||
onSelectAll: config.onSelectAll,
|
||||
rows: _getRows(_firstRowIndex, config.rowsPerPage)
|
||||
)
|
||||
),
|
||||
new DefaultTextStyle(
|
||||
style: textStyle,
|
||||
child: new IconTheme(
|
||||
data: new IconThemeData(
|
||||
opacity: 0.54
|
||||
),
|
||||
child: new Container(
|
||||
height: 56.0,
|
||||
child: new Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: footerWidgets
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
DataTableCard
|
||||
- top: 64px
|
||||
- caption, 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
|
||||
|
||||
bottom:
|
||||
- 56px
|
||||
- handles pagination
|
||||
- 12px Roboto Regular, black54
|
||||
|
||||
*/
|
||||
|
@ -400,7 +400,9 @@ abstract class State<T extends StatefulWidget> {
|
||||
/// Whenever you need to change internal state for a State object, make the
|
||||
/// change in a function that you pass to setState(), as in:
|
||||
///
|
||||
/// setState(() { myState = newValue });
|
||||
/// ```dart
|
||||
/// setState(() { myState = newValue });
|
||||
/// ```
|
||||
///
|
||||
/// If you just change the state directly without calling setState(), then the
|
||||
/// widget will not be scheduled for rebuilding, meaning that its rendering
|
||||
|
@ -71,12 +71,29 @@ abstract class Route<T> {
|
||||
void dispose() { }
|
||||
|
||||
/// Whether this route is the top-most route on the navigator.
|
||||
///
|
||||
/// If this is true, then [isActive] is also true.
|
||||
bool get isCurrent {
|
||||
if (_navigator == null)
|
||||
return false;
|
||||
assert(_navigator._history.contains(this));
|
||||
return _navigator._history.last == this;
|
||||
}
|
||||
|
||||
/// Whether this route is on the navigator.
|
||||
///
|
||||
/// If the route is not only active, but also the current route (the top-most
|
||||
/// route), then [isCurrent] will also be true.
|
||||
///
|
||||
/// If a later route is entirely opaque, then the route will be active but not
|
||||
/// rendered. In particular, it's possible for a route to be active but for
|
||||
/// stateful widgets within the route to not be instantiated.
|
||||
bool get isActive {
|
||||
if (_navigator == null)
|
||||
return false;
|
||||
assert(_navigator._history.contains(this));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Data that might be useful in constructing a [Route].
|
||||
|
@ -4,6 +4,8 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'basic.dart';
|
||||
import 'focus.dart';
|
||||
import 'framework.dart';
|
||||
@ -400,11 +402,8 @@ class _ModalScopeState extends State<_ModalScope> {
|
||||
});
|
||||
}
|
||||
|
||||
void _didChangeRouteOffStage() {
|
||||
setState(() {
|
||||
// We use the route's offstage bool in our build function, which means our
|
||||
// state has changed.
|
||||
});
|
||||
void _routeSetState(VoidCallback fn) {
|
||||
setState(fn);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -466,6 +465,28 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
|
||||
return widget?.route;
|
||||
}
|
||||
|
||||
/// Whenever you need to change internal state for a ModalRoute object, make
|
||||
/// the change in a function that you pass to setState(), as in:
|
||||
///
|
||||
/// ```dart
|
||||
/// setState(() { myState = newValue });
|
||||
/// ```
|
||||
///
|
||||
/// If you just change the state directly without calling setState(), then the
|
||||
/// route will not be scheduled for rebuilding, meaning that its rendering
|
||||
/// will not be updated.
|
||||
@protected
|
||||
void setState(VoidCallback fn) {
|
||||
if (_scopeKey.currentState != null) {
|
||||
_scopeKey.currentState._routeSetState(fn);
|
||||
} else {
|
||||
// The route isn't currently visible, so we don't have to call its setState
|
||||
// method, but we do still need to call the fn callback, otherwise the state
|
||||
// in the route won't be updated!
|
||||
fn();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// The API for subclasses to override - used by _ModalScope
|
||||
|
||||
@ -528,8 +549,9 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
|
||||
set offstage (bool value) {
|
||||
if (_offstage == value)
|
||||
return;
|
||||
_offstage = value;
|
||||
_scopeKey.currentState?._didChangeRouteOffStage();
|
||||
setState(() {
|
||||
_offstage = value;
|
||||
});
|
||||
}
|
||||
|
||||
/// The build context for the subtree containing the primary content of this route.
|
||||
|
@ -721,10 +721,11 @@ class ScrollNotification extends Notification {
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ScrollableList]
|
||||
/// * [PageableList]
|
||||
/// * [ScrollableGrid]
|
||||
/// * [LazyBlock]
|
||||
/// * [ScrollableList], if you have many identically-sized children.
|
||||
/// * [PageableList], if you have children that each take the entire screen.
|
||||
/// * [ScrollableGrid], if your children are in a grid pattern.
|
||||
/// * [LazyBlock], if you have many children of varying sizes.
|
||||
/// * [Block], if your single child is a [BlockBody] or a [Column].
|
||||
class ScrollableViewport extends StatelessWidget {
|
||||
/// Creates a simple scrolling widget that has a single child.
|
||||
///
|
||||
@ -849,14 +850,18 @@ class ScrollableViewport extends StatelessWidget {
|
||||
/// arrange in a block layout and that might exceed the height of its container
|
||||
/// (and therefore need to scroll).
|
||||
///
|
||||
/// If you have a large number of children, consider using [LazyBlock] (if the
|
||||
/// children have variable height) or [ScrollableList] (if the children all have
|
||||
/// the same fixed height).
|
||||
/// If you have a large number of children, or if you always expect this to need
|
||||
/// to scroll, consider using [LazyBlock] (if the children have variable height)
|
||||
/// or [ScrollableList] (if the children all have the same fixed height), as
|
||||
/// they avoid doing work for children that are not visible.
|
||||
///
|
||||
/// If you have a single child, then use [ScrollableViewport] directly.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ScrollableList]
|
||||
/// * [LazyBlock]
|
||||
/// * [ScrollableViewport], if you only have one child
|
||||
/// * [ScrollableList], if all your children are the same height
|
||||
/// * [LazyBlock], if you have children with varying heights
|
||||
class Block extends StatelessWidget {
|
||||
/// Creates a scrollable array of children.
|
||||
Block({
|
||||
|
Loading…
x
Reference in New Issue
Block a user