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';
7library;
8
9import 'dart:collection';
10
11import 'package:flutter/foundation.dart';
12import 'package:flutter/rendering.dart';
13
14import 'basic.dart';
15import 'debug.dart';
16import 'framework.dart';
17import 'image.dart';
18
19export '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
38class 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
79class _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/).
118class 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
280class _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].
439class 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
458class _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
485class _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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com