Commit a91bc0ba authored by Ian Hickson's avatar Ian Hickson

Material Data Tables (#3337)

+ Add new demo to gallery to show data tables. (This currently doesn't
use a Card; I'll create a Card version in a subsequent patch.)

+ Fix checkbox alignment. It now centers in its box regardless.

+ Add Colors.black54.

+ Some minor fixes to dartdocs.

+ DataTable, DataColumn, DataRow, DataCell

+ RowInkWell

+ Augment dartdocs of materia/debug.dart.

+ DropDownButtonHideUnderline to hide the underline in a drop-down when
  used in a DataTable.

+ Add new capabilities to InkResponse to support RowInkWell.

+ Augment dartdocs of materia/material.dart.

+ Add an assert to catch nested Blocks.

+ Fix a crash in RenderBox when you remove an object and an ancestor
  used its baseline. (https://github.com/flutter/flutter/issues/2874)

+ Fix (and redocument) RenderBaseline/Baseline.

+ Add flex support to IntrinsicColumnWidth.

+ Document more stuff on the RenderTable side.

+ Fix a bug with parentData handling on RenderTable children.

+ Completely rewrite the column width computations. The old logic made
  no sense at all.

+ Add dartdocs to widgets/debug.dart.

+ Add a toString for TableRow.
parent db2f66aa
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/rendering.dart';
import 'package:flutter/material.dart';
class Desert {
Desert(this.name, this.calories, this.fat, this.carbs, this.protein, this.sodium, this.calcium, this.iron);
final String name;
final int calories;
final double fat;
final int carbs;
final double protein;
final int sodium;
final int calcium;
final int iron;
bool selected = false;
}
class DataTableDemo extends StatefulWidget {
@override
_DataTableDemoState createState() => new _DataTableDemoState();
}
class _DataTableDemoState extends State<DataTableDemo> {
int _sortColumnIndex;
bool _sortAscending = true;
final List<Desert> _deserts = [
new Desert('Frozen yogurt', 159, 6.0, 24, 4.0, 87, 14, 1),
new Desert('Ice cream sandwich', 237, 9.0, 37, 4.3, 129, 8, 1),
new Desert('Eclair', 262, 16.0, 24, 6.0, 337, 6, 7),
new Desert('Cupcake', 305, 3.7, 67, 4.3, 413, 3, 8),
new Desert('Gingerbread', 356, 16.0, 49, 3.9, 327, 7, 16),
new Desert('Jelly bean', 375, 0.0, 94, 0.0, 50, 0, 0),
new Desert('Lollipop', 392, 0.2, 98, 0.0, 38, 0, 2),
new Desert('Honeycomb', 408, 3.2, 87, 6.5, 562, 0, 45),
new Desert('Donut', 452, 25.0, 51, 4.9, 326, 2, 22),
new Desert('KitKat', 518, 26.0, 65, 7.0, 54, 12, 6),
];
void _sort/*<T>*/(Comparable<dynamic/*=T*/> getField(Desert d), int columnIndex, bool ascending) {
setState(() {
_deserts.sort((Desert a, Desert b) {
if (!ascending) {
final Desert c = a;
a = b;
b = c;
}
final Comparable<dynamic/*=T*/> aValue = getField(a);
final Comparable<dynamic/*=T*/> bValue = getField(b);
return Comparable.compare(aValue, bValue);
});
_sortColumnIndex = columnIndex;
_sortAscending = ascending;
});
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title: new Text('Data tables')),
body: new Block(
children: <Widget>[
new Material(
child: new IntrinsicHeight(
child: new Block(
scrollDirection: Axis.horizontal,
children: <Widget>[
new DataTable(
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)
),
],
rows: _deserts.map/*<DataRow>*/((Desert desert) {
return new DataRow(
key: new ValueKey<Desert>(desert),
selected: desert.selected,
onSelectChanged: (bool selected) { setState(() { desert.selected = selected; }); },
cells: <DataCell>[
new DataCell(new Text('${desert.name}')),
new DataCell(new Text('${desert.calories}')),
new DataCell(new Text('${desert.fat.toStringAsFixed(1)}')),
new DataCell(new Text('${desert.carbs}')),
new DataCell(new Text('${desert.protein.toStringAsFixed(1)}')),
new DataCell(new Text('${desert.sodium}')),
new DataCell(new Text('${desert.calcium}%')),
new DataCell(new Text('${desert.iron}%')),
]
);
}).toList(growable: false)
)
]
)
)
)
]
)
);
}
}
......@@ -13,6 +13,7 @@ import '../demo/buttons_demo.dart';
import '../demo/cards_demo.dart';
import '../demo/colors_demo.dart';
import '../demo/chip_demo.dart';
import '../demo/data_table_demo.dart';
import '../demo/date_picker_demo.dart';
import '../demo/dialog_demo.dart';
import '../demo/drop_down_demo.dart';
......@@ -122,6 +123,7 @@ class GalleryHomeState extends State<GalleryHome> {
new GalleryItem(title: 'Cards', builder: () => new CardsDemo()),
new GalleryItem(title: 'Chips', builder: () => new ChipDemo()),
new GalleryItem(title: 'Date picker', builder: () => new DatePickerDemo()),
new GalleryItem(title: 'Data tables', builder: () => new DataTableDemo()),
new GalleryItem(title: 'Dialog', builder: () => new DialogDemo()),
new GalleryItem(title: 'Drop-down button', builder: () => new DropDownDemo()),
new GalleryItem(title: 'Expand/collapse list control', builder: () => new TwoLevelListDemo()),
......
......@@ -19,13 +19,14 @@ export 'src/material/chip.dart';
export 'src/material/circle_avatar.dart';
export 'src/material/colors.dart';
export 'src/material/constants.dart';
export 'src/material/data_table.dart';
export 'src/material/date_picker.dart';
export 'src/material/date_picker_dialog.dart';
export 'src/material/dialog.dart';
export 'src/material/divider.dart';
export 'src/material/drawer.dart';
export 'src/material/drawer_header.dart';
export 'src/material/drawer_item.dart';
export 'src/material/divider.dart';
export 'src/material/drop_down.dart';
export 'src/material/flat_button.dart';
export 'src/material/flexible_space_bar.dart';
......@@ -33,10 +34,10 @@ export 'src/material/floating_action_button.dart';
export 'src/material/grid_tile.dart';
export 'src/material/grid_tile_bar.dart';
export 'src/material/icon.dart';
export 'src/material/icons.dart';
export 'src/material/icon_button.dart';
export 'src/material/icon_theme.dart';
export 'src/material/icon_theme_data.dart';
export 'src/material/icons.dart';
export 'src/material/ink_well.dart';
export 'src/material/input.dart';
export 'src/material/list.dart';
......
......@@ -64,6 +64,9 @@ class Checkbox extends StatelessWidget {
/// If null, the checkbox will be displayed as disabled.
final ValueChanged<bool> onChanged;
/// The width of a checkbox widget.
static const double width = 18.0;
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
......@@ -114,10 +117,9 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
}
const double _kMidpoint = 0.5;
const double _kEdgeSize = 18.0;
const double _kEdgeSize = Checkbox.width;
const double _kEdgeRadius = 1.0;
const double _kStrokeWidth = 2.0;
const double _kOffset = kRadialReactionRadius - _kEdgeSize / 2.0;
class _RenderCheckbox extends RenderToggleable {
_RenderCheckbox({
......@@ -135,11 +137,13 @@ class _RenderCheckbox extends RenderToggleable {
@override
void paint(PaintingContext context, Offset offset) {
final Canvas canvas = context.canvas;
final double offsetX = _kOffset + offset.dx;
final double offsetY = _kOffset + offset.dy;
paintRadialReaction(canvas, offset, const Point(kRadialReactionRadius, kRadialReactionRadius));
final double offsetX = offset.dx + (size.width - _kEdgeSize) / 2.0;
final double offsetY = offset.dy + (size.height - _kEdgeSize) / 2.0;
paintRadialReaction(canvas, offset, size.center(Point.origin));
double t = position.value;
......
......@@ -12,6 +12,7 @@ class Colors {
/// Completely invisible.
static const Color transparent = const Color(0x00000000);
/// Completely opaque black.
static const Color black = const Color(0xFF000000);
......@@ -21,6 +22,11 @@ class Colors {
/// Black with 54% opacity.
static const Color black54 = const Color(0x8A000000);
/// Black with 38% opacity.
///
/// Used for the placeholder text in data tables in light themes.
static const Color black38 = const Color(0x61000000);
/// Black with 45% opacity.
///
/// Used for modal barriers.
......@@ -28,14 +34,15 @@ class Colors {
/// Black with 26% opacity.
///
/// Used for disabled radio buttons and text of disabled flat buttons in the light theme.
/// Used for disabled radio buttons and the text of disabled flat buttons in light themes.
static const Color black26 = const Color(0x42000000);
/// Black with 12% opacity.
///
/// Used for the background of disabled raised buttons in the light theme.
/// Used for the background of disabled raised buttons in light themes.
static const Color black12 = const Color(0x1F000000);
/// Completely opaque white.
static const Color white = const Color(0xFFFFFFFF);
......@@ -44,17 +51,18 @@ class Colors {
/// White with 32% opacity.
///
/// Used for disabled radio buttons and text of disabled flat buttons in the dark theme.
/// Used for disabled radio buttons and the text of disabled flat buttons in dark themes.
static const Color white30 = const Color(0x4DFFFFFF);
/// White with 12% opacity.
///
/// Used for the background of disabled raised buttons in the dark theme.
/// Used for the background of disabled raised buttons in dark themes.
static const Color white12 = const Color(0x1FFFFFFF);
/// White with 10% opacity.
static const Color white10 = const Color(0x1AFFFFFF);
/// The red primary swatch.
static const Map<int, Color> red = const <int, Color>{
50: const Color(0xFFFFEBEE),
......
// Copyright 2016 The Chromium Authors. All rights reserved.
// 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/rendering.dart';
import 'package:flutter/widgets.dart';
import 'checkbox.dart';
import 'colors.dart';
import 'debug.dart';
import 'drop_down.dart';
import 'icon.dart';
import 'icon_theme.dart';
import 'icon_theme_data.dart';
import 'icons.dart';
import 'ink_well.dart';
import 'material.dart';
import 'theme.dart';
import 'theme_data.dart';
import 'tooltip.dart';
typedef void DataColumnSortCallback(int columnIndex, bool ascending);
typedef void DataCellEditCallback(Rect cell);
/// Column configuration for a [DataTable].
///
/// One column configuration must be provided for each column to
/// display in the table. The list of [DataColumn] objects is passed
/// as the `columns` argument to the [new DataTable] constructor.
class DataColumn {
/// Creates the configuration for a column of a [DataTable].
///
/// The [label] argument must not be null.
const DataColumn({
this.label,
this.tooltip,
this.numeric: false,
this.onSort
});
/// The column heading.
///
/// Typically, this will be a [Text] widget. It could also be an
/// [Icon] (typically using size 18), or a [Row] with an icon and
/// some text.
///
/// The label should not include the sort indicator.
final Widget label;
/// The column heading's tooltip.
///
/// This is a longer description of the column heading, for cases
/// where the heading might have been abbreviated to keep the column
/// width to a reasonable size.
final String tooltip;
/// Whether this column represents numeric data or not.
///
/// The contents of cells of columns containing numeric data are
/// right-aligned.
final bool numeric;
/// Invoked when the user asks to sort the table using this column.
///
/// If null, the column will not be considered sortable.
///
/// See [DataTable.sortColumnIndex] and [DataTable.sortAscending].
final DataColumnSortCallback onSort;
bool get _debugInteractive => onSort != null;
}
/// Row configuration and cell data for a [DataTable].
///
/// One row configuration must be provided for each row to
/// display in the table. The list of [DataRow] objects is passed
/// as the `rows` argument to the [new DataTable] constructor.
///
/// The data for this row of the table is provided in the [cells]
/// property of the [DataRow] object.
class DataRow {
/// Creates the configuration for a row of a [DataTable].
///
/// The [cells] argument must not be null.
const DataRow({
this.key,
this.selected: false,
this.onSelectChanged,
this.cells
});
/// A [Key] that uniquely identifies this row. This is used to
/// ensure that if a row is added or removed, any stateful widgets
/// related to this row (e.g. an in-progress checkbox animation)
/// remain on the right row visually.
///
/// If the table never changes once created, no key is necessary.
final LocalKey key;
/// Invoked when the user selects or unselects a selectable row.
///
/// If this is not null, then the row is selectable. The current
/// selection state of the row is given by [selected].
///
/// If any row is selectable, then the table's heading row will have
/// a checkbox that can be checked to select all selectable rows
/// (and which is checked if all the rows are selected), and each
/// subsequent row will have a checkbox to toggle just that row.
///
/// A row whose [onSelectChanged] callback is null is ignored for
/// the purposes of determining the state of the "all" checkbox,
/// and its checkbox is disabled.
final ValueChanged<bool> onSelectChanged;
/// Whether the row is selected.
///
/// If [onSelectChanged] is non-null for any row in the table, then
/// a checkbox is shown at the start of each row. If the row is
/// selected (true), the checkbox will be checked and the row will
/// be highlighted.
///
/// Otherwise, the checkbox, if present, will not be checked.
final bool selected;
/// The data for this row.
///
/// There must be exactly as many cells as there are columns in the
/// table.
final List<DataCell> cells;
bool get _debugInteractive => onSelectChanged != null || cells.any((DataCell cell) => cell._debugInteractive);
}
/// The data for a cell of a [DataTable].
///
/// One list of [DataCell] objects must be provided for each [DataRow]
/// in the [DataTable], in the [new DataRow] constructor's `cells`
/// argument.
class DataCell {
/// Creates an object to hold the data for a cell in a [DataTable].
///
/// The first argument is the widget to show for the cell, typically
/// a [Text] or [DropDownButton] widget; this becomes the [widget]
/// property and must not be null.
///
/// If the cell has no data, then a [Text] widget with placeholder
/// text should be provided instead, and then the [placeholder]
/// argument should be set to true.
const DataCell(this.widget, {
this.placeholder: false,
this.showEditIcon: false,
this.onTap
});
/// The data for the row.
///
/// Typically a [Text] widget or a [DropDownButton] widget.
///
/// If the cell has no data, then a [Text] widget with placeholder
/// text should be provided instead, and [placeholder] should be set
/// to true.
final Widget widget;
/// Whether the [widget] is actually a placeholder.
///
/// If this is true, the default text style for the cell is changed
/// to be appropriate for placeholder text.
final bool placeholder;
/// Whether to show an edit icon at the end of the cell.
///
/// This does not make the cell actually editable; the caller must
/// implement editing behavior if desired (initiated from the
/// [onTap] callback).
///
/// If this is set, [onTap] should also be set, otherwise tapping
/// the icon will have no effect.
final bool showEditIcon;
/// Invoked if the cell is tapped.
///
/// If non-null, tapping the cell will invoke this callback. If
/// null, tapping the cell will attempt to select the row (if
/// [TableRow.onSelectChanged] is provided).
final VoidCallback onTap;
bool get _debugInteractive => onTap != null;
}
/// A material design data table.
///
/// Displaying data in a table is expensive, because to lay out the
/// table all the data must be measured twice, once to negotiate the
/// dimensions to use for each column, and once to actually lay out
/// the table given the results of the negotiation.
///
// /// For this reason, if you have a lot of data (say, more than a dozen
// /// rows with a dozen columns, though the precise limits depend on the
// /// target device), it is suggested that you use a [DataCard] which
// /// automatically splits the data into multiple pages.
// ///
/// See also:
///
/// * [DataColumn]
/// * [DataRow]
/// * [DataCell]
/// * <https://www.google.com/design/spec/components/data-tables.html>
class DataTable extends StatelessWidget {
/// Creates a widget describing a data table.
///
/// 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.
///
/// The [rows] argument must be a list of as many [DataRow] objects
/// as the table is to have rows, ignoring the leading heading row
/// that contains the column headings (derived from the [columns]
/// argument). There may be zero rows, but the rows argument must
/// not be null.
///
/// Each [DataRow] object in [rows] must have as many [DataCell]
/// objects in the [DataRow.cells] list as the table has columns.
///
/// If the table is sorted, the column that provides the current
/// primary key should be specified by index in [sortColumnIndex], 0
/// meaning the first column in [columns], 1 being the next one, and
/// so forth.
///
/// The actual sort order can be specified using [sortAscending]; if
/// the sort order is ascending, this should be true (the default),
/// otherwise it should be false.
DataTable({
Key key,
List<DataColumn> columns,
this.sortColumnIndex,
this.sortAscending: true,
this.rows
}) : columns = columns,
_onlyTextColumn = _initOnlyTextColumn(columns), super(key: key) {
assert(columns != null);
assert(columns.length > 0);
assert(sortAscending != null);
assert(rows != null);
assert(!rows.any((DataRow row) => row.cells.length != columns.length));
assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length));
}
/// The configuration and labels for the columns in the table.
final List<DataColumn> columns;
/// The current primary sort key's column.
///
/// If non-null, indicates that the indicated column is the column
/// by which the data is sorted. The number must correspond to the
/// index of the relevant column in [columns].
///
/// Setting this will cause the relevant column to have a sort
/// indicator displayed.
///
/// When this is null, it implies that the table's sort order does
/// not correspond to any of the columns.
final int sortColumnIndex;
/// Whether the column mentioned in [sortColumnIndex], if any, is sorted
/// in ascending order.
///
/// If true, the order is ascending (meaning the rows with the
/// smallest values for the current sort column are first in the
/// table).
///
/// If false, the order is descending (meaning the rows with the
/// smallest values for the current sort column are last in the
/// table).
final bool sortAscending;
/// The data to show in each row (excluding the row that contains
/// the column headings). Must be non-null, but may be empty.
final List<DataRow> rows;
// Set by the constructor to the index of the only Column that is
// non-numeric, if there is exactly one, otherwise null.
final int _onlyTextColumn;
static int _initOnlyTextColumn(List<DataColumn> columns) {
int result;
for (int index = 0; index < columns.length; index += 1) {
DataColumn column = columns[index];
if (!column.numeric) {
if (result != null)
return null;
result = index;
}
}
return result;
}
bool get _debugInteractive {
return columns.any((DataColumn column) => column._debugInteractive)
|| rows.any((DataRow row) => row._debugInteractive);
}
static final LocalKey _headingRowKey = new UniqueKey();
void _handleSelectAll(bool checked) {
for (DataRow row in rows) {
if ((row.onSelectChanged != null) && (row.selected != checked))
row.onSelectChanged(checked);
}
}
static const double _kHeadingRowHeight = 56.0;
static const double _kDataRowHeight = 48.0;
static const double _kTablePadding = 24.0;
static const double _kColumnSpacing = 56.0;
static const double _kSortArrowPadding = 2.0;
static const double _kHeadingFontSize = 12.0;
static const Duration _kSortArrowAnimationDuration = const Duration(milliseconds: 150);
static const Color _kGrey100Opacity = const Color(0x0A000000); // Grey 100 as opacity instead of solid color
Widget _buildCheckbox({
Color color,
bool checked,
VoidCallback onRowTap,
ValueChanged<bool> onCheckboxChanged
}) {
Widget contents = new Padding(
padding: const EdgeInsets.fromLTRB(_kTablePadding, 0.0, _kTablePadding / 2.0, 0.0),
child: new Center(
child: new Checkbox(
activeColor: color,
value: checked,
onChanged: onCheckboxChanged
)
)
);
if (onRowTap != null) {
contents = new RowInkWell(
onTap: onRowTap,
child: contents
);
}
return new TableCell(
verticalAlignment: TableCellVerticalAlignment.fill,
child: contents
);
}
Widget _buildHeadingCell({
EdgeInsets padding,
Widget label,
String tooltip,
bool numeric,
VoidCallback onSort,
bool sorted,
bool ascending
}) {
if (onSort != null) {
final Widget arrow = new _SortArrow(
visible: sorted,
down: sorted ? ascending : null,
duration: _kSortArrowAnimationDuration
);
final Widget arrowPadding = new SizedBox(width: _kSortArrowPadding);
label = new Row(
children: numeric ? <Widget>[ arrow, arrowPadding, label ]
: <Widget>[ label, arrowPadding, arrow ]
);
}
label = new Container(
padding: padding,
height: _kHeadingRowHeight,
child: new Align(
alignment: new FractionalOffset(numeric ? 1.0 : 0.0, 0.5), // TODO(ianh): RTL for non-numeric
child: new AnimatedDefaultTextStyle(
style: new TextStyle(
// TODO(ianh): font family should be Roboto; see https://github.com/flutter/flutter/issues/3116
fontWeight: FontWeight.w500,
fontSize: _kHeadingFontSize,
color: onSort != null && sorted ? Colors.black87 : Colors.black54,
height: _kHeadingRowHeight / _kHeadingFontSize
),
duration: _kSortArrowAnimationDuration,
child: label
)
)
);
if (tooltip != null) {
label = new Tooltip(
message: tooltip,
child: label
);
}
if (onSort != null) {
label = new InkWell(
onTap: onSort,
// TODO(ianh): When we do RTL, we need to use 'end' ordering for the non-numeric case
child: label
);
}
return label;
}
Widget _buildDataCell({
EdgeInsets padding,
Widget label,
bool numeric,
bool placeholder,
bool showEditIcon,
VoidCallback onTap,
VoidCallback onSelectChanged
}) {
if (showEditIcon) {
final Widget icon = new Icon(icon: Icons.edit, size: 18.0);
label = new Flexible(child: label);
label = new Row(children: numeric ? <Widget>[ icon, label ] : <Widget>[ label, icon ]);
}
label = new Container(
padding: padding,
height: _kDataRowHeight,
child: new Align(
alignment: new FractionalOffset(numeric ? 1.0 : 0.0, 0.5), // TODO(ianh): RTL for non-numeric
child: new DefaultTextStyle(
style: new TextStyle(
// TODO(ianh): font family should be Roboto; see https://github.com/flutter/flutter/issues/3116
fontSize: 13.0,
color: placeholder ? Colors.black38 : Colors.black87 // TODO(ianh): defer to theme, since this won't work in e.g. the dark theme
),
child: new IconTheme(
data: new IconThemeData(
color: Colors.black54
),
child: new DropDownButtonHideUnderline(child: label)
)
)
)
);
if (onTap != null) {
label = new InkWell(
onTap: onTap,
child: label
);
} else if (onSelectChanged != null) {
label = new RowInkWell(
onTap: onSelectChanged,
child: label
);
}
return label;
}
@override
Widget build(BuildContext context) {
assert(!_debugInteractive || debugCheckHasMaterial(context));
final ThemeData theme = Theme.of(context);
final BoxDecoration _kSelectedDecoration = new BoxDecoration(
backgroundColor: _kGrey100Opacity, // has to be transparent so you can see the ink on the material
border: new Border(bottom: new BorderSide(color: theme.dividerColor))
);
final BoxDecoration _kUnselectedDecoration = new BoxDecoration(
border: new Border(bottom: new BorderSide(color: theme.dividerColor))
);
final bool showCheckboxColumn = rows.any((DataRow row) => row.onSelectChanged != null);
final bool allChecked = showCheckboxColumn && !rows.any((DataRow row) => row.onSelectChanged != null && !row.selected);
List<TableColumnWidth> tableColumns = new List<TableColumnWidth>(columns.length + (showCheckboxColumn ? 1 : 0));
List<TableRow> tableRows = new List<TableRow>.generate(
rows.length + 1, // the +1 is for the header row
(int index) {
return new TableRow(
key: index == 0 ? _headingRowKey : rows[index - 1].key,
decoration: index > 0 && rows[index - 1].selected ? _kSelectedDecoration
: _kUnselectedDecoration,
children: new List<Widget>(tableColumns.length)
);
}
);
int rowIndex;
int displayColumnIndex = 0;
if (showCheckboxColumn) {
tableColumns[0] = new FixedColumnWidth(_kTablePadding + Checkbox.width + _kTablePadding / 2.0);
tableRows[0].children[0] = _buildCheckbox(
color: theme.accentColor,
checked: allChecked,
onCheckboxChanged: _handleSelectAll
);
rowIndex = 1;
for (DataRow row in rows) {
tableRows[rowIndex].children[0] = _buildCheckbox(
color: theme.accentColor,
checked: row.selected,
onRowTap: () => row.onSelectChanged(!row.selected),
onCheckboxChanged: row.onSelectChanged
);
rowIndex += 1;
}
displayColumnIndex += 1;
}
for (int dataColumnIndex = 0; dataColumnIndex < columns.length; dataColumnIndex += 1) {
DataColumn column = columns[dataColumnIndex];
final EdgeInsets padding = new EdgeInsets.fromLTRB(
dataColumnIndex == 0 ? showCheckboxColumn ? _kTablePadding / 2.0 : _kTablePadding : _kColumnSpacing / 2.0,
0.0,
dataColumnIndex == columns.length - 1 ? _kTablePadding : _kColumnSpacing / 2.0,
0.0
);
if (dataColumnIndex == _onlyTextColumn) {
tableColumns[displayColumnIndex] = const IntrinsicColumnWidth(flex: 1.0);
} else {
tableColumns[displayColumnIndex] = const IntrinsicColumnWidth();
}
tableRows[0].children[displayColumnIndex] = _buildHeadingCell(
padding: padding,
label: column.label,
tooltip: column.tooltip,
numeric: column.numeric,
onSort: () => column.onSort(dataColumnIndex, sortColumnIndex == dataColumnIndex ? !sortAscending : true),
sorted: dataColumnIndex == sortColumnIndex,
ascending: sortAscending
);
rowIndex = 1;
for (DataRow row in rows) {
DataCell cell = row.cells[dataColumnIndex];
tableRows[rowIndex].children[displayColumnIndex] = _buildDataCell(
padding: padding,
label: cell.widget,
numeric: column.numeric,
placeholder: cell.placeholder,
showEditIcon: cell.showEditIcon,
onTap: cell.onTap,
onSelectChanged: () => row.onSelectChanged(!row.selected)
);
rowIndex += 1;
}
displayColumnIndex += 1;
}
return new Table(
columnWidths: tableColumns.asMap(),
children: tableRows
);
}
}
/// A rectangular area of a Material that responds to touch but clips
/// its ink splashes to the current table row of the nearest table.
///
/// Must have an ancestor [Material] widget in which to cause ink
/// reactions and an ancestor [Table] widget to establish a row.
///
/// The RowInkWell must be in the same coordinate space (modulo
/// translations) as the [Table]. If it's rotated or scaled or
/// otherwise transformed, it will not be able to describe the
/// rectangle of the row in its own coordinate system as a [Rect], and
/// thus the splash will not occur. (In general, this is easy to
/// achieve: just put the RowInkWell as the direct child of the
/// [Table], and put the other contents of the cell inside it.)
class RowInkWell extends InkResponse {
RowInkWell({
Key key,
Widget child,
GestureTapCallback onTap,
GestureTapCallback onDoubleTap,
GestureLongPressCallback onLongPress,
ValueChanged<bool> onHighlightChanged
}) : super(
key: key,
child: child,
onTap: onTap,
onDoubleTap: onDoubleTap,
onLongPress: onLongPress,
onHighlightChanged: onHighlightChanged,
containedInkWell: true,
highlightShape: BoxShape.rectangle
);
@override
RectCallback getRectCallback(RenderBox referenceBox) {
return () {
RenderObject cell = referenceBox;
AbstractNode table = cell.parent;
Matrix4 transform = new Matrix4.identity();
while (table is RenderObject && table is! RenderTable) {
RenderTable parentBox = table;
parentBox.applyPaintTransform(cell, transform);
assert(table == cell.parent);
cell = table;
table = table.parent;
}
if (table is RenderTable) {
TableCellParentData cellParentData = cell.parentData;
assert(cellParentData.y != null);
Rect rect = table.getRowBox(cellParentData.y);
// The rect is in the table's coordinate space. We need to change it to the
// RowInkWell's coordinate space.
table.applyPaintTransform(cell, transform);
Offset offset = MatrixUtils.getAsTranslation(transform);
if (offset != null)
return rect.shift(-offset);
}
return Rect.zero;
};
}
@override
bool debugCheckContext(BuildContext context) {
assert(debugCheckHasTable(context));
return super.debugCheckContext(context);
}
}
class _SortArrow extends StatefulWidget {
_SortArrow({
Key key,
this.visible,
this.down,
this.duration
}) : super(key: key);
final bool visible;
final bool down;
final Duration duration;
@override
_SortArrowState createState() => new _SortArrowState();
}
class _SortArrowState extends State<_SortArrow> {
AnimationController _opacityController;
Animation<double> _opacityAnimation;
AnimationController _orientationController;
Animation<double> _orientationAnimation;
double _orientationOffset = 0.0;
bool _down;
@override
void initState() {
super.initState();
_opacityAnimation = new CurvedAnimation(
parent: _opacityController = new AnimationController(
duration: config.duration
),
curve: Curves.ease
)
..addListener(_rebuild);
_opacityController.value = config.visible ? 1.0 : 0.0;
_orientationAnimation = new Tween<double>(
begin: 0.0,
end: math.PI
).animate(new CurvedAnimation(
parent: _orientationController = new AnimationController(
duration: config.duration
),
curve: Curves.easeIn
))
..addListener(_rebuild)
..addStatusListener(_resetOrientationAnimation);
if (config.visible)
_orientationOffset = config.down ? 0.0 : math.PI;
}
void _rebuild() {
setState(() {
// The animations changed, so we need to rebuild.
});
}
void _resetOrientationAnimation(AnimationStatus status) {
if (status == AnimationStatus.completed) {
assert(_orientationAnimation.value == math.PI);
_orientationOffset += math.PI;
_orientationController.value = 0.0; // TODO(ianh): This triggers a pointless rebuild.
}
}
@override
void didUpdateConfig(_SortArrow oldConfig) {
super.didUpdateConfig(oldConfig);
bool skipArrow = false;
bool newDown = config.down != null ? config.down : _down;
if (oldConfig.visible != config.visible) {
if (config.visible && (_opacityController.status == AnimationStatus.dismissed)) {
_orientationController.stop();
_orientationController.value = 0.0;
_orientationOffset = newDown ? 0.0 : math.PI;
skipArrow = true;
}
if (config.visible) {
_opacityController.forward();
} else {
_opacityController.reverse();
}
}
if ((_down != newDown) && !skipArrow) {
if (_orientationController.status == AnimationStatus.dismissed) {
_orientationController.forward();
} else {
_orientationController.reverse();
}
}
_down = newDown;
}
@override
void dispose() {
_opacityController.dispose();
_orientationController.dispose();
super.dispose();
}
static const double _kArrowIconBaselineOffset = -1.5;
static const double _kArrowIconSize = 16.0;
@override
Widget build(BuildContext context) {
return new Opacity(
opacity: _opacityAnimation.value,
child: new Transform(
transform: new Matrix4.rotationZ(_orientationOffset + _orientationAnimation.value)
..setTranslationRaw(0.0, _kArrowIconBaselineOffset, 0.0),
alignment: FractionalOffset.center,
child: new Icon(
icon: Icons.arrow_downward,
size: _kArrowIconSize,
color: Colors.black87
)
)
);
}
}
/*
TODO(ianh): implement DataTableCard
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
*/
......@@ -7,9 +7,19 @@ import 'package:flutter/widgets.dart';
import 'material.dart';
import 'scaffold.dart';
/// Throws an exception of the given build context is not contained in a [Material] widget.
/// Asserts that the given context has a [Material] ancestor.
///
/// Does nothing if asserts are disabled.
/// Used by many material design widgets to make sure that they are
/// only used in contexts where they can print ink onto some material.
///
/// To invoke this function, use the following pattern, typically in the
/// relevant Widget's [build] method:
///
/// ```dart
/// assert(debugCheckHasMaterial(context));
/// ```
///
/// Does nothing if asserts are disabled. Always returns true.
bool debugCheckHasMaterial(BuildContext context) {
assert(() {
if (context.widget is! Material && context.ancestorWidgetOfExactType(Material) == null) {
......@@ -33,9 +43,22 @@ bool debugCheckHasMaterial(BuildContext context) {
return true;
}
/// Throws an exception of the given build context is not contained in a [Scaffold] widget.
/// Asserts that the given context has a [Scaffold] ancestor.
///
/// Used by some material design widgets to make sure that they are
/// only used in contexts where they can communicate with a Scaffold.
///
/// For example, the [AppBar] in some situations requires a Scaffold
/// to do the right thing with scrolling.
///
/// To invoke this function, use the following pattern, typically in the
/// relevant Widget's [build] method:
///
/// ```dart
/// assert(debugCheckHasScaffold(context));
/// ```
///
/// Does nothing if asserts are disabled.
/// Does nothing if asserts are disabled. Always returns true.
bool debugCheckHasScaffold(BuildContext context) {
assert(() {
if (Scaffold.of(context) == null) {
......
......@@ -15,11 +15,13 @@ 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);
const double _kBaselineOffsetFromBottom = 20.0;
const Border _kDropDownUnderline = const Border(bottom: const BorderSide(color: const Color(0xFFBDBDBD), width: 2.0));
const double _kBottomBorderHeight = 2.0;
const Border _kDropDownUnderline = const Border(bottom: const BorderSide(color: const Color(0xFFBDBDBD), width: _kBottomBorderHeight));
class _DropDownMenuPainter extends CustomPainter {
const _DropDownMenuPainter({
......@@ -235,7 +237,7 @@ class DropDownMenuItem<T> extends StatelessWidget {
Widget build(BuildContext context) {
return new Container(
height: _kMenuItemHeight,
padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 6.0),
padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: _kTopMargin),
child: new DefaultTextStyle(
style: Theme.of(context).textTheme.subhead,
child: new Baseline(
......@@ -247,6 +249,32 @@ class DropDownMenuItem<T> extends StatelessWidget {
}
}
/// An inherited widget that causes any descendant [DropDownButton]
/// widgets to not include their regular underline.
///
/// This is used by [DataTable] to remove the underline from any
/// [DropDownButton] widgets placed within material data tables, as
/// required by the material design specification.
class DropDownButtonHideUnderline extends InheritedWidget {
/// Creates a [DropDownButtonHideUnderline]. A non-null [child] must
/// be given.
DropDownButtonHideUnderline({
Key key,
Widget child
}) : super(key: key, child: child) {
assert(child != null);
}
/// Returns whether the underline of [DropDownButton] widgets should
/// be hidden.
static bool at(BuildContext context) {
return context.inheritFromWidgetOfExactType(DropDownButtonHideUnderline) != null;
}
@override
bool updateShouldNotify(DropDownButtonHideUnderline old) => false;
}
/// A material design button for selecting from a list of items.
///
/// A dropdown button lets the user select from a number of items. The button
......@@ -336,26 +364,35 @@ class _DropDownButtonState<T> extends State<DropDownButton<T>> {
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
Widget result = new Row(
children: <Widget>[
new IndexedStack(
children: config.items,
key: indexedStackKey,
index: _selectedIndex,
alignment: FractionalOffset.topCenter
),
new Container(
child: new Icon(icon: Icons.arrow_drop_down, size: 36.0),
padding: const EdgeInsets.only(top: _kTopMargin)
)
],
mainAxisAlignment: MainAxisAlignment.collapse
);
if (DropDownButtonHideUnderline.at(context)) {
result = new Padding(
padding: const EdgeInsets.only(bottom: _kBottomBorderHeight),
child: result
);
} else {
result = new Container(
decoration: const BoxDecoration(border: _kDropDownUnderline),
child: result
);
}
return new GestureDetector(
onTap: _handleTap,
child: new Container(
decoration: new BoxDecoration(border: _kDropDownUnderline),
child: new Row(
children: <Widget>[
new IndexedStack(
children: config.items,
key: indexedStackKey,
index: _selectedIndex,
alignment: FractionalOffset.topCenter
),
new Container(
child: new Icon(icon: Icons.arrow_drop_down, size: 36.0),
padding: const EdgeInsets.only(top: 6.0)
)
],
mainAxisAlignment: MainAxisAlignment.collapse
)
)
child: result
);
}
}
......@@ -46,7 +46,7 @@ class Icon extends StatelessWidget {
/// Icons occupy a square with width and height equal to size.
final double size;
/// The icon to display.
/// The icon to display. The available icons are described in [Icons].
final IconData icon;
/// The color to use when drawing the icon.
......
......@@ -64,6 +64,31 @@ class InkResponse extends StatefulWidget {
/// The shape (e.g., circle, rectangle) to use for the highlight drawn around this part of the material.
final BoxShape highlightShape;
/// The rectangle to use for the highlight effect and for clipping
/// the splash effects if [containedInkWell] is true.
///
/// This method is intended to be overridden by descendants that
/// specialize [InkResponse] for unusual cases. For example,
/// [RowInkWell] implements this method to return the rectangle
/// corresponding to the row that the widget is in.
///
/// The default behavior returns null, which is equivalent to
/// returning the referenceBox argument's bounding box (though
/// slightly more efficient).
RectCallback getRectCallback(RenderBox referenceBox) => null;
/// Asserts that the given context satisfies the prerequisites for
/// this class.
///
/// This method is intended to be overridden by descendants that
/// specialize [InkResponse] for unusual cases. For example,
/// [RowInkWell] implements this method to verify that the widget is
/// in a table.
bool debugCheckContext(BuildContext context) {
assert(debugCheckHasMaterial(context));
return true;
}
@override
_InkResponseState<InkResponse> createState() => new _InkResponseState<InkResponse>();
}
......@@ -85,6 +110,7 @@ class _InkResponseState<T extends InkResponse> extends State<T> {
referenceBox: referenceBox,
color: Theme.of(context).highlightColor,
shape: config.highlightShape,
rectCallback: config.getRectCallback(referenceBox),
onRemoved: () {
assert(_lastHighlight != null);
_lastHighlight = null;
......@@ -105,11 +131,13 @@ class _InkResponseState<T extends InkResponse> extends State<T> {
RenderBox referenceBox = context.findRenderObject();
assert(Material.of(context) != null);
InkSplash splash;
RectCallback rectCallback = config.getRectCallback(referenceBox);
splash = Material.of(context).splashAt(
referenceBox: referenceBox,
position: referenceBox.globalToLocal(position),
color: Theme.of(context).splashColor,
containedInWell: config.containedInkWell,
containedInkWell: config.containedInkWell,
rectCallback: config.containedInkWell ? rectCallback : null,
onRemoved: () {
if (_splashes != null) {
assert(_splashes.contains(splash));
......@@ -176,7 +204,7 @@ class _InkResponseState<T extends InkResponse> extends State<T> {
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
assert(config.debugCheckContext(context));
final bool enabled = config.onTap != null || config.onDoubleTap != null || config.onLongPress != null;
return new GestureDetector(
onTapDown: enabled ? _handleTapDown : null,
......
......@@ -12,10 +12,15 @@ import 'constants.dart';
import 'shadows.dart';
import 'theme.dart';
/// The various kinds of material in material design.
/// Signature for callback used by ink effects to obtain the rectangle for the effect.
typedef Rect RectCallback();
/// The various kinds of material in material design. Used to
/// configure the default behavior of [Material] widgets.
///
/// See also:
/// * [Material]
///
/// * [Material], in particular [Material.type]
/// * [kMaterialEdges]
enum MaterialType {
/// Infinite extent using default theme canvas color.
......@@ -27,7 +32,7 @@ enum MaterialType {
/// A circle, no color by default (used for floating action buttons).
circle,
/// Rounded edges, no color by default (used for MaterialButton buttons).
/// Rounded edges, no color by default (used for [MaterialButton] buttons).
button,
/// A transparent piece of material that draws ink splashes and highlights.
......@@ -95,13 +100,36 @@ abstract class MaterialInkController {
Color get color;
/// Begin a splash, centered at position relative to referenceBox.
/// If containedInWell is true, then the splash will be sized to fit
/// the referenceBox, then clipped to it when drawn.
///
/// If containedInkWell is true, then the splash will be sized to fit
/// the well rectangle, then clipped to it when drawn. The well
/// rectangle is the box returned by rectCallback, if provided, or
/// otherwise is the bounds of the referenceBox.
///
/// If containedInkWell is false, then rectCallback should be null.
/// The ink splash is clipped only to the edges of the [Material].
/// This is the default.
///
/// When the splash is removed, onRemoved will be invoked.
InkSplash splashAt({ RenderBox referenceBox, Point position, Color color, bool containedInWell, VoidCallback onRemoved });
InkSplash splashAt({
RenderBox referenceBox,
Point position,
Color color,
bool containedInkWell: false,
RectCallback rectCallback,
VoidCallback onRemoved
});
/// Begin a highlight, coincident with the referenceBox.
InkHighlight highlightAt({ RenderBox referenceBox, Color color, BoxShape shape: BoxShape.rectangle, VoidCallback onRemoved });
/// Begin a highlight animation. If a rectCallback is given, then it
/// provides the highlight rectangle, otherwise, the highlight
/// rectangle is coincident with the referenceBox.
InkHighlight highlightAt({
RenderBox referenceBox,
Color color,
BoxShape shape: BoxShape.rectangle,
RectCallback rectCallback,
VoidCallback onRemoved
});
/// Add an arbitrary InkFeature to this InkController.
void addInkFeature(InkFeature feature);
......@@ -147,22 +175,27 @@ class Material extends StatefulWidget {
/// The widget below this widget in the tree.
final Widget child;
/// The kind of material (e.g., card or canvas).
/// The kind of material to show (e.g., card or canvas). This
/// affects the shape of the widget, the roundness of its corners if
/// the shape is rectangular, and the default color.
final MaterialType type;
/// The z-coordinate at which to place this material.
final int elevation;
/// The color of the material.
/// The color to paint the material.
///
/// Must be opaque. To create a transparent piece of material, use
/// [MaterialType.transparency].
///
/// By default, the color is derived from the [type] of material.
final Color color;
/// The typographical style to use for text within this material.
final TextStyle textStyle;
/// The ink controller from the closest instance of this class that encloses the given context.
/// The ink controller from the closest instance of this class that
/// encloses the given context.
static MaterialInkController of(BuildContext context) {
final _RenderInkFeatures result = context.ancestorRenderObjectOfType(const TypeMatcher<_RenderInkFeatures>());
return result;
......@@ -273,13 +306,24 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
RenderBox referenceBox,
Point position,
Color color,
bool containedInWell,
bool containedInkWell: false,
RectCallback rectCallback,
VoidCallback onRemoved
}) {
double radius;
if (containedInWell) {
radius = _getSplashTargetSize(referenceBox.size, position);
RectCallback clipCallback;
if (containedInkWell) {
Size size;
if (rectCallback != null) {
size = rectCallback().size;
clipCallback = rectCallback;
} else {
size = referenceBox.size;
clipCallback = () => Point.origin & size;
}
radius = _getSplashTargetSize(size, position);
} else {
assert(rectCallback == null);
radius = _kDefaultSplashRadius;
}
_InkSplash splash = new _InkSplash(
......@@ -288,8 +332,8 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
position: position,
color: color,
targetRadius: radius,
clipToReferenceBox: containedInWell,
repositionToReferenceBox: !containedInWell,
clipCallback: clipCallback,
repositionToReferenceBox: !containedInkWell,
onRemoved: onRemoved
);
addInkFeature(splash);
......@@ -309,6 +353,7 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
RenderBox referenceBox,
Color color,
BoxShape shape: BoxShape.rectangle,
RectCallback rectCallback,
VoidCallback onRemoved
}) {
_InkHighlight highlight = new _InkHighlight(
......@@ -316,6 +361,7 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
referenceBox: referenceBox,
color: color,
shape: shape,
rectCallback: rectCallback,
onRemoved: onRemoved
);
addInkFeature(highlight);
......@@ -436,7 +482,7 @@ class _InkSplash extends InkFeature implements InkSplash {
this.position,
this.color,
this.targetRadius,
this.clipToReferenceBox,
this.clipCallback,
this.repositionToReferenceBox,
VoidCallback onRemoved
}) : super(renderer: renderer, referenceBox: referenceBox, onRemoved: onRemoved) {
......@@ -460,7 +506,7 @@ class _InkSplash extends InkFeature implements InkSplash {
final Point position;
final Color color;
final double targetRadius;
final bool clipToReferenceBox;
final RectCallback clipCallback;
final bool repositionToReferenceBox;
Animation<double> _radius;
......@@ -499,25 +545,23 @@ class _InkSplash extends InkFeature implements InkSplash {
void paintFeature(Canvas canvas, Matrix4 transform) {
Paint paint = new Paint()..color = color.withAlpha(_alpha.value);
Point center = position;
if (repositionToReferenceBox)
center = Point.lerp(center, referenceBox.size.center(Point.origin), _radiusController.value);
Offset originOffset = MatrixUtils.getAsTranslation(transform);
if (originOffset == null) {
canvas.save();
canvas.transform(transform.storage);
if (clipToReferenceBox)
canvas.clipRect(Point.origin & referenceBox.size);
if (repositionToReferenceBox)
center = Point.lerp(center, Point.origin, _radiusController.value);
if (clipCallback != null)
canvas.clipRect(clipCallback());
canvas.drawCircle(center, _radius.value, paint);
canvas.restore();
} else {
if (clipToReferenceBox) {
if (clipCallback != null) {
canvas.save();
canvas.clipRect(originOffset.toPoint() & referenceBox.size);
canvas.clipRect(clipCallback().shift(originOffset));
}
if (repositionToReferenceBox)
center = Point.lerp(center, referenceBox.size.center(Point.origin), _radiusController.value);
canvas.drawCircle(center + originOffset, _radius.value, paint);
if (clipToReferenceBox)
if (clipCallback != null)
canvas.restore();
}
}
......@@ -527,6 +571,7 @@ class _InkHighlight extends InkFeature implements InkHighlight {
_InkHighlight({
_RenderInkFeatures renderer,
RenderBox referenceBox,
this.rectCallback,
Color color,
this.shape,
VoidCallback onRemoved
......@@ -542,6 +587,8 @@ class _InkHighlight extends InkFeature implements InkHighlight {
).animate(_alphaController);
}
final RectCallback rectCallback;
@override
Color get color => _color;
Color _color;
......@@ -597,13 +644,14 @@ class _InkHighlight extends InkFeature implements InkHighlight {
void paintFeature(Canvas canvas, Matrix4 transform) {
Paint paint = new Paint()..color = color.withAlpha(_alpha.value);
Offset originOffset = MatrixUtils.getAsTranslation(transform);
final Rect rect = (rectCallback != null ? rectCallback() : Point.origin & referenceBox.size);
if (originOffset == null) {
canvas.save();
canvas.transform(transform.storage);
_paintHighlight(canvas, Point.origin & referenceBox.size, paint);
_paintHighlight(canvas, rect, paint);
canvas.restore();
} else {
_paintHighlight(canvas, originOffset.toPoint() & referenceBox.size, paint);
_paintHighlight(canvas, rect.shift(originOffset), paint);
}
}
......
......@@ -93,6 +93,32 @@ class RenderBlock extends RenderBox
);
return false;
});
assert(() {
switch (mainAxis) {
case Axis.horizontal:
if (!constraints.maxHeight.isInfinite)
return true;
break;
case Axis.vertical:
if (!constraints.maxWidth.isInfinite)
return true;
break;
}
// TODO(ianh): Detect if we're actually nested blocks and say something
// more specific to the exact situation in that case, and don't mention
// nesting blocks in the negative case.
throw new FlutterError(
'RenderBlock must have a bounded constraint for its cross axis.\n'
'RenderBlock forces its children to expand to fit the block\'s container, '
'so it must be placed in a parent that does constrain the block\'s cross '
'axis to a finite dimension. If you are attempting to nest a block with '
'one direction inside a block of another direction, you will want to '
'wrap the inner one inside a box that fixes the dimension in that direction, '
'for example, a RenderIntrinsicWidth or RenderIntrinsicHeight object. '
'This is relatively expensive, however.' // (that's why we don't do it automatically)
);
return false;
});
BoxConstraints innerConstraints = _getInnerConstraints(constraints);
double position = 0.0;
RenderBox child = firstChild;
......
......@@ -609,12 +609,12 @@ abstract class RenderBox extends RenderObject {
/// baseline, regardless of padding, font size differences, etc. If there is
/// no baseline, this function returns the distance from the y-coordinate of
/// the position of the box to the y-coordinate of the bottom of the box
/// (i.e., the height of the box) unless the the caller passes true
/// (i.e., the height of the box) unless the caller passes true
/// for `onlyReal`, in which case the function returns null.
///
/// Only call this function calling [layout] on this box. You are only
/// allowed to call this from the parent of this box during that parent's
/// [performLayout] or [paint] functions.
/// Only call this function after calling [layout] on this box. You
/// are only allowed to call this from the parent of this box during
/// that parent's [performLayout] or [paint] functions.
double getDistanceToBaseline(TextBaseline baseline, { bool onlyReal: false }) {
assert(!needsLayout);
assert(!_debugDoingBaseline);
......@@ -724,6 +724,10 @@ abstract class RenderBox extends RenderObject {
'as big as possible, but it was put inside another render object '
'that allows its children to pick their own size.\n'
'$information'
'The constraints that applied to the $runtimeType were:\n'
' $constraints\n'
'The exact size it was given was:\n'
' $_size\n'
'See https://flutter.io/layout/ for more information.'
);
}
......@@ -788,7 +792,7 @@ abstract class RenderBox extends RenderObject {
// if we have cached data, then someone must have used our data
assert(_ancestorUsesBaseline);
final RenderObject parent = this.parent;
parent.markNeedsLayout();
parent?.markNeedsLayout();
assert(parent == this.parent);
// Now that they're dirty, we can forget that they used the
// baseline. If they use it again, then we'll set the bit
......
......@@ -456,6 +456,7 @@ class RenderIntrinsicWidth extends RenderProxyBox {
if (child == null)
return constraints.constrainWidth(0.0);
double childResult = child.getMaxIntrinsicWidth(constraints);
assert(!childResult.isInfinite);
return constraints.constrainWidth(_applyStep(childResult, _stepWidth));
}
......@@ -465,6 +466,7 @@ class RenderIntrinsicWidth extends RenderProxyBox {
if (child == null)
return constraints.constrainHeight(0.0);
double childResult = child.getMinIntrinsicHeight(_getInnerConstraints(constraints));
assert(!childResult.isInfinite);
return constraints.constrainHeight(_applyStep(childResult, _stepHeight));
}
......@@ -474,6 +476,7 @@ class RenderIntrinsicWidth extends RenderProxyBox {
if (child == null)
return constraints.constrainHeight(0.0);
double childResult = child.getMaxIntrinsicHeight(_getInnerConstraints(constraints));
assert(!childResult.isInfinite);
return constraints.constrainHeight(_applyStep(childResult, _stepHeight));
}
......
......@@ -821,9 +821,16 @@ class RenderCustomSingleChildLayoutBox extends RenderShiftedBox {
}
}
/// Positions its child vertically according to the child's baseline.
/// Shifts the child down such that the child's baseline (or the
/// bottom of the child, if the child has no baseline) is [baseline]
/// logical pixels below the top of this box, then sizes this box to
/// contain the child. If [baseline] is less than the distance from
/// the top of the child to the baseline of the child, then the child
/// is top-aligned instead.
class RenderBaseline extends RenderShiftedBox {
/// Creates a [RenderBaseline] object.
///
/// The [baseline] and [baselineType] arguments are required.
RenderBaseline({
RenderBox child,
double baseline,
......@@ -862,10 +869,13 @@ class RenderBaseline extends RenderShiftedBox {
void performLayout() {
if (child != null) {
child.layout(constraints.loosen(), parentUsesSize: true);
size = constraints.constrain(child.size);
double delta = baseline - child.getDistanceToBaseline(baselineType);
final double childBaseline = child.getDistanceToBaseline(baselineType);
final double actualBaseline = math.max(baseline, childBaseline);
final double top = actualBaseline - childBaseline;
final BoxParentData childParentData = child.parentData;
childParentData.offset = new Offset(0.0, delta);
childParentData.offset = new Offset(0.0, top);
final Size childSize = child.size;
size = constraints.constrain(new Size(childSize.width, top + childSize.height));
} else {
performResize();
}
......
......@@ -19,17 +19,47 @@ class TableCellParentData extends BoxParentData {
int y;
@override
String toString() => '${super.toString()}; $verticalAlignment';
String toString() => '${super.toString()}; ${verticalAlignment == null ? "default vertical alignment" : "$verticalAlignment"}';
}
/// Base class to describe how wide a column in a [RenderTable] should be.
abstract class TableColumnWidth {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const TableColumnWidth();
/// The smallest width that the column can have.
///
/// The `cells` argument is an iterable that provides all the cells
/// in the table for this column. Walking the cells is by definition
/// O(N), so algorithms that do that should be considered expensive.
///
/// The `containerWidth` argument is the `maxWidth` of the incoming
/// constraints for the table, and might be infinite.
double minIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth);
/// The ideal width that the column should have. This must be equal
/// to or greater than the [minIntrinsicWidth]. The column might be
/// bigger than this width, e.g. if the column is flexible or if the
/// table's width ends up being forced to be bigger than the sum of
/// all the maxIntrinsicWidth values.
///
/// The `cells` argument is an iterable that provides all the cells
/// in the table for this column. Walking the cells is by definition
/// O(N), so algorithms that do that should be considered expensive.
///
/// The `containerWidth` argument is the `maxWidth` of the incoming
/// constraints for the table, and might be infinite.
double maxIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth);
/// The flex factor to apply to the cell if there is any room left
/// over when laying out the table. The remaining space is
/// distributed to any columns with flex in proportion to their flex
/// value (higher values get more space).
///
/// The `cells` argument is an iterable that provides all the cells
/// in the table for this column. Walking the cells is by definition
/// O(N), so algorithms that do that should be considered expensive.
double flex(Iterable<RenderBox> cells) => null;
@override
......@@ -40,8 +70,12 @@ abstract class TableColumnWidth {
/// cells in that column.
///
/// This is a very expensive way to size a column.
///
/// A flex value can be provided. If specified (and non-null), the
/// column will participate in the distribution of remaining space
/// once all the non-flexible columns have been sized.
class IntrinsicColumnWidth extends TableColumnWidth {
const IntrinsicColumnWidth();
const IntrinsicColumnWidth({ double flex }) : _flex = flex;
@override
double minIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) {
......@@ -58,6 +92,11 @@ class IntrinsicColumnWidth extends TableColumnWidth {
result = math.max(result, cell.getMaxIntrinsicWidth(const BoxConstraints()));
return result;
}
final double _flex;
@override
double flex(Iterable<RenderBox> cells) => _flex;
}
/// Sizes the column to a specific number of pixels.
......@@ -169,10 +208,13 @@ class MaxColumnWidth extends TableColumnWidth {
@override
double flex(Iterable<RenderBox> cells) {
double aFlex = a.flex(cells);
final double aFlex = a.flex(cells);
if (aFlex == null)
return b.flex(cells);
return math.max(aFlex, b.flex(cells));
final double bFlex = b.flex(cells);
if (bFlex == null)
return null;
return math.max(aFlex, bFlex);
}
@override
......@@ -215,7 +257,10 @@ class MinColumnWidth extends TableColumnWidth {
double aFlex = a.flex(cells);
if (aFlex == null)
return b.flex(cells);
return math.min(aFlex, b.flex(cells));
double bFlex = b.flex(cells);
if (bFlex == null)
return null;
return math.min(aFlex, bFlex);
}
@override
......@@ -535,26 +580,34 @@ class RenderTable extends RenderBox {
}
assert(cells != null);
assert(cells.length % columns == 0);
// remove cells that are moving away
// fill a set with the cells that are moving (it's important not
// to dropChild a child that's remaining with us, because that
// would clear their parentData field)
final Set<RenderBox> lostChildren = new HashSet<RenderBox>();
for (int y = 0; y < _rows; y += 1) {
for (int x = 0; x < _columns; x += 1) {
int xyOld = x + y * _columns;
int xyNew = x + y * columns;
if (_children[xyOld] != null && (x >= columns || xyNew >= cells.length || _children[xyOld] != cells[xyNew]))
dropChild(_children[xyOld]);
lostChildren.add(_children[xyOld]);
}
}
// adopt cells that are arriving
// adopt cells that are arriving, and cross cells that are just moving off our list of lostChildren
int y = 0;
while (y * columns < cells.length) {
for (int x = 0; x < columns; x += 1) {
int xyNew = x + y * columns;
int xyOld = x + y * _columns;
if (cells[xyNew] != null && (x >= _columns || y >= _rows || _children[xyOld] != cells[xyNew]))
adoptChild(cells[xyNew]);
if (cells[xyNew] != null && (x >= _columns || y >= _rows || _children[xyOld] != cells[xyNew])) {
if (!lostChildren.remove(cells[xyNew]))
adoptChild(cells[xyNew]);
}
}
y += 1;
}
// drop all the lost children
for (RenderBox oldChild in lostChildren)
dropChild(oldChild);
// update our internal values
_columns = columns;
_rows = cells.length ~/ columns;
......@@ -666,7 +719,7 @@ class RenderTable extends RenderBox {
// honorable mention, most likely to improve if taught about memoization award
assert(constraints.debugAssertIsValid());
assert(_children.length == rows * columns);
final List<double> widths = computeColumnWidths(constraints);
final List<double> widths = _computeColumnWidths(constraints);
double rowTop = 0.0;
for (int y = 0; y < rows; y += 1) {
double rowHeight = 0.0;
......@@ -694,6 +747,10 @@ class RenderTable extends RenderBox {
return _baselineDistance;
}
/// Returns the list of [RenderBox] objects that are in the given
/// column, in row order, starting from the first row.
///
/// This is a lazily-evaluated iterable.
Iterable<RenderBox> column(int x) sync* {
for (int y = 0; y < rows; y += 1) {
final int xy = x + y * columns;
......@@ -703,6 +760,10 @@ class RenderTable extends RenderBox {
}
}
/// Returns the list of [RenderBox] objects that are on the given
/// row, in column order, starting with the first column.
///
/// This is a lazily-evaluated iterable.
Iterable<RenderBox> row(int y) sync* {
final int start = y * columns;
final int end = (y + 1) * columns;
......@@ -713,48 +774,166 @@ class RenderTable extends RenderBox {
}
}
List<double> computeColumnWidths(BoxConstraints constraints) {
List<double> _computeColumnWidths(BoxConstraints constraints) {
assert(_children.length == rows * columns);
// We apply the constraints to the column widths in the order of
// least important to most important:
// 1. apply the ideal widths (maxIntrinsicWidth)
// 2. grow the flex columns so that the table has the maxWidth (if
// finite) or the minWidth (if not)
// 3. if there were no flex columns, then grow the table to the
// minWidth.
// 4. apply the maximum width of the table, shrinking columns as
// necessary, applying minimum column widths as we go
// 1. apply ideal widths, and collect information we'll need later
final List<double> widths = new List<double>(columns);
final List<double> minWidths = new List<double>(columns);
final List<double> flexes = new List<double>(columns);
double totalMinWidth = 0.0;
double totalMaxWidth = constraints.maxWidth.isFinite ? constraints.maxWidth : 0.0;
double tableWidth = 0.0; // running tally of the sum of widths[x] for all x
double unflexedTableWidth = 0.0; // sum of the maxIntrinsicWidths of any column that has null flex
double totalFlex = 0.0;
for (int x = 0; x < columns; x += 1) {
TableColumnWidth columnWidth = _columnWidths[x] ?? defaultColumnWidth;
Iterable<RenderBox> columnCells = column(x);
double minIntrinsicWidth = columnWidth.minIntrinsicWidth(columnCells, constraints.maxWidth);
widths[x] = minIntrinsicWidth;
totalMinWidth += minIntrinsicWidth;
if (!constraints.maxWidth.isFinite) {
double maxIntrinsicWidth = columnWidth.maxIntrinsicWidth(columnCells, constraints.maxWidth);
assert(minIntrinsicWidth <= maxIntrinsicWidth);
totalMaxWidth += maxIntrinsicWidth;
}
// apply ideal width (maxIntrinsicWidth)
final double maxIntrinsicWidth = columnWidth.maxIntrinsicWidth(columnCells, constraints.maxWidth);
assert(maxIntrinsicWidth.isFinite);
assert(maxIntrinsicWidth >= 0.0);
widths[x] = maxIntrinsicWidth;
tableWidth += maxIntrinsicWidth;
// collect min width information while we're at it
final double minIntrinsicWidth = columnWidth.minIntrinsicWidth(columnCells, constraints.maxWidth);
assert(minIntrinsicWidth.isFinite);
assert(minIntrinsicWidth >= 0.0);
minWidths[x] = minIntrinsicWidth;
assert(maxIntrinsicWidth >= minIntrinsicWidth);
// collect flex information while we're at it
double flex = columnWidth.flex(columnCells);
if (flex != null) {
assert(flex != 0.0);
assert(flex.isFinite);
assert(flex > 0.0);
flexes[x] = flex;
totalFlex += flex;
} else {
unflexedTableWidth += maxIntrinsicWidth;
}
}
assert(!widths.any((double value) => value == null));
// table is going to be the biggest of:
// - the incoming minimum width
// - the sum of the cells' minimum widths
// - the incoming maximum width if it is finite, or else the table's ideal shrink-wrap width
double tableWidth = math.max(constraints.minWidth, math.max(totalMinWidth, totalMaxWidth));
double remainingWidth = tableWidth - totalMinWidth;
if (remainingWidth > 0.0) {
if (totalFlex > 0.0) {
final double maxWidthConstraint = constraints.maxWidth;
final double minWidthConstraint = constraints.minWidth;
// 2. grow the flex columns so that the table has the maxWidth (if
// finite) or the minWidth (if not)
if (totalFlex > 0.0) {
// this can only grow the table, but it _will_ grow the table at
// least as big as the target width.
double targetWidth;
if (maxWidthConstraint.isFinite) {
targetWidth = maxWidthConstraint;
} else {
targetWidth = minWidthConstraint;
}
if (tableWidth < targetWidth) {
final double remainingWidth = targetWidth - unflexedTableWidth;
assert(remainingWidth.isFinite);
assert(remainingWidth >= 0.0);
for (int x = 0; x < columns; x += 1) {
if (flexes[x] != null) {
widths[x] += math.max((flexes[x] / totalFlex) * remainingWidth - widths[x], 0.0);
final double flexedWidth = remainingWidth * flexes[x] / totalFlex;
assert(flexedWidth.isFinite);
assert(flexedWidth >= 0.0);
if (widths[x] < flexedWidth) {
final double delta = flexedWidth - widths[x];
tableWidth += delta;
widths[x] = flexedWidth;
}
}
}
} else {
for (int x = 0; x < columns; x += 1)
widths[x] += remainingWidth / columns;
assert(tableWidth >= targetWidth);
}
} else // step 2 and 3 are mutually exclusive
// 3. if there were no flex columns, then grow the table to the
// minWidth.
if (tableWidth < minWidthConstraint) {
final double delta = (minWidthConstraint - tableWidth) / columns;
for (int x = 0; x < columns; x += 1)
widths[x] += delta;
tableWidth = minWidthConstraint;
}
// beyond this point, unflexedTableWidth is no longer valid
assert(() { unflexedTableWidth = null; return true; });
// 4. apply the maximum width of the table, shrinking columns as
// necessary, applying minimum column widths as we go
if (tableWidth > maxWidthConstraint) {
double deficit = tableWidth - maxWidthConstraint;
// Some columns may have low flex but have all the free space.
// (Consider a case with a 1px wide column of flex 1000.0 and
// a 1000px wide column of flex 1.0; the sizes coming from the
// maxIntrinsicWidths. If the maximum table width is 2px, then
// just applying the flexes to the deficit would result in a
// table with one column at -998px and one column at 990px,
// which is wildly unhelpful.)
// Similarly, some columns may be flexible, but not actually
// be shrinkable due to a large minimum width. (Consider a
// case with two columns, one is flex and one isn't, both have
// 1000px maxIntrinsicWidths, but the flex one has 1000px
// minIntrinsicWidth also. The whole deficit will have to come
// from the non-flex column.)
// So what we do is we repeatedly iterate through the flexible
// columns shrinking them proportionally until we have no
// available columns, then do the same to the non-flexible ones.
int availableColumns = columns;
while (deficit > 0.0 && totalFlex > 0.0) {
double newTotalFlex = 0.0;
for (int x = 0; x < columns; x += 1) {
if (flexes[x] != null) {
final double newWidth = widths[x] - deficit * flexes[x] / totalFlex;
assert(newWidth.isFinite);
assert(newWidth >= 0.0);
if (newWidth <= minWidths[x]) {
// shrank to minimum
deficit -= widths[x] - minWidths[x];
widths[x] = minWidths[x];
flexes[x] = null;
availableColumns -= 1;
} else {
deficit -= widths[x] - newWidth;
widths[x] = newWidth;
newTotalFlex += flexes[x];
}
}
}
totalFlex = newTotalFlex;
}
if (deficit > 0.0) {
// Now we have to take out the remaining space from the
// columns that aren't minimum sized.
// To make this fair, we repeatedly remove equal amounts from
// each column, clamped to the minimum width, until we run out
// of columns that aren't at their minWidth.
do {
final double delta = deficit / availableColumns;
int newAvailableColumns = 0;
for (int x = 0; x < columns; x += 1) {
double availableDelta = widths[x] - minWidths[x];
if (availableDelta > 0.0) {
if (availableDelta <= delta) {
// shrank to minimum
deficit -= widths[x] - minWidths[x];
widths[x] = minWidths[x];
} else {
deficit -= availableDelta;
widths[x] -= availableDelta;
newAvailableColumns += 1;
}
}
}
availableColumns = newAvailableColumns;
} while (deficit > 0.0 && availableColumns > 0);
}
}
return widths;
......@@ -764,16 +943,30 @@ class RenderTable extends RenderBox {
List<double> _rowTops = <double>[];
List<double> _columnLefts;
/// Returns the position and dimensions of the box that the given
/// row covers, in this render object's coordinate space (so the
/// left coordinate is always 0.0).
///
/// The row being queried must exist.
///
/// This is only valid after layout.
Rect getRowBox(int row) {
assert(row >= 0);
assert(row < rows);
assert(!needsLayout);
return new Rect.fromLTRB(0.0, _rowTops[row], size.width, _rowTops[row + 1]);
}
@override
void performLayout() {
assert(_children.length == rows * columns);
if (rows * columns == 0) {
// TODO(ianh): if columns is zero, this should be zero width
// TODO(ianh): if columns is not zero, this should be based on the column width specifications
size = constraints.constrain(const Size(double.INFINITY, 0.0));
size = constraints.constrain(const Size(0.0, 0.0));
return;
}
final List<double> widths = computeColumnWidths(constraints);
final List<double> widths = _computeColumnWidths(constraints);
final List<double> positions = new List<double>(columns);
_rowTops.clear();
positions[0] = 0.0;
......
......@@ -957,9 +957,17 @@ class IntrinsicHeight extends SingleChildRenderObjectWidget {
RenderIntrinsicHeight createRenderObject(BuildContext context) => new RenderIntrinsicHeight();
}
/// Positions its child vertically according to the child's baseline.
/// Shifts the child down such that the child's baseline (or the
/// bottom of the child, if the child has no baseline) is [baseline]
/// logical pixels below the top of this box, then sizes this box to
/// contain the child. If [baseline] is less than the distance from
/// the top of the child to the baseline of the child, then the child
/// is top-aligned instead.
class Baseline extends SingleChildRenderObjectWidget {
Baseline({ Key key, this.baseline, this.baselineType: TextBaseline.alphabetic, Widget child })
/// Creates a [Baseline] object.
///
/// The [baseline] and [baselineType] arguments are required.
Baseline({ Key key, this.baseline, this.baselineType, Widget child })
: super(key: key, child: child) {
assert(baseline != null);
assert(baselineType != null);
......
......@@ -5,6 +5,7 @@
import 'dart:collection';
import 'framework.dart';
import 'table.dart';
Key _firstNonUniqueKey(Iterable<Widget> widgets) {
Set<Key> keySet = new HashSet<Key>();
......@@ -18,6 +19,20 @@ Key _firstNonUniqueKey(Iterable<Widget> widgets) {
return null;
}
/// Asserts if the given child list contains any duplicate non-null keys.
///
/// To invoke this function, use the following pattern, typically in the
/// relevant Widget's constructor:
///
/// ```dart
/// assert(!debugChildrenHaveDuplicateKeys(this, children));
/// ```
///
/// For a version of this function that can be used in contexts where
/// the list of items does not have a particular parent, see
/// [debugItemsHaveDuplicateKeys].
///
/// Does nothing if asserts are disabled. Always returns true.
bool debugChildrenHaveDuplicateKeys(Widget parent, Iterable<Widget> children) {
assert(() {
final Key nonUniqueKey = _firstNonUniqueKey(children);
......@@ -33,12 +48,54 @@ bool debugChildrenHaveDuplicateKeys(Widget parent, Iterable<Widget> children) {
return false;
}
/// Asserts if the given list of items contains any duplicate non-null keys.
///
/// To invoke this function, use the following pattern:
///
/// ```dart
/// assert(!debugItemsHaveDuplicateKeys(items));
/// ```
///
/// For a version of this function specifically intended for parents
/// checking their children lists, see [debugChildrenHaveDuplicateKeys].
///
/// Does nothing if asserts are disabled. Always returns true.
bool debugItemsHaveDuplicateKeys(Iterable<Widget> items) {
assert(() {
final Key nonUniqueKey = _firstNonUniqueKey(items);
if (nonUniqueKey != null)
throw new FlutterError('Duplicate key found: $nonUniqueKey.\n');
throw new FlutterError('Duplicate key found: $nonUniqueKey.');
return true;
});
return false;
}
/// Asserts that the given context has a [Table] ancestor.
///
/// Used by [RowInkWell] to make sure that it is only used in an appropriate context.
///
/// To invoke this function, use the following pattern, typically in the
/// relevant Widget's [build] method:
///
/// ```dart
/// assert(debugCheckHasTable(context));
/// ```
///
/// Does nothing if asserts are disabled. Always returns true.
bool debugCheckHasTable(BuildContext context) {
assert(() {
if (context.widget is! Table && context.ancestorWidgetOfExactType(Table) == null) {
Element element = context;
throw new FlutterError(
'No Table widget found.\n'
'${context.widget.runtimeType} widgets require a Table widget ancestor.\n'
'The specific widget that could not find a Table ancestor was:\n'
' ${context.widget}\n'
'The ownership chain for the affected widget is:\n'
' ${element.debugGetCreatorChain(10)}'
);
}
return true;
});
return true;
}
......@@ -1009,6 +1009,7 @@ abstract class Element implements BuildContext {
}
Element inflateWidget(Widget newWidget, dynamic newSlot) {
assert(newWidget != null);
Key key = newWidget.key;
if (key is GlobalKey) {
Element newChild = _retakeInactiveElement(key, newWidget);
......
......@@ -25,6 +25,25 @@ class TableRow {
final LocalKey key;
final Decoration decoration;
final List<Widget> children;
@override
String toString() {
StringBuffer result = new StringBuffer();
result.write('TableRow(');
if (key != null)
result.write('$key, ');
if (decoration != null)
result.write('$decoration, ');
if (children != null) {
result.write('child list is null');
} else if (children.length == 0) {
result.write('no children');
} else {
result.write('$children');
}
result.write(')');
return result.toString();
}
}
class _TableElementRow {
......@@ -54,6 +73,7 @@ class Table extends RenderObjectWidget {
assert(children != null);
assert(defaultColumnWidth != null);
assert(defaultVerticalAlignment != null);
assert(!children.any((TableRow row) => row.children.any((Widget cell) => cell == null)));
assert(() {
List<Widget> flatChildren = children.expand((TableRow row) => row.children).toList(growable: false);
return !debugChildrenHaveDuplicateKeys(this, flatChildren);
......@@ -125,7 +145,10 @@ class _TableElement extends RenderObjectElement {
_children = widget.children.map((TableRow row) {
return new _TableElementRow(
key: row.key,
children: row.children.map((Widget child) => inflateWidget(child, null)).toList(growable: false)
children: row.children.map/*<Element>*/((Widget child) {
assert(child != null);
return inflateWidget(child, null);
}).toList(growable: false)
);
}).toList(growable: false);
assert(() { _debugWillReattachChildren = false; return true; });
......
......@@ -26,7 +26,7 @@ void main() {
RenderTable table;
layout(new RenderPositionedBox(child: table = new RenderTable()));
expect(table.size, equals(const Size(800.0, 0.0)));
expect(table.size, equals(const Size(0.0, 0.0)));
});
test('Table test: combinations', () {
......@@ -39,13 +39,13 @@ void main() {
textBaseline: TextBaseline.alphabetic
)));
expect(table.size, equals(const Size(800.0, 0.0)));
expect(table.size, equals(const Size(0.0, 0.0)));
table.setChild(2, 4, sizedBox(100.0, 200.0));
pumpFrame();
expect(table.size, equals(new Size(800.0, 200.0)));
expect(table.size, equals(new Size(100.0, 200.0)));
table.setChild(0, 0, sizedBox(10.0, 30.0));
table.setChild(1, 0, sizedBox(20.0, 20.0));
......@@ -53,7 +53,7 @@ void main() {
pumpFrame();
expect(table.size, equals(new Size(800.0, 230.0)));
expect(table.size, equals(new Size(130.0, 230.0)));
});
test('Table test: removing cells', () {
......
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