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 = |
166 | children.any((TableRow row) => row.decoration != null) |
167 | ? children.map<Decoration?>((TableRow row) => row.decoration).toList(growable: false) |
168 | : null { |
169 | assert(() { |
170 | final List<Widget> flatChildren = children |
171 | .expand<Widget>((TableRow row) => row.children) |
172 | .toList(growable: false); |
173 | return !debugChildrenHaveDuplicateKeys( |
174 | this, |
175 | flatChildren, |
176 | message: |
177 | 'Two or more cells in this Table contain widgets with the same key.\n' |
178 | 'Every widget child of every TableRow in a Table must have different keys. The cells of a Table are ' |
179 | 'flattened out for processing, so separate cells cannot have duplicate keys even if they are in ' |
180 | 'different rows.', |
181 | ); |
182 | }()); |
183 | } |
184 | |
185 | /// The rows of the table. |
186 | /// |
187 | /// Every row in a table must have the same number of children. |
188 | final List<TableRow> children; |
189 | |
190 | /// How the horizontal extents of the columns of this table should be determined. |
191 | /// |
192 | /// If the [Map] has a null entry for a given column, the table uses the |
193 | /// [defaultColumnWidth] instead. By default, that uses flex sizing to |
194 | /// distribute free space equally among the columns. |
195 | /// |
196 | /// The [FixedColumnWidth] class can be used to specify a specific width in |
197 | /// pixels. That is the cheapest way to size a table's columns. |
198 | /// |
199 | /// The layout performance of the table depends critically on which column |
200 | /// sizing algorithms are used here. In particular, [IntrinsicColumnWidth] is |
201 | /// quite expensive because it needs to measure each cell in the column to |
202 | /// determine the intrinsic size of the column. |
203 | /// |
204 | /// The keys of this map (column indexes) are zero-based. |
205 | /// |
206 | /// If this is set to null, then an empty map is assumed. |
207 | final Map<int, TableColumnWidth>? columnWidths; |
208 | |
209 | /// How to determine with widths of columns that don't have an explicit sizing |
210 | /// algorithm. |
211 | /// |
212 | /// Specifically, the [defaultColumnWidth] is used for column `i` if |
213 | /// `columnWidths[i]` is null. Defaults to [FlexColumnWidth], which will |
214 | /// divide the remaining horizontal space up evenly between columns of the |
215 | /// same type [TableColumnWidth]. |
216 | /// |
217 | /// A [Table] in a horizontal [ScrollView] must use a [FixedColumnWidth], or |
218 | /// an [IntrinsicColumnWidth] as the horizontal space is infinite. |
219 | final TableColumnWidth defaultColumnWidth; |
220 | |
221 | /// The direction in which the columns are ordered. |
222 | /// |
223 | /// Defaults to the ambient [Directionality]. |
224 | final TextDirection? textDirection; |
225 | |
226 | /// The style to use when painting the boundary and interior divisions of the table. |
227 | final TableBorder? border; |
228 | |
229 | /// How cells that do not explicitly specify a vertical alignment are aligned vertically. |
230 | /// |
231 | /// Cells may specify a vertical alignment by wrapping their contents in a |
232 | /// [TableCell] widget. |
233 | final TableCellVerticalAlignment defaultVerticalAlignment; |
234 | |
235 | /// The text baseline to use when aligning rows using [TableCellVerticalAlignment.baseline]. |
236 | /// |
237 | /// This must be set if using baseline alignment. There is no default because there is no |
238 | /// way for the framework to know the correct baseline _a priori_. |
239 | final TextBaseline? textBaseline; |
240 | |
241 | final List<Decoration?>? _rowDecorations; |
242 | |
243 | @override |
244 | RenderObjectElement createElement() => _TableElement(this); |
245 | |
246 | @override |
247 | RenderTable createRenderObject(BuildContext context) { |
248 | assert(debugCheckHasDirectionality(context)); |
249 | return RenderTable( |
250 | columns: children.isNotEmpty ? children[0].children.length : 0, |
251 | rows: children.length, |
252 | columnWidths: columnWidths, |
253 | defaultColumnWidth: defaultColumnWidth, |
254 | textDirection: textDirection ?? Directionality.of(context), |
255 | border: border, |
256 | rowDecorations: _rowDecorations, |
257 | configuration: createLocalImageConfiguration(context), |
258 | defaultVerticalAlignment: defaultVerticalAlignment, |
259 | textBaseline: textBaseline, |
260 | ); |
261 | } |
262 | |
263 | @override |
264 | void updateRenderObject(BuildContext context, RenderTable renderObject) { |
265 | assert(debugCheckHasDirectionality(context)); |
266 | assert(renderObject.columns == (children.isNotEmpty ? children[0].children.length : 0)); |
267 | assert(renderObject.rows == children.length); |
268 | renderObject |
269 | ..columnWidths = columnWidths |
270 | ..defaultColumnWidth = defaultColumnWidth |
271 | ..textDirection = textDirection ?? Directionality.of(context) |
272 | ..border = border |
273 | ..rowDecorations = _rowDecorations |
274 | ..configuration = createLocalImageConfiguration(context) |
275 | ..defaultVerticalAlignment = defaultVerticalAlignment |
276 | ..textBaseline = textBaseline; |
277 | } |
278 | } |
279 | |
280 | class _TableElement extends RenderObjectElement { |
281 | _TableElement(Table super.widget); |
282 | |
283 | @override |
284 | RenderTable get renderObject => super.renderObject as RenderTable; |
285 | |
286 | List<_TableElementRow> _children = const <_TableElementRow>[]; |
287 | |
288 | bool _doingMountOrUpdate = false; |
289 | |
290 | @override |
291 | void mount(Element? parent, Object? newSlot) { |
292 | assert(!_doingMountOrUpdate); |
293 | _doingMountOrUpdate = true; |
294 | super.mount(parent, newSlot); |
295 | int rowIndex = -1; |
296 | _children = (widget as Table).children |
297 | .map<_TableElementRow>((TableRow row) { |
298 | int columnIndex = 0; |
299 | rowIndex += 1; |
300 | return _TableElementRow( |
301 | key: row.key, |
302 | children: row.children |
303 | .map<Element>((Widget child) { |
304 | return inflateWidget(child, _TableSlot(columnIndex++, rowIndex)); |
305 | }) |
306 | .toList(growable: false), |
307 | ); |
308 | }) |
309 | .toList(growable: false); |
310 | _updateRenderObjectChildren(); |
311 | assert(_doingMountOrUpdate); |
312 | _doingMountOrUpdate = false; |
313 | } |
314 | |
315 | @override |
316 | void insertRenderObjectChild(RenderBox child, _TableSlot slot) { |
317 | renderObject.setupParentData(child); |
318 | // Once [mount]/[update] are done, the children are getting set all at once |
319 | // in [_updateRenderObjectChildren]. |
320 | if (!_doingMountOrUpdate) { |
321 | renderObject.setChild(slot.column, slot.row, child); |
322 | } |
323 | } |
324 | |
325 | @override |
326 | void moveRenderObjectChild(RenderBox child, _TableSlot oldSlot, _TableSlot newSlot) { |
327 | assert(_doingMountOrUpdate); |
328 | // Child gets moved at the end of [update] in [_updateRenderObjectChildren]. |
329 | } |
330 | |
331 | @override |
332 | void removeRenderObjectChild(RenderBox child, _TableSlot slot) { |
333 | renderObject.setChild(slot.column, slot.row, null); |
334 | } |
335 | |
336 | final Set<Element> _forgottenChildren = HashSet<Element>(); |
337 | |
338 | @override |
339 | void update(Table newWidget) { |
340 | assert(!_doingMountOrUpdate); |
341 | _doingMountOrUpdate = true; |
342 | final Map<LocalKey, List<Element>> oldKeyedRows = <LocalKey, List<Element>>{}; |
343 | for (final _TableElementRow row in _children) { |
344 | if (row.key != null) { |
345 | oldKeyedRows[row.key!] = row.children; |
346 | } |
347 | } |
348 | final Iterator<_TableElementRow> oldUnkeyedRows = |
349 | _children.where((_TableElementRow row) => row.key == null).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 |
Definitions
- TableRow
- TableRow
- toString
- _TableElementRow
- _TableElementRow
- Table
- Table
- createElement
- createRenderObject
- updateRenderObject
- _TableElement
- _TableElement
- renderObject
- mount
- insertRenderObjectChild
- moveRenderObjectChild
- removeRenderObjectChild
- update
- _updateRenderObjectChildren
- visitChildren
- forgetChild
- TableCell
- TableCell
- build
- _TableCell
- _TableCell
- applyParentData
- debugTypicalAncestorWidgetClass
- debugFillProperties
- _TableSlot
- _TableSlot
- ==
- hashCode
Learn more about Flutter for embedded and desktop on industrialflutter.com