table.dart 13.5 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].
31
@immutable
Hixie's avatar
Hixie committed
32
class TableRow {
33
  /// Creates a row in a [Table].
34
  const TableRow({ this.key, this.decoration, this.children });
35 36

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

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

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

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

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

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

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

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

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

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

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

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

191 192
  final List<Decoration> _rowDecorations;

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

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

  @override
  void updateRenderObject(BuildContext context, RenderTable renderObject) {
213
    assert(renderObject.columns == (children.isNotEmpty ? children[0].children.length : 0));
Hixie's avatar
Hixie committed
214 215 216 217 218
    assert(renderObject.rows == children.length);
    renderObject
      ..columnWidths = columnWidths
      ..defaultColumnWidth = defaultColumnWidth
      ..border = border
219
      ..rowDecorations = _rowDecorations
220
      ..configuration = createLocalImageConfiguration(context)
Hixie's avatar
Hixie committed
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 249
      ..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,
250
        children: row.children.map<Element>((Widget child) {
251 252 253
          assert(child != null);
          return inflateWidget(child, null);
        }).toList(growable: false)
Hixie's avatar
Hixie committed
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272
      );
    }).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) {
273 274 275 276 277 278 279 280 281
    assert(() {
      if (_debugWillReattachChildren)
        return true;
      for (Element forgottenChild in _forgottenChildren) {
        if (forgottenChild.renderObject == child)
          return true;
      }
      return false;
    });
282
    final TableCellParentData childParentData = child.parentData;
Hixie's avatar
Hixie committed
283 284 285
    renderObject.setChild(childParentData.x, childParentData.y, null);
  }

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

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

  void _updateRenderObjectChildren() {
    assert(renderObject != null);
    renderObject.setFlatChildren(
330
      _children.isNotEmpty ? _children[0].children.length : 0,
Hixie's avatar
Hixie committed
331 332 333 334 335 336 337
      _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)) {
338
      if (!_forgottenChildren.contains(child))
Hixie's avatar
Hixie committed
339 340 341 342 343
        visitor(child);
    }
  }

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

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

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

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

  @override
379
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
380 381
    super.debugFillProperties(description);
    description.add(new EnumProperty<TableCellVerticalAlignment>('verticalAlignment', verticalAlignment));
Hixie's avatar
Hixie committed
382 383
  }
}