| 1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | /// @docImport 'scroll_view.dart'; |
| 6 | /// @docImport 'sliver.dart'; |
| 7 | library; |
| 8 | |
| 9 | import 'dart:collection'; |
| 10 | |
| 11 | import 'package:flutter/foundation.dart'; |
| 12 | import 'package:flutter/rendering.dart'; |
| 13 | |
| 14 | import 'basic.dart'; |
| 15 | import 'debug.dart'; |
| 16 | import 'framework.dart'; |
| 17 | import 'image.dart'; |
| 18 | |
| 19 | export 'package:flutter/rendering.dart' |
| 20 | show |
| 21 | FixedColumnWidth, |
| 22 | FlexColumnWidth, |
| 23 | FractionColumnWidth, |
| 24 | IntrinsicColumnWidth, |
| 25 | MaxColumnWidth, |
| 26 | MinColumnWidth, |
| 27 | TableBorder, |
| 28 | TableCellVerticalAlignment, |
| 29 | TableColumnWidth; |
| 30 | |
| 31 | /// A horizontal group of cells in a [Table]. |
| 32 | /// |
| 33 | /// Every row in a table must have the same number of children. |
| 34 | /// |
| 35 | /// The alignment of individual cells in a row can be controlled using a |
| 36 | /// [TableCell]. |
| 37 | @immutable |
| 38 | class TableRow { |
| 39 | /// Creates a row in a [Table]. |
| 40 | const TableRow({this.key, this.decoration, this.children = const <Widget>[]}); |
| 41 | |
| 42 | /// An identifier for the row. |
| 43 | final LocalKey? key; |
| 44 | |
| 45 | /// A decoration to paint behind this row. |
| 46 | /// |
| 47 | /// Row decorations fill the horizontal and vertical extent of each row in |
| 48 | /// the table, unlike decorations for individual cells, which might not fill |
| 49 | /// either. |
| 50 | final Decoration? decoration; |
| 51 | |
| 52 | /// The widgets that comprise the cells in this row. |
| 53 | /// |
| 54 | /// Children may be wrapped in [TableCell] widgets to provide per-cell |
| 55 | /// configuration to the [Table], but children are not required to be wrapped |
| 56 | /// in [TableCell] widgets. |
| 57 | final List<Widget> children; |
| 58 | |
| 59 | @override |
| 60 | String toString() { |
| 61 | final StringBuffer result = StringBuffer(); |
| 62 | result.write('TableRow(' ); |
| 63 | if (key != null) { |
| 64 | result.write(' $key, ' ); |
| 65 | } |
| 66 | if (decoration != null) { |
| 67 | result.write(' $decoration, ' ); |
| 68 | } |
| 69 | if (children.isEmpty) { |
| 70 | result.write('no children' ); |
| 71 | } else { |
| 72 | result.write(' $children' ); |
| 73 | } |
| 74 | result.write(')' ); |
| 75 | return result.toString(); |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | class _TableElementRow { |
| 80 | const _TableElementRow({this.key, required this.children}); |
| 81 | final LocalKey? key; |
| 82 | final List<Element> children; |
| 83 | } |
| 84 | |
| 85 | /// A widget that uses the table layout algorithm for its children. |
| 86 | /// |
| 87 | /// {@youtube 560 315 https://www.youtube.com/watch?v=_lbE0wsVZSw} |
| 88 | /// |
| 89 | /// {@tool dartpad} |
| 90 | /// This sample shows a [Table] with borders, multiple types of column widths |
| 91 | /// and different vertical cell alignments. |
| 92 | /// |
| 93 | /// ** See code in examples/api/lib/widgets/table/table.0.dart ** |
| 94 | /// {@end-tool} |
| 95 | /// |
| 96 | /// If you only have one row, the [Row] widget is more appropriate. If you only |
| 97 | /// have one column, the [SliverList] or [Column] widgets will be more |
| 98 | /// appropriate. |
| 99 | /// |
| 100 | /// Rows size vertically based on their contents. To control the individual |
| 101 | /// column widths, use the [columnWidths] property to specify a |
| 102 | /// [TableColumnWidth] for each column. If [columnWidths] is null, or there is a |
| 103 | /// null entry for a given column in [columnWidths], the table uses the |
| 104 | /// [defaultColumnWidth] instead. |
| 105 | /// |
| 106 | /// By default, [defaultColumnWidth] is a [FlexColumnWidth]. This |
| 107 | /// [TableColumnWidth] divides up the remaining space in the horizontal axis to |
| 108 | /// determine the column width. If wrapping a [Table] in a horizontal |
| 109 | /// [ScrollView], choose a different [TableColumnWidth], such as |
| 110 | /// [FixedColumnWidth]. |
| 111 | /// |
| 112 | /// For more details about the table layout algorithm, see [RenderTable]. |
| 113 | /// To control the alignment of children, see [TableCell]. |
| 114 | /// |
| 115 | /// See also: |
| 116 | /// |
| 117 | /// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/). |
| 118 | class Table extends RenderObjectWidget { |
| 119 | /// Creates a table. |
| 120 | Table({ |
| 121 | super.key, |
| 122 | this.children = const <TableRow>[], |
| 123 | this.columnWidths, |
| 124 | this.defaultColumnWidth = const FlexColumnWidth(), |
| 125 | this.textDirection, |
| 126 | this.border, |
| 127 | this.defaultVerticalAlignment = TableCellVerticalAlignment.top, |
| 128 | this.textBaseline, // NO DEFAULT: we don't know what the text's baseline should be |
| 129 | }) : assert( |
| 130 | defaultVerticalAlignment != TableCellVerticalAlignment.baseline || textBaseline != null, |
| 131 | 'textBaseline is required if you specify the defaultVerticalAlignment with TableCellVerticalAlignment.baseline' , |
| 132 | ), |
| 133 | assert(() { |
| 134 | if (children.any( |
| 135 | (TableRow row1) => |
| 136 | row1.key != null && |
| 137 | children.any((TableRow row2) => row1 != row2 && row1.key == row2.key), |
| 138 | )) { |
| 139 | throw FlutterError( |
| 140 | 'Two or more TableRow children of this Table had the same key.\n' |
| 141 | 'All the keyed TableRow children of a Table must have different Keys.' , |
| 142 | ); |
| 143 | } |
| 144 | return true; |
| 145 | }()), |
| 146 | assert(() { |
| 147 | if (children.isNotEmpty) { |
| 148 | final int cellCount = children.first.children.length; |
| 149 | if (children.any((TableRow row) => row.children.length != cellCount)) { |
| 150 | throw FlutterError( |
| 151 | 'Table contains irregular row lengths.\n' |
| 152 | 'Every TableRow in a Table must have the same number of children, so that every cell is filled. ' |
| 153 | 'Otherwise, the table will contain holes.' , |
| 154 | ); |
| 155 | } |
| 156 | if (children.any((TableRow row) => row.children.isEmpty)) { |
| 157 | throw FlutterError( |
| 158 | 'One or more TableRow have no children.\n' |
| 159 | 'Every TableRow in a Table must have at least one child, so there is no empty row. ' , |
| 160 | ); |
| 161 | } |
| 162 | } |
| 163 | return true; |
| 164 | }()), |
| 165 | _rowDecorations = children.any((TableRow row) => row.decoration != null) |
| 166 | ? children.map<Decoration?>((TableRow row) => row.decoration).toList(growable: false) |
| 167 | : null { |
| 168 | assert(() { |
| 169 | final List<Widget> flatChildren = children |
| 170 | .expand<Widget>((TableRow row) => row.children) |
| 171 | .toList(growable: false); |
| 172 | return !debugChildrenHaveDuplicateKeys( |
| 173 | this, |
| 174 | flatChildren, |
| 175 | message: |
| 176 | 'Two or more cells in this Table contain widgets with the same key.\n' |
| 177 | 'Every widget child of every TableRow in a Table must have different keys. The cells of a Table are ' |
| 178 | 'flattened out for processing, so separate cells cannot have duplicate keys even if they are in ' |
| 179 | 'different rows.' , |
| 180 | ); |
| 181 | }()); |
| 182 | } |
| 183 | |
| 184 | /// The rows of the table. |
| 185 | /// |
| 186 | /// Every row in a table must have the same number of children. |
| 187 | final List<TableRow> children; |
| 188 | |
| 189 | /// How the horizontal extents of the columns of this table should be determined. |
| 190 | /// |
| 191 | /// If the [Map] has a null entry for a given column, the table uses the |
| 192 | /// [defaultColumnWidth] instead. By default, that uses flex sizing to |
| 193 | /// distribute free space equally among the columns. |
| 194 | /// |
| 195 | /// The [FixedColumnWidth] class can be used to specify a specific width in |
| 196 | /// pixels. That is the cheapest way to size a table's columns. |
| 197 | /// |
| 198 | /// The layout performance of the table depends critically on which column |
| 199 | /// sizing algorithms are used here. In particular, [IntrinsicColumnWidth] is |
| 200 | /// quite expensive because it needs to measure each cell in the column to |
| 201 | /// determine the intrinsic size of the column. |
| 202 | /// |
| 203 | /// The keys of this map (column indexes) are zero-based. |
| 204 | /// |
| 205 | /// If this is set to null, then an empty map is assumed. |
| 206 | final Map<int, TableColumnWidth>? columnWidths; |
| 207 | |
| 208 | /// How to determine with widths of columns that don't have an explicit sizing |
| 209 | /// algorithm. |
| 210 | /// |
| 211 | /// Specifically, the [defaultColumnWidth] is used for column `i` if |
| 212 | /// `columnWidths[i]` is null. Defaults to [FlexColumnWidth], which will |
| 213 | /// divide the remaining horizontal space up evenly between columns of the |
| 214 | /// same type [TableColumnWidth]. |
| 215 | /// |
| 216 | /// A [Table] in a horizontal [ScrollView] must use a [FixedColumnWidth], or |
| 217 | /// an [IntrinsicColumnWidth] as the horizontal space is infinite. |
| 218 | final TableColumnWidth defaultColumnWidth; |
| 219 | |
| 220 | /// The direction in which the columns are ordered. |
| 221 | /// |
| 222 | /// Defaults to the ambient [Directionality]. |
| 223 | final TextDirection? textDirection; |
| 224 | |
| 225 | /// The style to use when painting the boundary and interior divisions of the table. |
| 226 | final TableBorder? border; |
| 227 | |
| 228 | /// How cells that do not explicitly specify a vertical alignment are aligned vertically. |
| 229 | /// |
| 230 | /// Cells may specify a vertical alignment by wrapping their contents in a |
| 231 | /// [TableCell] widget. |
| 232 | final TableCellVerticalAlignment defaultVerticalAlignment; |
| 233 | |
| 234 | /// The text baseline to use when aligning rows using [TableCellVerticalAlignment.baseline]. |
| 235 | /// |
| 236 | /// This must be set if using baseline alignment. There is no default because there is no |
| 237 | /// way for the framework to know the correct baseline _a priori_. |
| 238 | final TextBaseline? textBaseline; |
| 239 | |
| 240 | final List<Decoration?>? _rowDecorations; |
| 241 | |
| 242 | @override |
| 243 | RenderObjectElement createElement() => _TableElement(this); |
| 244 | |
| 245 | @override |
| 246 | RenderTable createRenderObject(BuildContext context) { |
| 247 | assert(debugCheckHasDirectionality(context)); |
| 248 | return RenderTable( |
| 249 | columns: children.isNotEmpty ? children[0].children.length : 0, |
| 250 | rows: children.length, |
| 251 | columnWidths: columnWidths, |
| 252 | defaultColumnWidth: defaultColumnWidth, |
| 253 | textDirection: textDirection ?? Directionality.of(context), |
| 254 | border: border, |
| 255 | rowDecorations: _rowDecorations, |
| 256 | configuration: createLocalImageConfiguration(context), |
| 257 | defaultVerticalAlignment: defaultVerticalAlignment, |
| 258 | textBaseline: textBaseline, |
| 259 | ); |
| 260 | } |
| 261 | |
| 262 | @override |
| 263 | void updateRenderObject(BuildContext context, RenderTable renderObject) { |
| 264 | assert(debugCheckHasDirectionality(context)); |
| 265 | assert(renderObject.columns == (children.isNotEmpty ? children[0].children.length : 0)); |
| 266 | assert(renderObject.rows == children.length); |
| 267 | renderObject |
| 268 | ..columnWidths = columnWidths |
| 269 | ..defaultColumnWidth = defaultColumnWidth |
| 270 | ..textDirection = textDirection ?? Directionality.of(context) |
| 271 | ..border = border |
| 272 | ..rowDecorations = _rowDecorations |
| 273 | ..configuration = createLocalImageConfiguration(context) |
| 274 | ..defaultVerticalAlignment = defaultVerticalAlignment |
| 275 | ..textBaseline = textBaseline; |
| 276 | } |
| 277 | } |
| 278 | |
| 279 | class _TableElement extends RenderObjectElement { |
| 280 | _TableElement(Table super.widget); |
| 281 | |
| 282 | @override |
| 283 | RenderTable get renderObject => super.renderObject as RenderTable; |
| 284 | |
| 285 | List<_TableElementRow> _children = const <_TableElementRow>[]; |
| 286 | |
| 287 | bool _doingMountOrUpdate = false; |
| 288 | |
| 289 | @override |
| 290 | void mount(Element? parent, Object? newSlot) { |
| 291 | assert(!_doingMountOrUpdate); |
| 292 | _doingMountOrUpdate = true; |
| 293 | super.mount(parent, newSlot); |
| 294 | int rowIndex = -1; |
| 295 | _children = (widget as Table).children |
| 296 | .map<_TableElementRow>((TableRow row) { |
| 297 | int columnIndex = 0; |
| 298 | rowIndex += 1; |
| 299 | return _TableElementRow( |
| 300 | key: row.key, |
| 301 | children: row.children |
| 302 | .map<Element>((Widget child) { |
| 303 | return inflateWidget(child, _TableSlot(columnIndex++, rowIndex)); |
| 304 | }) |
| 305 | .toList(growable: false), |
| 306 | ); |
| 307 | }) |
| 308 | .toList(growable: false); |
| 309 | _updateRenderObjectChildren(); |
| 310 | assert(_doingMountOrUpdate); |
| 311 | _doingMountOrUpdate = false; |
| 312 | } |
| 313 | |
| 314 | @override |
| 315 | void insertRenderObjectChild(RenderBox child, _TableSlot slot) { |
| 316 | renderObject.setupParentData(child); |
| 317 | // Once [mount]/[update] are done, the children are getting set all at once |
| 318 | // in [_updateRenderObjectChildren]. |
| 319 | if (!_doingMountOrUpdate) { |
| 320 | renderObject.setChild(slot.column, slot.row, child); |
| 321 | } |
| 322 | } |
| 323 | |
| 324 | @override |
| 325 | void moveRenderObjectChild(RenderBox child, _TableSlot oldSlot, _TableSlot newSlot) { |
| 326 | assert(_doingMountOrUpdate); |
| 327 | // Child gets moved at the end of [update] in [_updateRenderObjectChildren]. |
| 328 | } |
| 329 | |
| 330 | @override |
| 331 | void removeRenderObjectChild(RenderBox child, _TableSlot slot) { |
| 332 | renderObject.setChild(slot.column, slot.row, null); |
| 333 | } |
| 334 | |
| 335 | final Set<Element> _forgottenChildren = HashSet<Element>(); |
| 336 | |
| 337 | @override |
| 338 | void update(Table newWidget) { |
| 339 | assert(!_doingMountOrUpdate); |
| 340 | _doingMountOrUpdate = true; |
| 341 | final Map<LocalKey, List<Element>> oldKeyedRows = <LocalKey, List<Element>>{}; |
| 342 | for (final _TableElementRow row in _children) { |
| 343 | if (row.key != null) { |
| 344 | oldKeyedRows[row.key!] = row.children; |
| 345 | } |
| 346 | } |
| 347 | final Iterator<_TableElementRow> oldUnkeyedRows = _children |
| 348 | .where((_TableElementRow row) => row.key == null) |
| 349 | .iterator; |
| 350 | final List<_TableElementRow> newChildren = <_TableElementRow>[]; |
| 351 | final Set<List<Element>> taken = <List<Element>>{}; |
| 352 | for (int rowIndex = 0; rowIndex < newWidget.children.length; rowIndex++) { |
| 353 | final TableRow row = newWidget.children[rowIndex]; |
| 354 | List<Element> oldChildren; |
| 355 | if (row.key != null && oldKeyedRows.containsKey(row.key)) { |
| 356 | oldChildren = oldKeyedRows[row.key]!; |
| 357 | taken.add(oldChildren); |
| 358 | } else if (row.key == null && oldUnkeyedRows.moveNext()) { |
| 359 | oldChildren = oldUnkeyedRows.current.children; |
| 360 | } else { |
| 361 | oldChildren = const <Element>[]; |
| 362 | } |
| 363 | final List<_TableSlot> slots = List<_TableSlot>.generate( |
| 364 | row.children.length, |
| 365 | (int columnIndex) => _TableSlot(columnIndex, rowIndex), |
| 366 | ); |
| 367 | newChildren.add( |
| 368 | _TableElementRow( |
| 369 | key: row.key, |
| 370 | children: updateChildren( |
| 371 | oldChildren, |
| 372 | row.children, |
| 373 | forgottenChildren: _forgottenChildren, |
| 374 | slots: slots, |
| 375 | ), |
| 376 | ), |
| 377 | ); |
| 378 | } |
| 379 | while (oldUnkeyedRows.moveNext()) { |
| 380 | updateChildren( |
| 381 | oldUnkeyedRows.current.children, |
| 382 | const <Widget>[], |
| 383 | forgottenChildren: _forgottenChildren, |
| 384 | ); |
| 385 | } |
| 386 | for (final List<Element> oldChildren in oldKeyedRows.values.where( |
| 387 | (List<Element> list) => !taken.contains(list), |
| 388 | )) { |
| 389 | updateChildren(oldChildren, const <Widget>[], forgottenChildren: _forgottenChildren); |
| 390 | } |
| 391 | |
| 392 | _children = newChildren; |
| 393 | _updateRenderObjectChildren(); |
| 394 | _forgottenChildren.clear(); |
| 395 | super.update(newWidget); |
| 396 | assert(widget == newWidget); |
| 397 | assert(_doingMountOrUpdate); |
| 398 | _doingMountOrUpdate = false; |
| 399 | } |
| 400 | |
| 401 | void _updateRenderObjectChildren() { |
| 402 | renderObject.setFlatChildren( |
| 403 | _children.isNotEmpty ? _children[0].children.length : 0, |
| 404 | _children.expand<RenderBox>((_TableElementRow row) { |
| 405 | return row.children.map<RenderBox>((Element child) { |
| 406 | final RenderBox box = child.renderObject! as RenderBox; |
| 407 | return box; |
| 408 | }); |
| 409 | }).toList(), |
| 410 | ); |
| 411 | } |
| 412 | |
| 413 | @override |
| 414 | void visitChildren(ElementVisitor visitor) { |
| 415 | for (final Element child in _children.expand<Element>((_TableElementRow row) => row.children)) { |
| 416 | if (!_forgottenChildren.contains(child)) { |
| 417 | visitor(child); |
| 418 | } |
| 419 | } |
| 420 | } |
| 421 | |
| 422 | @override |
| 423 | bool forgetChild(Element child) { |
| 424 | _forgottenChildren.add(child); |
| 425 | super.forgetChild(child); |
| 426 | return true; |
| 427 | } |
| 428 | } |
| 429 | |
| 430 | /// A widget that controls how a child of a [Table] is aligned. |
| 431 | /// |
| 432 | /// A [TableCell] widget must be a descendant of a [Table], and the path from |
| 433 | /// the [TableCell] widget to its enclosing [Table] must contain only |
| 434 | /// [TableRow]s, [StatelessWidget]s, or [StatefulWidget]s (not |
| 435 | /// other kinds of widgets, like [RenderObjectWidget]s). |
| 436 | /// |
| 437 | /// To create an empty [TableCell], provide a [SizedBox.shrink] |
| 438 | /// as the [child]. |
| 439 | class TableCell extends StatelessWidget { |
| 440 | /// Creates a widget that controls how a child of a [Table] is aligned. |
| 441 | const TableCell({super.key, this.verticalAlignment, required this.child}); |
| 442 | |
| 443 | /// How this cell is aligned vertically. |
| 444 | final TableCellVerticalAlignment? verticalAlignment; |
| 445 | |
| 446 | /// The child of this cell. |
| 447 | final Widget child; |
| 448 | |
| 449 | @override |
| 450 | Widget build(BuildContext context) { |
| 451 | return _TableCell( |
| 452 | verticalAlignment: verticalAlignment, |
| 453 | child: Semantics(role: SemanticsRole.cell, child: child), |
| 454 | ); |
| 455 | } |
| 456 | } |
| 457 | |
| 458 | class _TableCell extends ParentDataWidget<TableCellParentData> { |
| 459 | const _TableCell({this.verticalAlignment, required super.child}); |
| 460 | |
| 461 | final TableCellVerticalAlignment? verticalAlignment; |
| 462 | |
| 463 | @override |
| 464 | void applyParentData(RenderObject renderObject) { |
| 465 | final TableCellParentData parentData = renderObject.parentData! as TableCellParentData; |
| 466 | if (parentData.verticalAlignment != verticalAlignment) { |
| 467 | parentData.verticalAlignment = verticalAlignment; |
| 468 | renderObject.parent?.markNeedsLayout(); |
| 469 | } |
| 470 | } |
| 471 | |
| 472 | @override |
| 473 | Type get debugTypicalAncestorWidgetClass => Table; |
| 474 | |
| 475 | @override |
| 476 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| 477 | super.debugFillProperties(properties); |
| 478 | properties.add( |
| 479 | EnumProperty<TableCellVerticalAlignment>('verticalAlignment' , verticalAlignment), |
| 480 | ); |
| 481 | } |
| 482 | } |
| 483 | |
| 484 | @immutable |
| 485 | class _TableSlot with Diagnosticable { |
| 486 | const _TableSlot(this.column, this.row); |
| 487 | |
| 488 | final int column; |
| 489 | final int row; |
| 490 | |
| 491 | @override |
| 492 | bool operator ==(Object other) { |
| 493 | if (other.runtimeType != runtimeType) { |
| 494 | return false; |
| 495 | } |
| 496 | return other is _TableSlot && column == other.column && row == other.row; |
| 497 | } |
| 498 | |
| 499 | @override |
| 500 | int get hashCode => Object.hash(column, row); |
| 501 | |
| 502 | @override |
| 503 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| 504 | super.debugFillProperties(properties); |
| 505 | properties.add(IntProperty('x' , column)); |
| 506 | properties.add(IntProperty('y' , row)); |
| 507 | } |
| 508 | } |
| 509 | |