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