table.dart 14 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
import 'package:flutter/rendering.dart';

10
import 'basic.dart';
Hixie's avatar
Hixie committed
11 12
import 'debug.dart';
import 'framework.dart';
13
import 'image.dart';
Hixie's avatar
Hixie committed
14 15

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

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

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

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

  /// 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
52
  final List<Widget> children;
53 54 55

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

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

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

157
  /// The rows of the table.
158 159 160
  ///
  /// 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
161
  final List<TableRow> children;
162 163 164 165

  /// 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
166 167 168 169 170
  /// [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.
171 172 173 174 175
  ///
  /// 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
176
  final Map<int, TableColumnWidth> columnWidths;
177 178 179 180 181

  /// 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
182
  final TableColumnWidth defaultColumnWidth;
183

184 185 186 187 188
  /// The direction in which the columns are ordered.
  ///
  /// Defaults to the ambient [Directionality].
  final TextDirection textDirection;

189
  /// The style to use when painting the boundary and interior divisions of the table.
Hixie's avatar
Hixie committed
190
  final TableBorder border;
191 192

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

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

198 199
  final List<Decoration> _rowDecorations;

Hixie's avatar
Hixie committed
200
  @override
201
  _TableElement createElement() => _TableElement(this);
Hixie's avatar
Hixie committed
202 203 204

  @override
  RenderTable createRenderObject(BuildContext context) {
205
    assert(debugCheckHasDirectionality(context));
206
    return RenderTable(
207
      columns: children.isNotEmpty ? children[0].children.length : 0,
Hixie's avatar
Hixie committed
208 209 210
      rows: children.length,
      columnWidths: columnWidths,
      defaultColumnWidth: defaultColumnWidth,
211
      textDirection: textDirection ?? Directionality.of(context),
Hixie's avatar
Hixie committed
212
      border: border,
213
      rowDecorations: _rowDecorations,
214
      configuration: createLocalImageConfiguration(context),
Hixie's avatar
Hixie committed
215
      defaultVerticalAlignment: defaultVerticalAlignment,
216
      textBaseline: textBaseline,
Hixie's avatar
Hixie committed
217 218 219 220 221
    );
  }

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

297
  final Set<Element> _forgottenChildren = HashSet<Element>();
Hixie's avatar
Hixie committed
298 299 300 301

  @override
  void update(Table newWidget) {
    assert(!_debugWillReattachChildren);
302
    assert(() { _debugWillReattachChildren = true; return true; }());
303 304 305 306 307 308
    final Map<LocalKey, List<Element>> oldKeyedRows = <LocalKey, List<Element>>{};
    for (_TableElementRow row in _children) {
      if (row.key != null) {
        oldKeyedRows[row.key] = row.children;
      }
    }
309 310
    final Iterator<_TableElementRow> oldUnkeyedRows = _children.where((_TableElementRow row) => row.key == null).iterator;
    final List<_TableElementRow> newChildren = <_TableElementRow>[];
311
    final Set<List<Element>> taken = Set<List<Element>>();
Hixie's avatar
Hixie committed
312 313 314 315 316 317 318 319 320 321
    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>[];
      }
322
      newChildren.add(_TableElementRow(
Hixie's avatar
Hixie committed
323
        key: row.key,
324
        children: updateChildren(oldChildren, row.children, forgottenChildren: _forgottenChildren)
Hixie's avatar
Hixie committed
325 326 327
      ));
    }
    while (oldUnkeyedRows.moveNext())
328
      updateChildren(oldUnkeyedRows.current.children, const <Widget>[], forgottenChildren: _forgottenChildren);
Hixie's avatar
Hixie committed
329
    for (List<Element> oldChildren in oldKeyedRows.values.where((List<Element> list) => !taken.contains(list)))
330
      updateChildren(oldChildren, const <Widget>[], forgottenChildren: _forgottenChildren);
331
    assert(() { _debugWillReattachChildren = false; return true; }());
Hixie's avatar
Hixie committed
332 333
    _children = newChildren;
    _updateRenderObjectChildren();
334
    _forgottenChildren.clear();
Hixie's avatar
Hixie committed
335 336 337 338 339 340 341
    super.update(newWidget);
    assert(widget == newWidget);
  }

  void _updateRenderObjectChildren() {
    assert(renderObject != null);
    renderObject.setFlatChildren(
342
      _children.isNotEmpty ? _children[0].children.length : 0,
343 344 345 346 347 348
      _children.expand<RenderBox>((_TableElementRow row) {
        return row.children.map<RenderBox>((Element child) {
          final RenderBox box = child.renderObject;
          return box;
        });
      }).toList()
Hixie's avatar
Hixie committed
349 350 351 352 353
    );
  }

  @override
  void visitChildren(ElementVisitor visitor) {
354
    for (Element child in _children.expand<Element>((_TableElementRow row) => row.children)) {
355
      if (!_forgottenChildren.contains(child))
Hixie's avatar
Hixie committed
356 357 358 359 360
        visitor(child);
    }
  }

  @override
361 362
  bool forgetChild(Element child) {
    _forgottenChildren.add(child);
Hixie's avatar
Hixie committed
363 364 365 366
    return true;
  }
}

367 368 369 370 371 372
/// 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
373
class TableCell extends ParentDataWidget<Table> {
374
  /// Creates a widget that controls how a child of a [Table] is aligned.
375
  const TableCell({
376 377 378 379
    Key key,
    this.verticalAlignment,
    @required Widget child
  }) : super(key: key, child: child);
Hixie's avatar
Hixie committed
380

381
  /// How this cell is aligned vertically.
Hixie's avatar
Hixie committed
382 383 384 385 386 387 388
  final TableCellVerticalAlignment verticalAlignment;

  @override
  void applyParentData(RenderObject renderObject) {
    final TableCellParentData parentData = renderObject.parentData;
    if (parentData.verticalAlignment != verticalAlignment) {
      parentData.verticalAlignment = verticalAlignment;
389
      final AbstractNode targetParent = renderObject.parent;
Hixie's avatar
Hixie committed
390 391 392 393 394 395
      if (targetParent is RenderObject)
        targetParent.markNeedsLayout();
    }
  }

  @override
396 397
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
398
    properties.add(EnumProperty<TableCellVerticalAlignment>('verticalAlignment', verticalAlignment));
Hixie's avatar
Hixie committed
399 400
  }
}