table.dart 13.4 KB
Newer Older
Hixie's avatar
Hixie committed
1 2 3 4 5 6
// 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';

7
import 'package:flutter/foundation.dart';
Hixie's avatar
Hixie committed
8 9 10 11
import 'package:flutter/rendering.dart';

import 'debug.dart';
import 'framework.dart';
12
import 'image.dart';
Hixie's avatar
Hixie committed
13 14

export 'package:flutter/rendering.dart' show
15 16 17 18 19 20 21 22 23
  FixedColumnWidth,
  FlexColumnWidth,
  FractionColumnWidth,
  IntrinsicColumnWidth,
  MaxColumnWidth,
  MinColumnWidth,
  TableBorder,
  TableCellVerticalAlignment,
  TableColumnWidth;
Hixie's avatar
Hixie committed
24

25
/// A horizontal group of cells in a [Table].
26 27 28 29 30
///
/// 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].
Hixie's avatar
Hixie committed
31
class TableRow {
32
  /// Creates a row in a [Table].
33
  const TableRow({ this.key, this.decoration, this.children });
34 35

  /// An identifier for the row.
Hixie's avatar
Hixie committed
36
  final LocalKey key;
37 38 39 40 41 42

  /// 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.
43
  final Decoration decoration;
44 45 46 47 48 49

  /// 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.
Hixie's avatar
Hixie committed
50
  final List<Widget> children;
51 52 53

  @override
  String toString() {
54
    final StringBuffer result = new StringBuffer();
55 56 57 58 59
    result.write('TableRow(');
    if (key != null)
      result.write('$key, ');
    if (decoration != null)
      result.write('$decoration, ');
60
    if (children == null) {
61
      result.write('child list is null');
62
    } else if (children.isEmpty) {
63 64 65 66 67 68 69
      result.write('no children');
    } else {
      result.write('$children');
    }
    result.write(')');
    return result.toString();
  }
Hixie's avatar
Hixie committed
70 71 72 73 74 75 76 77
}

class _TableElementRow {
  const _TableElementRow({ this.key, this.children });
  final LocalKey key;
  final List<Element> children;
}

78
/// A widget that uses the table layout algorithm for its children.
Hixie's avatar
Hixie committed
79
///
80
/// If you only have one row, the [Row] widget is more appropriate. If you only
81 82
/// have one column, the [SliverList] or [Column] widgets will be more
/// appropriate.
83 84 85 86 87
///
/// 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].
Hixie's avatar
Hixie committed
88 89
/// To control the alignment of children, see [TableCell].
class Table extends RenderObjectWidget {
90 91 92 93
  /// Creates a table.
  ///
  /// The [children], [defaultColumnWidth], and [defaultVerticalAlignment]
  /// arguments must not be null.
Hixie's avatar
Hixie committed
94 95
  Table({
    Key key,
96
    this.children: const <TableRow>[],
Hixie's avatar
Hixie committed
97 98 99 100 101
    this.columnWidths,
    this.defaultColumnWidth: const FlexColumnWidth(1.0),
    this.border,
    this.defaultVerticalAlignment: TableCellVerticalAlignment.top,
    this.textBaseline
102
  }) : _rowDecorations = children.any((TableRow row) => row.decoration != null)
103
                         ? children.map<Decoration>((TableRow row) => row.decoration).toList(growable: false)
104
                         : null,
105
       super(key: key) {
Hixie's avatar
Hixie committed
106 107 108
    assert(children != null);
    assert(defaultColumnWidth != null);
    assert(defaultVerticalAlignment != null);
109 110 111 112 113 114 115 116 117
    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;
    });
Hixie's avatar
Hixie committed
118
    assert(() {
119
      final List<Widget> flatChildren = children.expand((TableRow row) => row.children).toList(growable: false);
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
      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;
    });
    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(() {
140
      if (children.isNotEmpty) {
141 142 143 144 145 146 147 148 149 150
        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;
Hixie's avatar
Hixie committed
151 152 153
    });
  }

154
  /// The rows of the table.
155 156 157
  ///
  /// Every row in a table must have the same number of children, and all the
  /// children must be non-null.
Hixie's avatar
Hixie committed
158
  final List<TableRow> children;
159 160 161 162

  /// 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
163 164 165 166 167
  /// [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.
168 169 170 171 172
  ///
  /// 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.
Hixie's avatar
Hixie committed
173
  final Map<int, TableColumnWidth> columnWidths;
174 175 176 177 178

  /// 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.
Hixie's avatar
Hixie committed
179
  final TableColumnWidth defaultColumnWidth;
180 181

  /// The style to use when painting the boundary and interior divisions of the table.
Hixie's avatar
Hixie committed
182
  final TableBorder border;
183 184

  /// How cells that do not explicitly specify a vertical alignment are aligned vertically.
Hixie's avatar
Hixie committed
185
  final TableCellVerticalAlignment defaultVerticalAlignment;
186 187

  /// The text baseline to use when aligning rows using [TableCellVerticalAlignment.baseline].
Hixie's avatar
Hixie committed
188 189
  final TextBaseline textBaseline;

190 191
  final List<Decoration> _rowDecorations;

Hixie's avatar
Hixie committed
192 193 194 195 196 197
  @override
  _TableElement createElement() => new _TableElement(this);

  @override
  RenderTable createRenderObject(BuildContext context) {
    return new RenderTable(
198
      columns: children.isNotEmpty ? children[0].children.length : 0,
Hixie's avatar
Hixie committed
199 200 201 202
      rows: children.length,
      columnWidths: columnWidths,
      defaultColumnWidth: defaultColumnWidth,
      border: border,
203
      rowDecorations: _rowDecorations,
204
      configuration: createLocalImageConfiguration(context),
Hixie's avatar
Hixie committed
205 206 207 208 209 210 211
      defaultVerticalAlignment: defaultVerticalAlignment,
      textBaseline: textBaseline
    );
  }

  @override
  void updateRenderObject(BuildContext context, RenderTable renderObject) {
212
    assert(renderObject.columns == (children.isNotEmpty ? children[0].children.length : 0));
Hixie's avatar
Hixie committed
213 214 215 216 217
    assert(renderObject.rows == children.length);
    renderObject
      ..columnWidths = columnWidths
      ..defaultColumnWidth = defaultColumnWidth
      ..border = border
218
      ..rowDecorations = _rowDecorations
219
      ..configuration = createLocalImageConfiguration(context)
Hixie's avatar
Hixie committed
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248
      ..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,
249
        children: row.children.map<Element>((Widget child) {
250 251 252
          assert(child != null);
          return inflateWidget(child, null);
        }).toList(growable: false)
Hixie's avatar
Hixie committed
253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271
      );
    }).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) {
272 273 274 275 276 277 278 279 280
    assert(() {
      if (_debugWillReattachChildren)
        return true;
      for (Element forgottenChild in _forgottenChildren) {
        if (forgottenChild.renderObject == child)
          return true;
      }
      return false;
    });
281
    final TableCellParentData childParentData = child.parentData;
Hixie's avatar
Hixie committed
282 283 284
    renderObject.setChild(childParentData.x, childParentData.y, null);
  }

285
  final Set<Element> _forgottenChildren = new HashSet<Element>();
Hixie's avatar
Hixie committed
286 287 288 289 290

  @override
  void update(Table newWidget) {
    assert(!_debugWillReattachChildren);
    assert(() { _debugWillReattachChildren = true; return true; });
291
    final Map<LocalKey, List<Element>> oldKeyedRows = new Map<LocalKey, List<Element>>.fromIterable(
Hixie's avatar
Hixie committed
292 293 294 295
      _children.where((_TableElementRow row) => row.key != null),
      key:   (_TableElementRow row) => row.key,
      value: (_TableElementRow row) => row.children
    );
296 297 298
    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>>();
Hixie's avatar
Hixie committed
299 300 301 302 303 304 305 306 307 308 309 310
    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,
311
        children: updateChildren(oldChildren, row.children, forgottenChildren: _forgottenChildren)
Hixie's avatar
Hixie committed
312 313 314
      ));
    }
    while (oldUnkeyedRows.moveNext())
315
      updateChildren(oldUnkeyedRows.current.children, const <Widget>[], forgottenChildren: _forgottenChildren);
Hixie's avatar
Hixie committed
316
    for (List<Element> oldChildren in oldKeyedRows.values.where((List<Element> list) => !taken.contains(list)))
317
      updateChildren(oldChildren, const <Widget>[], forgottenChildren: _forgottenChildren);
Hixie's avatar
Hixie committed
318 319 320
    assert(() { _debugWillReattachChildren = false; return true; });
    _children = newChildren;
    _updateRenderObjectChildren();
321
    _forgottenChildren.clear();
Hixie's avatar
Hixie committed
322 323 324 325 326 327 328
    super.update(newWidget);
    assert(widget == newWidget);
  }

  void _updateRenderObjectChildren() {
    assert(renderObject != null);
    renderObject.setFlatChildren(
329
      _children.isNotEmpty ? _children[0].children.length : 0,
Hixie's avatar
Hixie committed
330 331 332 333 334 335 336
      _children.expand((_TableElementRow row) => row.children.map((Element child) => child.renderObject)).toList()
    );
  }

  @override
  void visitChildren(ElementVisitor visitor) {
    for (Element child in _children.expand((_TableElementRow row) => row.children)) {
337
      if (!_forgottenChildren.contains(child))
Hixie's avatar
Hixie committed
338 339 340 341 342
        visitor(child);
    }
  }

  @override
343 344
  bool forgetChild(Element child) {
    _forgottenChildren.add(child);
Hixie's avatar
Hixie committed
345 346 347 348
    return true;
  }
}

349 350 351 352 353 354
/// 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).
Hixie's avatar
Hixie committed
355
class TableCell extends ParentDataWidget<Table> {
356
  /// Creates a widget that controls how a child of a [Table] is aligned.
357 358 359 360 361
  TableCell({
    Key key,
    this.verticalAlignment,
    @required Widget child
  }) : super(key: key, child: child);
Hixie's avatar
Hixie committed
362

363
  /// How this cell is aligned vertically.
Hixie's avatar
Hixie committed
364 365 366 367 368 369 370
  final TableCellVerticalAlignment verticalAlignment;

  @override
  void applyParentData(RenderObject renderObject) {
    final TableCellParentData parentData = renderObject.parentData;
    if (parentData.verticalAlignment != verticalAlignment) {
      parentData.verticalAlignment = verticalAlignment;
371
      final AbstractNode targetParent = renderObject.parent;
Hixie's avatar
Hixie committed
372 373 374 375 376 377 378 379 380 381 382
      if (targetParent is RenderObject)
        targetParent.markNeedsLayout();
    }
  }

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('verticalAlignment: $verticalAlignment');
  }
}