table.dart 16.4 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
Hixie's avatar
Hixie committed
2 3 4 5 6
// 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 = const <Widget>[]});
36 37

  /// An identifier for the row.
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.
52
  final List<Widget> children;
53 54 55

  @override
  String toString() {
56
    final StringBuffer result = StringBuffer();
57
    result.write('TableRow(');
58
    if (key != null) {
59
      result.write('$key, ');
60 61
    }
    if (decoration != null) {
62
      result.write('$decoration, ');
63
    }
64
    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
}

class _TableElementRow {
75 76
  const _TableElementRow({ this.key, required this.children });
  final LocalKey? key;
Hixie's avatar
Hixie committed
77 78 79
  final List<Element> children;
}

80
/// A widget that uses the table layout algorithm for its children.
Hixie's avatar
Hixie committed
81
///
82 83
/// {@youtube 560 315 https://www.youtube.com/watch?v=_lbE0wsVZSw}
///
84
/// {@tool dartpad}
85 86
/// This sample shows a [Table] with borders, multiple types of column widths
/// and different vertical cell alignments.
87
///
88
/// ** See code in examples/api/lib/widgets/table/table.0.dart **
89 90
/// {@end-tool}
///
91
/// If you only have one row, the [Row] widget is more appropriate. If you only
92 93
/// have one column, the [SliverList] or [Column] widgets will be more
/// appropriate.
94
///
95 96 97 98 99 100 101 102 103 104 105
/// Rows size vertically based on their contents. To control the individual
/// column widths, use the [columnWidths] property to specify a
/// [TableColumnWidth] for each column. If [columnWidths] is null, or there is a
/// null entry for a given column in [columnWidths], the table uses the
/// [defaultColumnWidth] instead.
///
/// By default, [defaultColumnWidth] is a [FlexColumnWidth]. This
/// [TableColumnWidth] divides up the remaining space in the horizontal axis to
/// determine the column width. If wrapping a [Table] in a horizontal
/// [ScrollView], choose a different [TableColumnWidth], such as
/// [FixedColumnWidth].
106 107
///
/// For more details about the table layout algorithm, see [RenderTable].
Hixie's avatar
Hixie committed
108
/// To control the alignment of children, see [TableCell].
109 110 111 112
///
/// See also:
///
///  * The [catalog of layout widgets](https://flutter.dev/widgets/layout/).
Hixie's avatar
Hixie committed
113
class Table extends RenderObjectWidget {
114 115 116 117
  /// Creates a table.
  ///
  /// The [children], [defaultColumnWidth], and [defaultVerticalAlignment]
  /// arguments must not be null.
Hixie's avatar
Hixie committed
118
  Table({
119
    super.key,
120
    this.children = const <TableRow>[],
Hixie's avatar
Hixie committed
121
    this.columnWidths,
122
    this.defaultColumnWidth = const FlexColumnWidth(),
123
    this.textDirection,
Hixie's avatar
Hixie committed
124
    this.border,
125
    this.defaultVerticalAlignment = TableCellVerticalAlignment.top,
126
    this.textBaseline, // NO DEFAULT: we don't know what the text's baseline should be
127
  }) : assert(defaultVerticalAlignment != TableCellVerticalAlignment.baseline || textBaseline != null, 'textBaseline is required if you specify the defaultVerticalAlignment with TableCellVerticalAlignment.baseline'),
128 129
       assert(() {
         if (children.any((TableRow row1) => row1.key != null && children.any((TableRow row2) => row1 != row2 && row1.key == row2.key))) {
130
           throw FlutterError(
131
             'Two or more TableRow children of this Table had the same key.\n'
132
             'All the keyed TableRow children of a Table must have different Keys.',
133 134 135
           );
         }
         return true;
136
       }()),
137 138
       assert(() {
         if (children.isNotEmpty) {
139 140
           final int cellCount = children.first.children.length;
           if (children.any((TableRow row) => row.children.length != cellCount)) {
141
             throw FlutterError(
142 143
               'Table contains irregular row lengths.\n'
               'Every TableRow in a Table must have the same number of children, so that every cell is filled. '
144
               'Otherwise, the table will contain holes.',
145 146
             );
           }
147 148 149 150 151 152
           if (children.any((TableRow row) => row.children.isEmpty)) {
             throw FlutterError(
               'One or more TableRow have no children.\n'
               'Every TableRow in a Table must have at least one child, so there is no empty row. ',
             );
           }
153 154
         }
         return true;
155
       }()),
156
       _rowDecorations = children.any((TableRow row) => row.decoration != null)
157
                              ? children.map<Decoration?>((TableRow row) => row.decoration).toList(growable: false)
158
                              : null {
Hixie's avatar
Hixie committed
159
    assert(() {
160
      final List<Widget> flatChildren = children.expand<Widget>((TableRow row) => row.children).toList(growable: false);
161 162 163 164 165 166
      return !debugChildrenHaveDuplicateKeys(this, flatChildren, message:
        '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.',
      );
167
    }());
Hixie's avatar
Hixie committed
168 169
  }

170
  /// The rows of the table.
171 172 173
  ///
  /// 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
174
  final List<TableRow> children;
175 176 177 178

  /// 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
179 180 181 182 183
  /// [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.
184 185 186 187 188
  ///
  /// 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.
189 190
  ///
  /// The keys of this map (column indexes) are zero-based.
191 192
  ///
  /// If this is set to null, then an empty map is assumed.
193
  final Map<int, TableColumnWidth>? columnWidths;
194

195 196
  /// How to determine with widths of columns that don't have an explicit sizing
  /// algorithm.
197 198
  ///
  /// Specifically, the [defaultColumnWidth] is used for column `i` if
199 200 201 202 203 204
  /// `columnWidths[i]` is null. Defaults to [FlexColumnWidth], which will
  /// divide the remaining horizontal space up evenly between columns of the
  /// same type [TableColumnWidth].
  ///
  /// A [Table] in a horizontal [ScrollView] must use a [FixedColumnWidth], or
  /// an [IntrinsicColumnWidth] as the horizontal space is infinite.
Hixie's avatar
Hixie committed
205
  final TableColumnWidth defaultColumnWidth;
206

207 208 209
  /// The direction in which the columns are ordered.
  ///
  /// Defaults to the ambient [Directionality].
210
  final TextDirection? textDirection;
211

212
  /// The style to use when painting the boundary and interior divisions of the table.
213
  final TableBorder? border;
214 215

  /// How cells that do not explicitly specify a vertical alignment are aligned vertically.
216 217 218
  ///
  /// Cells may specify a vertical alignment by wrapping their contents in a
  /// [TableCell] widget.
Hixie's avatar
Hixie committed
219
  final TableCellVerticalAlignment defaultVerticalAlignment;
220 221

  /// The text baseline to use when aligning rows using [TableCellVerticalAlignment.baseline].
222
  ///
223 224 225
  /// This must be set if using baseline alignment. There is no default because there is no
  /// way for the framework to know the correct baseline _a priori_.
  final TextBaseline? textBaseline;
Hixie's avatar
Hixie committed
226

227
  final List<Decoration?>? _rowDecorations;
228

Hixie's avatar
Hixie committed
229
  @override
230
  RenderObjectElement createElement() => _TableElement(this);
Hixie's avatar
Hixie committed
231 232 233

  @override
  RenderTable createRenderObject(BuildContext context) {
234
    assert(debugCheckHasDirectionality(context));
235
    return RenderTable(
236
      columns: children.isNotEmpty ? children[0].children.length : 0,
Hixie's avatar
Hixie committed
237 238 239
      rows: children.length,
      columnWidths: columnWidths,
      defaultColumnWidth: defaultColumnWidth,
240
      textDirection: textDirection ?? Directionality.of(context),
Hixie's avatar
Hixie committed
241
      border: border,
242
      rowDecorations: _rowDecorations,
243
      configuration: createLocalImageConfiguration(context),
Hixie's avatar
Hixie committed
244
      defaultVerticalAlignment: defaultVerticalAlignment,
245
      textBaseline: textBaseline,
Hixie's avatar
Hixie committed
246 247 248 249 250
    );
  }

  @override
  void updateRenderObject(BuildContext context, RenderTable renderObject) {
251
    assert(debugCheckHasDirectionality(context));
252
    assert(renderObject.columns == (children.isNotEmpty ? children[0].children.length : 0));
Hixie's avatar
Hixie committed
253 254 255 256
    assert(renderObject.rows == children.length);
    renderObject
      ..columnWidths = columnWidths
      ..defaultColumnWidth = defaultColumnWidth
257
      ..textDirection = textDirection ?? Directionality.of(context)
Hixie's avatar
Hixie committed
258
      ..border = border
259
      ..rowDecorations = _rowDecorations
260
      ..configuration = createLocalImageConfiguration(context)
Hixie's avatar
Hixie committed
261 262 263 264 265 266
      ..defaultVerticalAlignment = defaultVerticalAlignment
      ..textBaseline = textBaseline;
  }
}

class _TableElement extends RenderObjectElement {
267
  _TableElement(Table super.widget);
Hixie's avatar
Hixie committed
268 269

  @override
270
  RenderTable get renderObject => super.renderObject as RenderTable;
Hixie's avatar
Hixie committed
271 272 273

  List<_TableElementRow> _children = const<_TableElementRow>[];

274 275
  bool _doingMountOrUpdate = false;

Hixie's avatar
Hixie committed
276
  @override
277 278 279
  void mount(Element? parent, Object? newSlot) {
    assert(!_doingMountOrUpdate);
    _doingMountOrUpdate = true;
Hixie's avatar
Hixie committed
280
    super.mount(parent, newSlot);
281
    int rowIndex = -1;
282
    _children = (widget as Table).children.map<_TableElementRow>((TableRow row) {
283 284
      int columnIndex = 0;
      rowIndex += 1;
285
      return _TableElementRow(
Hixie's avatar
Hixie committed
286
        key: row.key,
287
        children: row.children.map<Element>((Widget child) {
288
          return inflateWidget(child, _TableSlot(columnIndex++, rowIndex));
289
        }).toList(growable: false),
Hixie's avatar
Hixie committed
290 291 292
      );
    }).toList(growable: false);
    _updateRenderObjectChildren();
293 294
    assert(_doingMountOrUpdate);
    _doingMountOrUpdate = false;
Hixie's avatar
Hixie committed
295 296 297
  }

  @override
298
  void insertRenderObjectChild(RenderBox child, _TableSlot slot) {
Hixie's avatar
Hixie committed
299
    renderObject.setupParentData(child);
300 301 302 303 304
    // Once [mount]/[update] are done, the children are getting set all at once
    // in [_updateRenderObjectChildren].
    if (!_doingMountOrUpdate) {
      renderObject.setChild(slot.column, slot.row, child);
    }
Hixie's avatar
Hixie committed
305 306 307
  }

  @override
308 309 310
  void moveRenderObjectChild(RenderBox child, _TableSlot oldSlot, _TableSlot newSlot) {
    assert(_doingMountOrUpdate);
    // Child gets moved at the end of [update] in [_updateRenderObjectChildren].
Hixie's avatar
Hixie committed
311 312 313
  }

  @override
314
  void removeRenderObjectChild(RenderBox child, _TableSlot slot) {
315
    renderObject.setChild(slot.column, slot.row, null);
Hixie's avatar
Hixie committed
316 317
  }

318
  final Set<Element> _forgottenChildren = HashSet<Element>();
Hixie's avatar
Hixie committed
319 320 321

  @override
  void update(Table newWidget) {
322 323
    assert(!_doingMountOrUpdate);
    _doingMountOrUpdate = true;
324
    final Map<LocalKey, List<Element>> oldKeyedRows = <LocalKey, List<Element>>{};
325
    for (final _TableElementRow row in _children) {
326
      if (row.key != null) {
327
        oldKeyedRows[row.key!] = row.children;
328 329
      }
    }
330 331
    final Iterator<_TableElementRow> oldUnkeyedRows = _children.where((_TableElementRow row) => row.key == null).iterator;
    final List<_TableElementRow> newChildren = <_TableElementRow>[];
332
    final Set<List<Element>> taken = <List<Element>>{};
333 334
    for (int rowIndex = 0; rowIndex < newWidget.children.length; rowIndex++) {
      final TableRow row = newWidget.children[rowIndex];
Hixie's avatar
Hixie committed
335 336
      List<Element> oldChildren;
      if (row.key != null && oldKeyedRows.containsKey(row.key)) {
337
        oldChildren = oldKeyedRows[row.key]!;
Hixie's avatar
Hixie committed
338 339 340 341 342 343
        taken.add(oldChildren);
      } else if (row.key == null && oldUnkeyedRows.moveNext()) {
        oldChildren = oldUnkeyedRows.current.children;
      } else {
        oldChildren = const <Element>[];
      }
344
      final List<_TableSlot> slots = List<_TableSlot>.generate(
345
        row.children.length,
346 347
        (int columnIndex) => _TableSlot(columnIndex, rowIndex),
      );
348
      newChildren.add(_TableElementRow(
Hixie's avatar
Hixie committed
349
        key: row.key,
350
        children: updateChildren(oldChildren, row.children, forgottenChildren: _forgottenChildren, slots: slots),
Hixie's avatar
Hixie committed
351 352
      ));
    }
353
    while (oldUnkeyedRows.moveNext()) {
354
      updateChildren(oldUnkeyedRows.current.children, const <Widget>[], forgottenChildren: _forgottenChildren);
355 356
    }
    for (final List<Element> oldChildren in oldKeyedRows.values.where((List<Element> list) => !taken.contains(list))) {
357
      updateChildren(oldChildren, const <Widget>[], forgottenChildren: _forgottenChildren);
358
    }
359

Hixie's avatar
Hixie committed
360 361
    _children = newChildren;
    _updateRenderObjectChildren();
362
    _forgottenChildren.clear();
Hixie's avatar
Hixie committed
363 364
    super.update(newWidget);
    assert(widget == newWidget);
365 366
    assert(_doingMountOrUpdate);
    _doingMountOrUpdate = false;
Hixie's avatar
Hixie committed
367 368 369 370
  }

  void _updateRenderObjectChildren() {
    renderObject.setFlatChildren(
371
      _children.isNotEmpty ? _children[0].children.length : 0,
372 373
      _children.expand<RenderBox>((_TableElementRow row) {
        return row.children.map<RenderBox>((Element child) {
374
          final RenderBox box = child.renderObject! as RenderBox;
375 376
          return box;
        });
377
      }).toList(),
Hixie's avatar
Hixie committed
378 379 380 381 382
    );
  }

  @override
  void visitChildren(ElementVisitor visitor) {
383
    for (final Element child in _children.expand<Element>((_TableElementRow row) => row.children)) {
384
      if (!_forgottenChildren.contains(child)) {
Hixie's avatar
Hixie committed
385
        visitor(child);
386
      }
Hixie's avatar
Hixie committed
387 388 389 390
    }
  }

  @override
391 392
  bool forgetChild(Element child) {
    _forgottenChildren.add(child);
393
    super.forgetChild(child);
Hixie's avatar
Hixie committed
394 395 396 397
    return true;
  }
}

398 399 400 401 402 403
/// 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).
404
class TableCell extends ParentDataWidget<TableCellParentData> {
405
  /// Creates a widget that controls how a child of a [Table] is aligned.
406
  const TableCell({
407
    super.key,
408
    this.verticalAlignment,
409 410
    required super.child,
  });
Hixie's avatar
Hixie committed
411

412
  /// How this cell is aligned vertically.
413
  final TableCellVerticalAlignment? verticalAlignment;
Hixie's avatar
Hixie committed
414 415 416

  @override
  void applyParentData(RenderObject renderObject) {
417
    final TableCellParentData parentData = renderObject.parentData! as TableCellParentData;
Hixie's avatar
Hixie committed
418 419
    if (parentData.verticalAlignment != verticalAlignment) {
      parentData.verticalAlignment = verticalAlignment;
420
      final RenderObject? targetParent = renderObject.parent;
421
      if (targetParent is RenderObject) {
Hixie's avatar
Hixie committed
422
        targetParent.markNeedsLayout();
423
      }
Hixie's avatar
Hixie committed
424 425 426
    }
  }

427 428 429
  @override
  Type get debugTypicalAncestorWidgetClass => Table;

Hixie's avatar
Hixie committed
430
  @override
431 432
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
433
    properties.add(EnumProperty<TableCellVerticalAlignment>('verticalAlignment', verticalAlignment));
Hixie's avatar
Hixie committed
434 435
  }
}
436 437 438 439 440 441 442 443 444 445

@immutable
class _TableSlot with Diagnosticable {
  const _TableSlot(this.column, this.row);

  final int column;
  final int row;

  @override
  bool operator ==(Object other) {
446
    if (other.runtimeType != runtimeType) {
447
      return false;
448
    }
449 450 451 452 453 454
    return other is _TableSlot
        && column == other.column
        && row == other.row;
  }

  @override
455
  int get hashCode => Object.hash(column, row);
456 457 458 459 460 461 462 463

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(IntProperty('x', column));
    properties.add(IntProperty('y', row));
  }
}