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