blob: 7d3c86dde137634a9afc60c379494ad6a43d6754 [file] [log] [blame]
// Copyright 2015 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:collection';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'basic.dart';
import 'debug.dart';
import 'framework.dart';
import 'image.dart';
export 'package:flutter/rendering.dart' show
FixedColumnWidth,
FlexColumnWidth,
FractionColumnWidth,
IntrinsicColumnWidth,
MaxColumnWidth,
MinColumnWidth,
TableBorder,
TableCellVerticalAlignment,
TableColumnWidth;
/// A horizontal group of cells in a [Table].
///
/// Every row in a table must have the same number of children.
///
/// The alignment of individual cells in a row can be controlled using a
/// [TableCell].
@immutable
class TableRow {
/// Creates a row in a [Table].
const TableRow({ this.key, this.decoration, this.children });
/// An identifier for the row.
final LocalKey key;
/// A decoration to paint behind this row.
///
/// Row decorations fill the horizontal and vertical extent of each row in
/// the table, unlike decorations for individual cells, which might not fill
/// either.
final Decoration decoration;
/// The widgets that comprise the cells in this row.
///
/// Children may be wrapped in [TableCell] widgets to provide per-cell
/// configuration to the [Table], but children are not required to be wrapped
/// in [TableCell] widgets.
final List<Widget> children;
@override
String toString() {
final 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.isEmpty) {
result.write('no children');
} else {
result.write('$children');
}
result.write(')');
return result.toString();
}
}
class _TableElementRow {
const _TableElementRow({ this.key, this.children });
final LocalKey key;
final List<Element> children;
}
/// A widget that uses the table layout algorithm for its children.
///
/// If you only have one row, the [Row] widget is more appropriate. If you only
/// have one column, the [SliverList] or [Column] widgets will be more
/// appropriate.
///
/// Rows size vertically based on their contents. To control the column widths,
/// use the [columnWidths] property.
///
/// For more details about the table layout algorithm, see [RenderTable].
/// To control the alignment of children, see [TableCell].
class Table extends RenderObjectWidget {
/// Creates a table.
///
/// The [children], [defaultColumnWidth], and [defaultVerticalAlignment]
/// arguments must not be null.
Table({
Key key,
this.children: const <TableRow>[],
this.columnWidths,
this.defaultColumnWidth: const FlexColumnWidth(1.0),
this.textDirection,
this.border,
this.defaultVerticalAlignment: TableCellVerticalAlignment.top,
this.textBaseline
}) : assert(children != null),
assert(defaultColumnWidth != null),
assert(defaultVerticalAlignment != null),
assert(() {
if (children.any((TableRow row) => row.children.any((Widget cell) => cell == null))) {
throw new FlutterError(
'One of the children of one of the rows of the table was null.\n'
'The children of a TableRow must not be null.'
);
}
return true;
}()),
assert(() {
if (children.any((TableRow row1) => row1.key != null && children.any((TableRow row2) => row1 != row2 && row1.key == row2.key))) {
throw new FlutterError(
'Two or more TableRow children of this Table had the same key.\n'
'All the keyed TableRow children of a Table must have different Keys.'
);
}
return true;
}()),
assert(() {
if (children.isNotEmpty) {
final int cellCount = children.first.children.length;
if (children.any((TableRow row) => row.children.length != cellCount)) {
throw new FlutterError(
'Table contains irregular row lengths.\n'
'Every TableRow in a Table must have the same number of children, so that every cell is filled. '
'Otherwise, the table will contain holes.'
);
}
}
return true;
}()),
_rowDecorations = children.any((TableRow row) => row.decoration != null)
? children.map<Decoration>((TableRow row) => row.decoration).toList(growable: false)
: null,
super(key: key) {
assert(() {
final List<Widget> flatChildren = children.expand((TableRow row) => row.children).toList(growable: false);
if (debugChildrenHaveDuplicateKeys(this, flatChildren)) {
throw new FlutterError(
'Two or more cells in this Table contain widgets with the same key.\n'
'Every widget child of every TableRow in a Table must have different keys. The cells of a Table are '
'flattened out for processing, so separate cells cannot have duplicate keys even if they are in '
'different rows.'
);
}
return true;
}());
}
/// The rows of the table.
///
/// Every row in a table must have the same number of children, and all the
/// children must be non-null.
final List<TableRow> children;
/// How the horizontal extents of the columns of this table should be determined.
///
/// If the [Map] has a null entry for a given column, the table uses the
/// [defaultColumnWidth] instead. By default, that uses flex sizing to
/// distribute free space equally among the columns.
///
/// The [FixedColumnWidth] class can be used to specify a specific width in
/// pixels. That is the cheapest way to size a table's columns.
///
/// The layout performance of the table depends critically on which column
/// sizing algorithms are used here. In particular, [IntrinsicColumnWidth] is
/// quite expensive because it needs to measure each cell in the column to
/// determine the intrinsic size of the column.
final Map<int, TableColumnWidth> columnWidths;
/// How to determine with widths of columns that don't have an explicit sizing algorithm.
///
/// Specifically, the [defaultColumnWidth] is used for column `i` if
/// `columnWidths[i]` is null.
final TableColumnWidth defaultColumnWidth;
/// The direction in which the columns are ordered.
///
/// Defaults to the ambient [Directionality].
final TextDirection textDirection;
/// The style to use when painting the boundary and interior divisions of the table.
final TableBorder border;
/// How cells that do not explicitly specify a vertical alignment are aligned vertically.
final TableCellVerticalAlignment defaultVerticalAlignment;
/// The text baseline to use when aligning rows using [TableCellVerticalAlignment.baseline].
final TextBaseline textBaseline;
final List<Decoration> _rowDecorations;
@override
_TableElement createElement() => new _TableElement(this);
@override
RenderTable createRenderObject(BuildContext context) {
assert(debugCheckHasDirectionality(context));
return new RenderTable(
columns: children.isNotEmpty ? children[0].children.length : 0,
rows: children.length,
columnWidths: columnWidths,
defaultColumnWidth: defaultColumnWidth,
textDirection: textDirection ?? Directionality.of(context),
border: border,
rowDecorations: _rowDecorations,
configuration: createLocalImageConfiguration(context),
defaultVerticalAlignment: defaultVerticalAlignment,
textBaseline: textBaseline
);
}
@override
void updateRenderObject(BuildContext context, RenderTable renderObject) {
assert(debugCheckHasDirectionality(context));
assert(renderObject.columns == (children.isNotEmpty ? children[0].children.length : 0));
assert(renderObject.rows == children.length);
renderObject
..columnWidths = columnWidths
..defaultColumnWidth = defaultColumnWidth
..textDirection = textDirection ?? Directionality.of(context)
..border = border
..rowDecorations = _rowDecorations
..configuration = createLocalImageConfiguration(context)
..defaultVerticalAlignment = defaultVerticalAlignment
..textBaseline = textBaseline;
}
}
class _TableElement extends RenderObjectElement {
_TableElement(Table widget) : super(widget);
@override
Table get widget => super.widget;
@override
RenderTable get renderObject => super.renderObject;
// This class ignores the child's slot entirely.
// Instead of doing incremental updates to the child list, it replaces the entire list each frame.
List<_TableElementRow> _children = const<_TableElementRow>[];
bool _debugWillReattachChildren = false;
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
assert(!_debugWillReattachChildren);
assert(() { _debugWillReattachChildren = true; return true; }());
_children = widget.children.map((TableRow row) {
return new _TableElementRow(
key: row.key,
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; }());
_updateRenderObjectChildren();
}
@override
void insertChildRenderObject(RenderObject child, Element slot) {
assert(_debugWillReattachChildren);
renderObject.setupParentData(child);
}
@override
void moveChildRenderObject(RenderObject child, dynamic slot) {
assert(_debugWillReattachChildren);
}
@override
void removeChildRenderObject(RenderObject child) {
assert(() {
if (_debugWillReattachChildren)
return true;
for (Element forgottenChild in _forgottenChildren) {
if (forgottenChild.renderObject == child)
return true;
}
return false;
}());
final TableCellParentData childParentData = child.parentData;
renderObject.setChild(childParentData.x, childParentData.y, null);
}
final Set<Element> _forgottenChildren = new HashSet<Element>();
@override
void update(Table newWidget) {
assert(!_debugWillReattachChildren);
assert(() { _debugWillReattachChildren = true; return true; }());
final Map<LocalKey, List<Element>> oldKeyedRows = <LocalKey, List<Element>>{};
for (_TableElementRow row in _children) {
if (row.key != null) {
oldKeyedRows[row.key] = row.children;
}
}
final Iterator<_TableElementRow> oldUnkeyedRows = _children.where((_TableElementRow row) => row.key == null).iterator;
final List<_TableElementRow> newChildren = <_TableElementRow>[];
final Set<List<Element>> taken = new Set<List<Element>>();
for (TableRow row in newWidget.children) {
List<Element> oldChildren;
if (row.key != null && oldKeyedRows.containsKey(row.key)) {
oldChildren = oldKeyedRows[row.key];
taken.add(oldChildren);
} else if (row.key == null && oldUnkeyedRows.moveNext()) {
oldChildren = oldUnkeyedRows.current.children;
} else {
oldChildren = const <Element>[];
}
newChildren.add(new _TableElementRow(
key: row.key,
children: updateChildren(oldChildren, row.children, forgottenChildren: _forgottenChildren)
));
}
while (oldUnkeyedRows.moveNext())
updateChildren(oldUnkeyedRows.current.children, const <Widget>[], forgottenChildren: _forgottenChildren);
for (List<Element> oldChildren in oldKeyedRows.values.where((List<Element> list) => !taken.contains(list)))
updateChildren(oldChildren, const <Widget>[], forgottenChildren: _forgottenChildren);
assert(() { _debugWillReattachChildren = false; return true; }());
_children = newChildren;
_updateRenderObjectChildren();
_forgottenChildren.clear();
super.update(newWidget);
assert(widget == newWidget);
}
void _updateRenderObjectChildren() {
assert(renderObject != null);
renderObject.setFlatChildren(
_children.isNotEmpty ? _children[0].children.length : 0,
_children.expand<RenderBox>((_TableElementRow row) {
return row.children.map<RenderBox>((Element child) {
final RenderBox box = child.renderObject;
return box;
});
}).toList()
);
}
@override
void visitChildren(ElementVisitor visitor) {
for (Element child in _children.expand((_TableElementRow row) => row.children)) {
if (!_forgottenChildren.contains(child))
visitor(child);
}
}
@override
bool forgetChild(Element child) {
_forgottenChildren.add(child);
return true;
}
}
/// A widget that controls how a child of a [Table] is aligned.
///
/// A [TableCell] widget must be a descendant of a [Table], and the path from
/// the [TableCell] widget to its enclosing [Table] must contain only
/// [TableRow]s, [StatelessWidget]s, or [StatefulWidget]s (not
/// other kinds of widgets, like [RenderObjectWidget]s).
class TableCell extends ParentDataWidget<Table> {
/// Creates a widget that controls how a child of a [Table] is aligned.
const TableCell({
Key key,
this.verticalAlignment,
@required Widget child
}) : super(key: key, child: child);
/// How this cell is aligned vertically.
final TableCellVerticalAlignment verticalAlignment;
@override
void applyParentData(RenderObject renderObject) {
final TableCellParentData parentData = renderObject.parentData;
if (parentData.verticalAlignment != verticalAlignment) {
parentData.verticalAlignment = verticalAlignment;
final AbstractNode targetParent = renderObject.parent;
if (targetParent is RenderObject)
targetParent.markNeedsLayout();
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(new EnumProperty<TableCellVerticalAlignment>('verticalAlignment', verticalAlignment));
}
}