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 'paginated_data_table.dart'; |
6 | /// @docImport 'text_theme.dart'; |
7 | library; |
8 | |
9 | import 'dart:math' as math; |
10 | |
11 | import 'package:flutter/rendering.dart'; |
12 | import 'package:flutter/widgets.dart'; |
13 | |
14 | import 'checkbox.dart'; |
15 | import 'constants.dart'; |
16 | import 'data_table_theme.dart'; |
17 | import 'debug.dart'; |
18 | import 'divider.dart'; |
19 | import 'dropdown.dart'; |
20 | import 'icons.dart'; |
21 | import 'ink_well.dart'; |
22 | import 'material.dart'; |
23 | import 'material_state.dart'; |
24 | import 'theme.dart'; |
25 | import 'tooltip.dart'; |
26 | |
27 | // Examples can assume: |
28 | // late BuildContext context; |
29 | // late List |
30 | // late List |
31 | |
32 | /// Signature for [DataColumn.onSort] callback. |
33 | typedef DataColumnSortCallback = void Function(int columnIndex, bool ascending); |
34 | |
35 | /// Column configuration for a [DataTable]. |
36 | /// |
37 | /// One column configuration must be provided for each column to |
38 | /// display in the table. The list of [DataColumn] objects is passed |
39 | /// as the `columns` argument to the [DataTable.new] constructor. |
40 | @immutable |
41 | class DataColumn { |
42 | /// Creates the configuration for a column of a [DataTable]. |
43 | const DataColumn({ |
44 | required this.label, |
45 | this.columnWidth, |
46 | this.tooltip, |
47 | this.numeric = false, |
48 | this.onSort, |
49 | this.mouseCursor, |
50 | this.headingRowAlignment, |
51 | }); |
52 | |
53 | /// The column heading. |
54 | /// |
55 | /// Typically, this will be a [Text] widget. It could also be an |
56 | /// [Icon] (typically using size 18), or a [Row] with an icon and |
57 | /// some text. |
58 | /// |
59 | /// The [label] is placed within a [Row] along with the |
60 | /// sort indicator (if applicable). By default, [label] only occupy minimal |
61 | /// space. It is recommended to place the label content in an [Expanded] or |
62 | /// [Flexible] as [label] to control how the content flexes. Otherwise, |
63 | /// an exception will occur when the available space is insufficient. |
64 | /// |
65 | /// By default, [DefaultTextStyle.softWrap] of this subtree will be set to false. |
66 | /// Use [DefaultTextStyle.merge] to override it if needed. |
67 | /// |
68 | /// The label should not include the sort indicator. |
69 | final Widget label; |
70 | |
71 | /// How the horizontal extents of this column of the table should be determined. |
72 | /// |
73 | /// The [FixedColumnWidth] class can be used to specify a specific width in |
74 | /// pixels. This is the cheapest way to size a table's columns. |
75 | /// |
76 | /// The layout performance of the table depends critically on which column |
77 | /// sizing algorithms are used here. In particular, [IntrinsicColumnWidth] is |
78 | /// quite expensive because it needs to measure each cell in the column to |
79 | /// determine the intrinsic size of the column. |
80 | /// |
81 | /// If this property is `null`, the table applies a default behavior: |
82 | /// - If the table has exactly one column identified as the only text column |
83 | /// (i.e., all the rest are numeric), that column uses `IntrinsicColumnWidth(flex: 1.0)`. |
84 | /// - All other columns use `IntrinsicColumnWidth()`. |
85 | final TableColumnWidth? columnWidth; |
86 | |
87 | /// The column heading's tooltip. |
88 | /// |
89 | /// This is a longer description of the column heading, for cases |
90 | /// where the heading might have been abbreviated to keep the column |
91 | /// width to a reasonable size. |
92 | final String? tooltip; |
93 | |
94 | /// Whether this column represents numeric data or not. |
95 | /// |
96 | /// The contents of cells of columns containing numeric data are |
97 | /// right-aligned. |
98 | final bool numeric; |
99 | |
100 | /// Called when the user asks to sort the table using this column. |
101 | /// |
102 | /// If null, the column will not be considered sortable. |
103 | /// |
104 | /// See [DataTable.sortColumnIndex] and [DataTable.sortAscending]. |
105 | final DataColumnSortCallback? onSort; |
106 | |
107 | bool get _debugInteractive => onSort != null; |
108 | |
109 | /// The cursor for a mouse pointer when it enters or is hovering over the |
110 | /// heading row. |
111 | /// |
112 | /// [WidgetStateProperty.resolve] is used for the following [WidgetState]s: |
113 | /// |
114 | /// * [WidgetState.disabled]. |
115 | /// |
116 | /// If this is null, then the value of [DataTableThemeData.headingCellCursor] |
117 | /// is used. If that's null, then [WidgetStateMouseCursor.clickable] is used. |
118 | /// |
119 | /// See also: |
120 | /// * [WidgetStateMouseCursor], which can be used to create a [MouseCursor]. |
121 | final MaterialStateProperty<MouseCursor?>? mouseCursor; |
122 | |
123 | /// Defines the horizontal layout of the [label] and sort indicator in the |
124 | /// heading row. |
125 | /// |
126 | /// If [headingRowAlignment] value is [MainAxisAlignment.center] and [onSort] is |
127 | /// not null, then a [SizedBox] with a width of sort arrow icon size and sort |
128 | /// arrow padding will be placed before the [label] to ensure the label is |
129 | /// centered in the column. |
130 | /// |
131 | /// If null, then defaults to [MainAxisAlignment.start]. |
132 | final MainAxisAlignment? headingRowAlignment; |
133 | } |
134 | |
135 | /// Row configuration and cell data for a [DataTable]. |
136 | /// |
137 | /// One row configuration must be provided for each row to |
138 | /// display in the table. The list of [DataRow] objects is passed |
139 | /// as the `rows` argument to the [DataTable.new] constructor. |
140 | /// |
141 | /// The data for this row of the table is provided in the [cells] |
142 | /// property of the [DataRow] object. |
143 | @immutable |
144 | class DataRow { |
145 | /// Creates the configuration for a row of a [DataTable]. |
146 | const DataRow({ |
147 | this.key, |
148 | this.selected = false, |
149 | this.onSelectChanged, |
150 | this.onLongPress, |
151 | this.color, |
152 | this.mouseCursor, |
153 | required this.cells, |
154 | }); |
155 | |
156 | /// Creates the configuration for a row of a [DataTable], deriving |
157 | /// the key from a row index. |
158 | DataRow.byIndex({ |
159 | int? index, |
160 | this.selected = false, |
161 | this.onSelectChanged, |
162 | this.onLongPress, |
163 | this.color, |
164 | this.mouseCursor, |
165 | required this.cells, |
166 | }) : key = ValueKey<int?>(index); |
167 | |
168 | /// A [Key] that uniquely identifies this row. This is used to |
169 | /// ensure that if a row is added or removed, any stateful widgets |
170 | /// related to this row (e.g. an in-progress checkbox animation) |
171 | /// remain on the right row visually. |
172 | /// |
173 | /// If the table never changes once created, no key is necessary. |
174 | final LocalKey? key; |
175 | |
176 | /// Called when the user selects or unselects a selectable row. |
177 | /// |
178 | /// If this is not null, then the row is selectable. The current |
179 | /// selection state of the row is given by [selected]. |
180 | /// |
181 | /// If any row is selectable, then the table's heading row will have |
182 | /// a checkbox that can be checked to select all selectable rows |
183 | /// (and which is checked if all the rows are selected), and each |
184 | /// subsequent row will have a checkbox to toggle just that row. |
185 | /// |
186 | /// A row whose [onSelectChanged] callback is null is ignored for |
187 | /// the purposes of determining the state of the "all" checkbox, |
188 | /// and its checkbox is disabled. |
189 | /// |
190 | /// If a [DataCell] in the row has its [DataCell.onTap] callback defined, |
191 | /// that callback behavior overrides the gesture behavior of the row for |
192 | /// that particular cell. |
193 | final ValueChanged<bool?>? onSelectChanged; |
194 | |
195 | /// Called if the row is long-pressed. |
196 | /// |
197 | /// If a [DataCell] in the row has its [DataCell.onTap], [DataCell.onDoubleTap], |
198 | /// [DataCell.onLongPress], [DataCell.onTapCancel] or [DataCell.onTapDown] callback defined, |
199 | /// that callback behavior overrides the gesture behavior of the row for |
200 | /// that particular cell. |
201 | final GestureLongPressCallback? onLongPress; |
202 | |
203 | /// Whether the row is selected. |
204 | /// |
205 | /// If [onSelectChanged] is non-null for any row in the table, then |
206 | /// a checkbox is shown at the start of each row. If the row is |
207 | /// selected (true), the checkbox will be checked and the row will |
208 | /// be highlighted. |
209 | /// |
210 | /// Otherwise, the checkbox, if present, will not be checked. |
211 | final bool selected; |
212 | |
213 | /// The data for this row. |
214 | /// |
215 | /// There must be exactly as many cells as there are columns in the |
216 | /// table. |
217 | final List<DataCell> cells; |
218 | |
219 | /// The color for the row. |
220 | /// |
221 | /// By default, the color is transparent unless selected. Selected rows has |
222 | /// a grey translucent color. |
223 | /// |
224 | /// The effective color can depend on the [WidgetState] state, if the |
225 | /// row is selected, pressed, hovered, focused, disabled or enabled. The |
226 | /// color is painted as an overlay to the row. To make sure that the row's |
227 | /// [InkWell] is visible (when pressed, hovered and focused), it is |
228 | /// recommended to use a translucent color. |
229 | /// |
230 | /// If [onSelectChanged] or [onLongPress] is null, the row's [InkWell] will be disabled. |
231 | /// |
232 | /// ```dart |
233 | /// DataRow( |
234 | /// color: WidgetStateProperty.resolveWith<Color?>((Set<WidgetState> states) { |
235 | /// if (states.contains(WidgetState.selected)) { |
236 | /// return Theme.of(context).colorScheme.primary.withOpacity(0.08); |
237 | /// } |
238 | /// return null; // Use the default value. |
239 | /// }), |
240 | /// cells: const <DataCell>[ |
241 | /// // ... |
242 | /// ], |
243 | /// ) |
244 | /// ``` |
245 | /// |
246 | /// See also: |
247 | /// |
248 | /// * The Material Design specification for overlay colors and how they |
249 | /// match a component's state: |
250 | /// <https://material.io/design/interaction/states.html#anatomy>. |
251 | final MaterialStateProperty<Color?>? color; |
252 | |
253 | /// The cursor for a mouse pointer when it enters or is hovering over the |
254 | /// data row. |
255 | /// |
256 | /// [WidgetStateProperty.resolve] is used for the following [WidgetState]s: |
257 | /// |
258 | /// * [WidgetState.selected]. |
259 | /// |
260 | /// If this is null, then the value of [DataTableThemeData.dataRowCursor] |
261 | /// is used. If that's null, then [WidgetStateMouseCursor.clickable] is used. |
262 | /// |
263 | /// See also: |
264 | /// * [WidgetStateMouseCursor], which can be used to create a [MouseCursor]. |
265 | final MaterialStateProperty<MouseCursor?>? mouseCursor; |
266 | |
267 | bool get _debugInteractive => |
268 | onSelectChanged != null || cells.any((DataCell cell) => cell._debugInteractive); |
269 | } |
270 | |
271 | /// The data for a cell of a [DataTable]. |
272 | /// |
273 | /// One list of [DataCell] objects must be provided for each [DataRow] |
274 | /// in the [DataTable], in the new [DataRow] constructor's `cells` |
275 | /// argument. |
276 | @immutable |
277 | class DataCell { |
278 | /// Creates an object to hold the data for a cell in a [DataTable]. |
279 | /// |
280 | /// The first argument is the widget to show for the cell, typically |
281 | /// a [Text] or [DropdownButton] widget. |
282 | /// |
283 | /// If the cell has no data, then a [Text] widget with placeholder |
284 | /// text should be provided instead, and then the [placeholder] |
285 | /// argument should be set to true. |
286 | const DataCell( |
287 | this.child, { |
288 | this.placeholder = false, |
289 | this.showEditIcon = false, |
290 | this.onTap, |
291 | this.onLongPress, |
292 | this.onTapDown, |
293 | this.onDoubleTap, |
294 | this.onTapCancel, |
295 | }); |
296 | |
297 | /// A cell that has no content and has zero width and height. |
298 | static const DataCell empty = DataCell(SizedBox.shrink()); |
299 | |
300 | /// The data for the row. |
301 | /// |
302 | /// Typically a [Text] widget or a [DropdownButton] widget. |
303 | /// |
304 | /// If the cell has no data, then a [Text] widget with placeholder |
305 | /// text should be provided instead, and [placeholder] should be set |
306 | /// to true. |
307 | /// |
308 | /// {@macro flutter.widgets.ProxyWidget.child} |
309 | final Widget child; |
310 | |
311 | /// Whether the [child] is actually a placeholder. |
312 | /// |
313 | /// If this is true, the default text style for the cell is changed |
314 | /// to be appropriate for placeholder text. |
315 | final bool placeholder; |
316 | |
317 | /// Whether to show an edit icon at the end of the cell. |
318 | /// |
319 | /// This does not make the cell actually editable; the caller must |
320 | /// implement editing behavior if desired (initiated from the |
321 | /// [onTap] callback). |
322 | /// |
323 | /// If this is set, [onTap] should also be set, otherwise tapping |
324 | /// the icon will have no effect. |
325 | final bool showEditIcon; |
326 | |
327 | /// Called if the cell is tapped. |
328 | /// |
329 | /// If non-null, tapping the cell will call this callback. If |
330 | /// null (including [onDoubleTap], [onLongPress], [onTapCancel] and [onTapDown]), |
331 | /// tapping the cell will attempt to select the row (if |
332 | /// [DataRow.onSelectChanged] is provided). |
333 | final GestureTapCallback? onTap; |
334 | |
335 | /// Called when the cell is double tapped. |
336 | /// |
337 | /// If non-null, tapping the cell will call this callback. If |
338 | /// null (including [onTap], [onLongPress], [onTapCancel] and [onTapDown]), |
339 | /// tapping the cell will attempt to select the row (if |
340 | /// [DataRow.onSelectChanged] is provided). |
341 | final GestureTapCallback? onDoubleTap; |
342 | |
343 | /// Called if the cell is long-pressed. |
344 | /// |
345 | /// If non-null, tapping the cell will invoke this callback. If |
346 | /// null (including [onDoubleTap], [onTap], [onTapCancel] and [onTapDown]), |
347 | /// tapping the cell will attempt to select the row (if |
348 | /// [DataRow.onSelectChanged] is provided). |
349 | final GestureLongPressCallback? onLongPress; |
350 | |
351 | /// Called if the cell is tapped down. |
352 | /// |
353 | /// If non-null, tapping the cell will call this callback. If |
354 | /// null (including [onTap] [onDoubleTap], [onLongPress] and [onTapCancel]), |
355 | /// tapping the cell will attempt to select the row (if |
356 | /// [DataRow.onSelectChanged] is provided). |
357 | final GestureTapDownCallback? onTapDown; |
358 | |
359 | /// Called if the user cancels a tap was started on cell. |
360 | /// |
361 | /// If non-null, canceling the tap gesture will invoke this callback. |
362 | /// If null (including [onTap], [onDoubleTap] and [onLongPress]), |
363 | /// tapping the cell will attempt to select the |
364 | /// row (if [DataRow.onSelectChanged] is provided). |
365 | final GestureTapCancelCallback? onTapCancel; |
366 | |
367 | bool get _debugInteractive => |
368 | onTap != null || |
369 | onDoubleTap != null || |
370 | onLongPress != null || |
371 | onTapDown != null || |
372 | onTapCancel != null; |
373 | } |
374 | |
375 | /// A data table that follows the |
376 | /// [Material 2](https://material.io/go/design-data-tables) |
377 | /// design specification. |
378 | /// |
379 | /// {@youtube 560 315 https://www.youtube.com/watch?v=ktTajqbhIcY} |
380 | /// |
381 | /// ## Performance considerations |
382 | /// |
383 | /// Columns are sized automatically based on the table's contents. |
384 | /// It's expensive to display large amounts of data with this widget, |
385 | /// since it must be measured twice: once to negotiate each column's |
386 | /// dimensions, and again when the table is laid out. |
387 | /// |
388 | /// A [SingleChildScrollView] mounts and paints the entire child, even |
389 | /// when only some of it is visible. For a table that effectively handles |
390 | /// large amounts of data, here are some other options to consider: |
391 | /// |
392 | /// * `TableView`, a widget from the |
393 | /// [two_dimensional_scrollables](https://pub.dev/packages/two_dimensional_scrollables) |
394 | /// package. |
395 | /// * [PaginatedDataTable], which automatically splits the data into |
396 | /// multiple pages. |
397 | /// * [CustomScrollView], for greater control over scrolling effects. |
398 | /// |
399 | /// {@tool dartpad} |
400 | /// This sample shows how to display a [DataTable] with three columns: name, age, and |
401 | /// role. The columns are defined by three [DataColumn] objects. The table |
402 | /// contains three rows of data for three example users, the data for which |
403 | /// is defined by three [DataRow] objects. |
404 | /// |
405 | ///  |
406 | /// |
407 | /// ** See code in examples/api/lib/material/data_table/data_table.0.dart ** |
408 | /// {@end-tool} |
409 | /// |
410 | /// |
411 | /// {@tool dartpad} |
412 | /// This sample shows how to display a [DataTable] with alternate colors per |
413 | /// row, and a custom color for when the row is selected. |
414 | /// |
415 | /// ** See code in examples/api/lib/material/data_table/data_table.1.dart ** |
416 | /// {@end-tool} |
417 | /// |
418 | /// [DataTable] can be sorted on the basis of any column in [columns] in |
419 | /// ascending or descending order. If [sortColumnIndex] is non-null, then the |
420 | /// table will be sorted by the values in the specified column. The boolean |
421 | /// [sortAscending] flag controls the sort order. |
422 | /// |
423 | /// See also: |
424 | /// |
425 | /// * [DataColumn], which describes a column in the data table. |
426 | /// * [DataRow], which contains the data for a row in the data table. |
427 | /// * [DataCell], which contains the data for a single cell in the data table. |
428 | /// * [PaginatedDataTable], which shows part of the data in a data table and |
429 | /// provides controls for paging through the remainder of the data. |
430 | /// * `TableView` from the |
431 | /// [two_dimensional_scrollables](https://pub.dev/packages/two_dimensional_scrollables) |
432 | /// package, for displaying large amounts of data without pagination. |
433 | /// * <https://material.io/go/design-data-tables> |
434 | class DataTable extends StatelessWidget { |
435 | /// Creates a widget describing a data table. |
436 | /// |
437 | /// The [columns] argument must be a list of as many [DataColumn] |
438 | /// objects as the table is to have columns, ignoring the leading |
439 | /// checkbox column if any. The [columns] argument must have a |
440 | /// length greater than zero. |
441 | /// |
442 | /// The [rows] argument must be a list of as many [DataRow] objects |
443 | /// as the table is to have rows, ignoring the leading heading row |
444 | /// that contains the column headings (derived from the [columns] |
445 | /// argument). There may be zero rows, but the rows argument must |
446 | /// not be null. |
447 | /// |
448 | /// Each [DataRow] object in [rows] must have as many [DataCell] |
449 | /// objects in the [DataRow.cells] list as the table has columns. |
450 | /// |
451 | /// If the table is sorted, the column that provides the current |
452 | /// primary key should be specified by index in [sortColumnIndex], 0 |
453 | /// meaning the first column in [columns], 1 being the next one, and |
454 | /// so forth. |
455 | /// |
456 | /// The actual sort order can be specified using [sortAscending]; if |
457 | /// the sort order is ascending, this should be true (the default), |
458 | /// otherwise it should be false. |
459 | DataTable({ |
460 | super.key, |
461 | required this.columns, |
462 | this.sortColumnIndex, |
463 | this.sortAscending = true, |
464 | this.onSelectAll, |
465 | this.decoration, |
466 | this.dataRowColor, |
467 | @Deprecated( |
468 | 'Migrate to use dataRowMinHeight and dataRowMaxHeight instead. ' |
469 | 'This feature was deprecated after v3.7.0-5.0.pre.', |
470 | ) |
471 | double? dataRowHeight, |
472 | double? dataRowMinHeight, |
473 | double? dataRowMaxHeight, |
474 | this.dataTextStyle, |
475 | this.headingRowColor, |
476 | this.headingRowHeight, |
477 | this.headingTextStyle, |
478 | this.horizontalMargin, |
479 | this.columnSpacing, |
480 | this.showCheckboxColumn = true, |
481 | this.showBottomBorder = false, |
482 | this.dividerThickness, |
483 | required this.rows, |
484 | this.checkboxHorizontalMargin, |
485 | this.border, |
486 | this.clipBehavior = Clip.none, |
487 | }) : assert(columns.isNotEmpty), |
488 | assert( |
489 | sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length), |
490 | ), |
491 | assert( |
492 | !rows.any((DataRow row) => row.cells.length != columns.length), |
493 | 'All rows must have the same number of cells as there are header cells (${columns.length} )', |
494 | ), |
495 | assert(dividerThickness == null || dividerThickness >= 0), |
496 | assert( |
497 | dataRowMinHeight == null || |
498 | dataRowMaxHeight == null || |
499 | dataRowMaxHeight >= dataRowMinHeight, |
500 | ), |
501 | assert( |
502 | dataRowHeight == null || (dataRowMinHeight == null && dataRowMaxHeight == null), |
503 | 'dataRowHeight ($dataRowHeight ) must not be set if dataRowMinHeight ($dataRowMinHeight ) or dataRowMaxHeight ($dataRowMaxHeight ) are set.', |
504 | ), |
505 | dataRowMinHeight = dataRowHeight ?? dataRowMinHeight, |
506 | dataRowMaxHeight = dataRowHeight ?? dataRowMaxHeight, |
507 | _onlyTextColumn = _initOnlyTextColumn(columns); |
508 | |
509 | /// The configuration and labels for the columns in the table. |
510 | final List<DataColumn> columns; |
511 | |
512 | /// The current primary sort key's column. |
513 | /// |
514 | /// If non-null, indicates that the indicated column is the column |
515 | /// by which the data is sorted. The number must correspond to the |
516 | /// index of the relevant column in [columns]. |
517 | /// |
518 | /// Setting this will cause the relevant column to have a sort |
519 | /// indicator displayed. |
520 | /// |
521 | /// When this is null, it implies that the table's sort order does |
522 | /// not correspond to any of the columns. |
523 | /// |
524 | /// The direction of the sort is specified using [sortAscending]. |
525 | final int? sortColumnIndex; |
526 | |
527 | /// Whether the column mentioned in [sortColumnIndex], if any, is sorted |
528 | /// in ascending order. |
529 | /// |
530 | /// If true, the order is ascending (meaning the rows with the |
531 | /// smallest values for the current sort column are first in the |
532 | /// table). |
533 | /// |
534 | /// If false, the order is descending (meaning the rows with the |
535 | /// smallest values for the current sort column are last in the |
536 | /// table). |
537 | /// |
538 | /// Ascending order is represented by an upwards-facing arrow. |
539 | final bool sortAscending; |
540 | |
541 | /// Invoked when the user selects or unselects every row, using the |
542 | /// checkbox in the heading row. |
543 | /// |
544 | /// If this is null, then the [DataRow.onSelectChanged] callback of |
545 | /// every row in the table is invoked appropriately instead. |
546 | /// |
547 | /// To control whether a particular row is selectable or not, see |
548 | /// [DataRow.onSelectChanged]. This callback is only relevant if any |
549 | /// row is selectable. |
550 | final ValueSetter<bool?>? onSelectAll; |
551 | |
552 | /// {@template flutter.material.dataTable.decoration} |
553 | /// The background and border decoration for the table. |
554 | /// {@endtemplate} |
555 | /// |
556 | /// If null, [DataTableThemeData.decoration] is used. By default there is no |
557 | /// decoration. |
558 | final Decoration? decoration; |
559 | |
560 | /// {@template flutter.material.dataTable.dataRowColor} |
561 | /// The background color for the data rows. |
562 | /// |
563 | /// The effective background color can be made to depend on the |
564 | /// [WidgetState] state, i.e. if the row is selected, pressed, hovered, |
565 | /// focused, disabled or enabled. The color is painted as an overlay to the |
566 | /// row. To make sure that the row's [InkWell] is visible (when pressed, |
567 | /// hovered and focused), it is recommended to use a translucent background |
568 | /// color. |
569 | /// |
570 | /// If [DataRow.onSelectChanged] or [DataRow.onLongPress] is null, the row's |
571 | /// [InkWell] will be disabled. |
572 | /// {@endtemplate} |
573 | /// |
574 | /// If null, [DataTableThemeData.dataRowColor] is used. By default, the |
575 | /// background color is transparent unless selected. Selected rows have a grey |
576 | /// translucent color. To set a different color for individual rows, see |
577 | /// [DataRow.color]. |
578 | /// |
579 | /// {@template flutter.material.DataTable.dataRowColor} |
580 | /// ```dart |
581 | /// DataTable( |
582 | /// dataRowColor: WidgetStateProperty.resolveWith<Color?>((Set<WidgetState> states) { |
583 | /// if (states.contains(WidgetState.selected)) { |
584 | /// return Theme.of(context).colorScheme.primary.withOpacity(0.08); |
585 | /// } |
586 | /// return null; // Use the default value. |
587 | /// }), |
588 | /// columns: _columns, |
589 | /// rows: _rows, |
590 | /// ) |
591 | /// ``` |
592 | /// |
593 | /// See also: |
594 | /// |
595 | /// * The Material Design specification for overlay colors and how they |
596 | /// match a component's state: |
597 | /// <https://material.io/design/interaction/states.html#anatomy>. |
598 | /// {@endtemplate} |
599 | final MaterialStateProperty<Color?>? dataRowColor; |
600 | |
601 | /// {@template flutter.material.dataTable.dataRowHeight} |
602 | /// The height of each row (excluding the row that contains column headings). |
603 | /// {@endtemplate} |
604 | /// |
605 | /// If null, [DataTableThemeData.dataRowHeight] is used. This value defaults |
606 | /// to [kMinInteractiveDimension] to adhere to the Material Design |
607 | /// specifications. |
608 | @Deprecated( |
609 | 'Migrate to use dataRowMinHeight and dataRowMaxHeight instead. ' |
610 | 'This feature was deprecated after v3.7.0-5.0.pre.', |
611 | ) |
612 | double? get dataRowHeight => dataRowMinHeight == dataRowMaxHeight ? dataRowMinHeight : null; |
613 | |
614 | /// {@template flutter.material.dataTable.dataRowMinHeight} |
615 | /// The minimum height of each row (excluding the row that contains column headings). |
616 | /// {@endtemplate} |
617 | /// |
618 | /// If null, [DataTableThemeData.dataRowMinHeight] is used. This value defaults |
619 | /// to [kMinInteractiveDimension] to adhere to the Material Design |
620 | /// specifications. |
621 | final double? dataRowMinHeight; |
622 | |
623 | /// {@template flutter.material.dataTable.dataRowMaxHeight} |
624 | /// The maximum height of each row (excluding the row that contains column headings). |
625 | /// {@endtemplate} |
626 | /// |
627 | /// If null, [DataTableThemeData.dataRowMaxHeight] is used. This value defaults |
628 | /// to [kMinInteractiveDimension] to adhere to the Material Design |
629 | /// specifications. |
630 | final double? dataRowMaxHeight; |
631 | |
632 | /// {@template flutter.material.dataTable.dataTextStyle} |
633 | /// The text style for data rows. |
634 | /// {@endtemplate} |
635 | /// |
636 | /// If null, [DataTableThemeData.dataTextStyle] is used. By default, the text |
637 | /// style is [TextTheme.bodyMedium]. |
638 | final TextStyle? dataTextStyle; |
639 | |
640 | /// {@template flutter.material.dataTable.headingRowColor} |
641 | /// The background color for the heading row. |
642 | /// |
643 | /// The effective background color can be made to depend on the |
644 | /// [WidgetState] state, i.e. if the row is pressed, hovered, focused when |
645 | /// sorted. The color is painted as an overlay to the row. To make sure that |
646 | /// the row's [InkWell] is visible (when pressed, hovered and focused), it is |
647 | /// recommended to use a translucent color. |
648 | /// {@endtemplate} |
649 | /// |
650 | /// If null, [DataTableThemeData.headingRowColor] is used. |
651 | /// |
652 | /// {@template flutter.material.DataTable.headingRowColor} |
653 | /// ```dart |
654 | /// DataTable( |
655 | /// columns: _columns, |
656 | /// rows: _rows, |
657 | /// headingRowColor: WidgetStateProperty.resolveWith<Color?>((Set<WidgetState> states) { |
658 | /// if (states.contains(WidgetState.hovered)) { |
659 | /// return Theme.of(context).colorScheme.primary.withOpacity(0.08); |
660 | /// } |
661 | /// return null; // Use the default value. |
662 | /// }), |
663 | /// ) |
664 | /// ``` |
665 | /// |
666 | /// See also: |
667 | /// |
668 | /// * The Material Design specification for overlay colors and how they |
669 | /// match a component's state: |
670 | /// <https://material.io/design/interaction/states.html#anatomy>. |
671 | /// {@endtemplate} |
672 | final MaterialStateProperty<Color?>? headingRowColor; |
673 | |
674 | /// {@template flutter.material.dataTable.headingRowHeight} |
675 | /// The height of the heading row. |
676 | /// {@endtemplate} |
677 | /// |
678 | /// If null, [DataTableThemeData.headingRowHeight] is used. This value |
679 | /// defaults to 56.0 to adhere to the Material Design specifications. |
680 | final double? headingRowHeight; |
681 | |
682 | /// {@template flutter.material.dataTable.headingTextStyle} |
683 | /// The text style for the heading row. |
684 | /// {@endtemplate} |
685 | /// |
686 | /// If null, [DataTableThemeData.headingTextStyle] is used. By default, the |
687 | /// text style is [TextTheme.titleSmall]. |
688 | final TextStyle? headingTextStyle; |
689 | |
690 | /// {@template flutter.material.dataTable.horizontalMargin} |
691 | /// The horizontal margin between the edges of the table and the content |
692 | /// in the first and last cells of each row. |
693 | /// |
694 | /// When a checkbox is displayed, it is also the margin between the checkbox |
695 | /// the content in the first data column. |
696 | /// {@endtemplate} |
697 | /// |
698 | /// If null, [DataTableThemeData.horizontalMargin] is used. This value |
699 | /// defaults to 24.0 to adhere to the Material Design specifications. |
700 | /// |
701 | /// If [checkboxHorizontalMargin] is null, then [horizontalMargin] is also the |
702 | /// margin between the edge of the table and the checkbox, as well as the |
703 | /// margin between the checkbox and the content in the first data column. |
704 | final double? horizontalMargin; |
705 | |
706 | /// {@template flutter.material.dataTable.columnSpacing} |
707 | /// The horizontal margin between the contents of each data column. |
708 | /// {@endtemplate} |
709 | /// |
710 | /// If null, [DataTableThemeData.columnSpacing] is used. This value defaults |
711 | /// to 56.0 to adhere to the Material Design specifications. |
712 | final double? columnSpacing; |
713 | |
714 | /// {@template flutter.material.dataTable.showCheckboxColumn} |
715 | /// Whether the widget should display checkboxes for selectable rows. |
716 | /// |
717 | /// If true, a [Checkbox] will be placed at the beginning of each row that is |
718 | /// selectable. However, if [DataRow.onSelectChanged] is not set for any row, |
719 | /// checkboxes will not be placed, even if this value is true. |
720 | /// |
721 | /// If false, all rows will not display a [Checkbox]. |
722 | /// {@endtemplate} |
723 | final bool showCheckboxColumn; |
724 | |
725 | /// The data to show in each row (excluding the row that contains |
726 | /// the column headings). |
727 | /// |
728 | /// The list may be empty. |
729 | final List<DataRow> rows; |
730 | |
731 | /// {@template flutter.material.dataTable.dividerThickness} |
732 | /// The width of the divider that appears between [TableRow]s. |
733 | /// |
734 | /// Must be greater than or equal to zero. |
735 | /// {@endtemplate} |
736 | /// |
737 | /// If null, [DataTableThemeData.dividerThickness] is used. This value |
738 | /// defaults to 1.0. |
739 | final double? dividerThickness; |
740 | |
741 | /// Whether a border at the bottom of the table is displayed. |
742 | /// |
743 | /// By default, a border is not shown at the bottom to allow for a border |
744 | /// around the table defined by [decoration]. |
745 | final bool showBottomBorder; |
746 | |
747 | /// {@template flutter.material.dataTable.checkboxHorizontalMargin} |
748 | /// Horizontal margin around the checkbox, if it is displayed. |
749 | /// {@endtemplate} |
750 | /// |
751 | /// If null, [DataTableThemeData.checkboxHorizontalMargin] is used. If that is |
752 | /// also null, then [horizontalMargin] is used as the margin between the edge |
753 | /// of the table and the checkbox, as well as the margin between the checkbox |
754 | /// and the content in the first data column. This value defaults to 24.0. |
755 | final double? checkboxHorizontalMargin; |
756 | |
757 | /// The style to use when painting the boundary and interior divisions of the table. |
758 | final TableBorder? border; |
759 | |
760 | /// {@macro flutter.material.Material.clipBehavior} |
761 | /// |
762 | /// This can be used to clip the content within the border of the [DataTable]. |
763 | /// |
764 | /// Defaults to [Clip.none]. |
765 | final Clip clipBehavior; |
766 | |
767 | // Set by the constructor to the index of the only Column that is |
768 | // non-numeric, if there is exactly one, otherwise null. |
769 | final int? _onlyTextColumn; |
770 | static int? _initOnlyTextColumn(List<DataColumn> columns) { |
771 | int? result; |
772 | for (int index = 0; index < columns.length; index += 1) { |
773 | final DataColumn column = columns[index]; |
774 | if (!column.numeric) { |
775 | if (result != null) { |
776 | return null; |
777 | } |
778 | result = index; |
779 | } |
780 | } |
781 | return result; |
782 | } |
783 | |
784 | bool get _debugInteractive { |
785 | return columns.any((DataColumn column) => column._debugInteractive) || |
786 | rows.any((DataRow row) => row._debugInteractive); |
787 | } |
788 | |
789 | static final LocalKey _headingRowKey = UniqueKey(); |
790 | |
791 | void _handleSelectAll(bool? checked, bool someChecked) { |
792 | // If some checkboxes are checked, all checkboxes are selected. Otherwise, |
793 | // use the new checked value but default to false if it's null. |
794 | final bool effectiveChecked = someChecked || (checked ?? false); |
795 | if (onSelectAll != null) { |
796 | onSelectAll!(effectiveChecked); |
797 | } else { |
798 | for (final DataRow row in rows) { |
799 | if (row.onSelectChanged != null && row.selected != effectiveChecked) { |
800 | row.onSelectChanged!(effectiveChecked); |
801 | } |
802 | } |
803 | } |
804 | } |
805 | |
806 | /// The default height of the heading row. |
807 | static const double _headingRowHeight = 56.0; |
808 | |
809 | /// The default horizontal margin between the edges of the table and the content |
810 | /// in the first and last cells of each row. |
811 | static const double _horizontalMargin = 24.0; |
812 | |
813 | /// The default horizontal margin between the contents of each data column. |
814 | static const double _columnSpacing = 56.0; |
815 | |
816 | /// The default padding between the heading content and sort arrow. |
817 | static const double _sortArrowPadding = 2.0; |
818 | |
819 | /// The default divider thickness. |
820 | static const double _dividerThickness = 1.0; |
821 | |
822 | static const Duration _sortArrowAnimationDuration = Duration(milliseconds: 150); |
823 | |
824 | Widget _buildCheckbox({ |
825 | required BuildContext context, |
826 | required bool? checked, |
827 | required VoidCallback? onRowTap, |
828 | required ValueChanged<bool?>? onCheckboxChanged, |
829 | required MaterialStateProperty<Color?>? overlayColor, |
830 | required bool tristate, |
831 | MouseCursor? rowMouseCursor, |
832 | }) { |
833 | final ThemeData themeData = Theme.of(context); |
834 | final double effectiveHorizontalMargin = |
835 | horizontalMargin ?? themeData.dataTableTheme.horizontalMargin ?? _horizontalMargin; |
836 | final double effectiveCheckboxHorizontalMarginStart = |
837 | checkboxHorizontalMargin ?? |
838 | themeData.dataTableTheme.checkboxHorizontalMargin ?? |
839 | effectiveHorizontalMargin; |
840 | final double effectiveCheckboxHorizontalMarginEnd = |
841 | checkboxHorizontalMargin ?? |
842 | themeData.dataTableTheme.checkboxHorizontalMargin ?? |
843 | effectiveHorizontalMargin / 2.0; |
844 | Widget contents = Semantics( |
845 | container: true, |
846 | child: Padding( |
847 | padding: EdgeInsetsDirectional.only( |
848 | start: effectiveCheckboxHorizontalMarginStart, |
849 | end: effectiveCheckboxHorizontalMarginEnd, |
850 | ), |
851 | child: Center( |
852 | child: Checkbox(value: checked, onChanged: onCheckboxChanged, tristate: tristate), |
853 | ), |
854 | ), |
855 | ); |
856 | if (onRowTap != null) { |
857 | contents = TableRowInkWell( |
858 | onTap: onRowTap, |
859 | overlayColor: overlayColor, |
860 | mouseCursor: rowMouseCursor, |
861 | child: contents, |
862 | ); |
863 | } |
864 | return TableCell(verticalAlignment: TableCellVerticalAlignment.fill, child: contents); |
865 | } |
866 | |
867 | Widget _buildHeadingCell({ |
868 | required BuildContext context, |
869 | required EdgeInsetsGeometry padding, |
870 | required Widget label, |
871 | required String? tooltip, |
872 | required bool numeric, |
873 | required VoidCallback? onSort, |
874 | required bool sorted, |
875 | required bool ascending, |
876 | required MaterialStateProperty<Color?>? overlayColor, |
877 | required MouseCursor? mouseCursor, |
878 | required MainAxisAlignment headingRowAlignment, |
879 | }) { |
880 | final ThemeData themeData = Theme.of(context); |
881 | final DataTableThemeData dataTableTheme = DataTableTheme.of(context); |
882 | label = Semantics( |
883 | role: SemanticsRole.columnHeader, |
884 | child: Row( |
885 | textDirection: numeric ? TextDirection.rtl : null, |
886 | mainAxisAlignment: headingRowAlignment, |
887 | children: <Widget>[ |
888 | if (headingRowAlignment == MainAxisAlignment.center && onSort != null) |
889 | const SizedBox(width: _SortArrowState._arrowIconSize + _sortArrowPadding), |
890 | label, |
891 | if (onSort != null) ...<Widget>[ |
892 | _SortArrow( |
893 | visible: sorted, |
894 | up: sorted ? ascending : null, |
895 | duration: _sortArrowAnimationDuration, |
896 | ), |
897 | const SizedBox(width: _sortArrowPadding), |
898 | ], |
899 | ], |
900 | ), |
901 | ); |
902 | |
903 | final TextStyle effectiveHeadingTextStyle = |
904 | headingTextStyle ?? |
905 | dataTableTheme.headingTextStyle ?? |
906 | themeData.dataTableTheme.headingTextStyle ?? |
907 | themeData.textTheme.titleSmall!; |
908 | final double effectiveHeadingRowHeight = |
909 | headingRowHeight ?? |
910 | dataTableTheme.headingRowHeight ?? |
911 | themeData.dataTableTheme.headingRowHeight ?? |
912 | _headingRowHeight; |
913 | label = Container( |
914 | padding: padding, |
915 | height: effectiveHeadingRowHeight, |
916 | alignment: numeric ? Alignment.centerRight : AlignmentDirectional.centerStart, |
917 | child: AnimatedDefaultTextStyle( |
918 | style: DefaultTextStyle.of(context).style.merge(effectiveHeadingTextStyle), |
919 | softWrap: false, |
920 | duration: _sortArrowAnimationDuration, |
921 | child: label, |
922 | ), |
923 | ); |
924 | if (tooltip != null) { |
925 | label = Tooltip(message: tooltip, child: label); |
926 | } |
927 | |
928 | // TODO(dkwingsmt): Only wrap Inkwell if onSort != null. Blocked by |
929 | // https://github.com/flutter/flutter/issues/51152 |
930 | label = InkWell( |
931 | onTap: onSort, |
932 | overlayColor: overlayColor, |
933 | mouseCursor: mouseCursor, |
934 | child: label, |
935 | ); |
936 | return label; |
937 | } |
938 | |
939 | Widget _buildDataCell({ |
940 | required BuildContext context, |
941 | required EdgeInsetsGeometry padding, |
942 | required Widget label, |
943 | required bool numeric, |
944 | required bool placeholder, |
945 | required bool showEditIcon, |
946 | required GestureTapCallback? onTap, |
947 | required VoidCallback? onSelectChanged, |
948 | required GestureTapCallback? onDoubleTap, |
949 | required GestureLongPressCallback? onLongPress, |
950 | required GestureTapDownCallback? onTapDown, |
951 | required GestureTapCancelCallback? onTapCancel, |
952 | required MaterialStateProperty<Color?>? overlayColor, |
953 | required GestureLongPressCallback? onRowLongPress, |
954 | required MouseCursor? mouseCursor, |
955 | }) { |
956 | final ThemeData themeData = Theme.of(context); |
957 | final DataTableThemeData dataTableTheme = DataTableTheme.of(context); |
958 | if (showEditIcon) { |
959 | const Widget icon = Icon(Icons.edit, size: 18.0); |
960 | label = Expanded(child: label); |
961 | label = Row( |
962 | textDirection: numeric ? TextDirection.rtl : null, |
963 | children: <Widget>[label, icon], |
964 | ); |
965 | } |
966 | |
967 | final TextStyle effectiveDataTextStyle = |
968 | dataTextStyle ?? |
969 | dataTableTheme.dataTextStyle ?? |
970 | themeData.dataTableTheme.dataTextStyle ?? |
971 | themeData.textTheme.bodyMedium!; |
972 | final double effectiveDataRowMinHeight = |
973 | dataRowMinHeight ?? |
974 | dataTableTheme.dataRowMinHeight ?? |
975 | themeData.dataTableTheme.dataRowMinHeight ?? |
976 | kMinInteractiveDimension; |
977 | final double effectiveDataRowMaxHeight = |
978 | dataRowMaxHeight ?? |
979 | dataTableTheme.dataRowMaxHeight ?? |
980 | themeData.dataTableTheme.dataRowMaxHeight ?? |
981 | kMinInteractiveDimension; |
982 | label = Container( |
983 | padding: padding, |
984 | constraints: BoxConstraints( |
985 | minHeight: effectiveDataRowMinHeight, |
986 | maxHeight: effectiveDataRowMaxHeight, |
987 | ), |
988 | alignment: numeric ? Alignment.centerRight : AlignmentDirectional.centerStart, |
989 | child: DefaultTextStyle( |
990 | style: DefaultTextStyle.of(context).style |
991 | .merge(effectiveDataTextStyle) |
992 | .copyWith(color: placeholder ? effectiveDataTextStyle.color!.withOpacity(0.6) : null), |
993 | child: DropdownButtonHideUnderline(child: label), |
994 | ), |
995 | ); |
996 | if (onTap != null || |
997 | onDoubleTap != null || |
998 | onLongPress != null || |
999 | onTapDown != null || |
1000 | onTapCancel != null) { |
1001 | label = InkWell( |
1002 | onTap: onTap, |
1003 | onDoubleTap: onDoubleTap, |
1004 | onLongPress: onLongPress, |
1005 | onTapCancel: onTapCancel, |
1006 | onTapDown: onTapDown, |
1007 | overlayColor: overlayColor, |
1008 | child: label, |
1009 | ); |
1010 | } else if (onSelectChanged != null || onRowLongPress != null) { |
1011 | label = TableRowInkWell( |
1012 | onTap: onSelectChanged, |
1013 | onLongPress: onRowLongPress, |
1014 | overlayColor: overlayColor, |
1015 | mouseCursor: mouseCursor, |
1016 | child: label, |
1017 | ); |
1018 | } |
1019 | return TableCell(child: label); |
1020 | } |
1021 | |
1022 | @override |
1023 | Widget build(BuildContext context) { |
1024 | assert(!_debugInteractive || debugCheckHasMaterial(context)); |
1025 | |
1026 | final ThemeData theme = Theme.of(context); |
1027 | final DataTableThemeData dataTableTheme = DataTableTheme.of(context); |
1028 | final MaterialStateProperty<Color?>? effectiveHeadingRowColor = |
1029 | headingRowColor ?? dataTableTheme.headingRowColor ?? theme.dataTableTheme.headingRowColor; |
1030 | final MaterialStateProperty<Color?>? effectiveDataRowColor = |
1031 | dataRowColor ?? dataTableTheme.dataRowColor ?? theme.dataTableTheme.dataRowColor; |
1032 | final MaterialStateProperty<Color?> defaultRowColor = MaterialStateProperty.resolveWith(( |
1033 | Set<MaterialState> states, |
1034 | ) { |
1035 | if (states.contains(MaterialState.selected)) { |
1036 | return theme.colorScheme.primary.withOpacity(0.08); |
1037 | } |
1038 | return null; |
1039 | }); |
1040 | final bool anyRowSelectable = rows.any((DataRow row) => row.onSelectChanged != null); |
1041 | final bool displayCheckboxColumn = showCheckboxColumn && anyRowSelectable; |
1042 | final Iterable<DataRow> rowsWithCheckbox = |
1043 | displayCheckboxColumn |
1044 | ? rows.where((DataRow row) => row.onSelectChanged != null) |
1045 | : <DataRow>[]; |
1046 | final Iterable<DataRow> rowsChecked = rowsWithCheckbox.where((DataRow row) => row.selected); |
1047 | final bool allChecked = displayCheckboxColumn && rowsChecked.length == rowsWithCheckbox.length; |
1048 | final bool anyChecked = displayCheckboxColumn && rowsChecked.isNotEmpty; |
1049 | final bool someChecked = anyChecked && !allChecked; |
1050 | final double effectiveHorizontalMargin = |
1051 | horizontalMargin ?? |
1052 | dataTableTheme.horizontalMargin ?? |
1053 | theme.dataTableTheme.horizontalMargin ?? |
1054 | _horizontalMargin; |
1055 | final double effectiveCheckboxHorizontalMarginStart = |
1056 | checkboxHorizontalMargin ?? |
1057 | dataTableTheme.checkboxHorizontalMargin ?? |
1058 | theme.dataTableTheme.checkboxHorizontalMargin ?? |
1059 | effectiveHorizontalMargin; |
1060 | final double effectiveCheckboxHorizontalMarginEnd = |
1061 | checkboxHorizontalMargin ?? |
1062 | dataTableTheme.checkboxHorizontalMargin ?? |
1063 | theme.dataTableTheme.checkboxHorizontalMargin ?? |
1064 | effectiveHorizontalMargin / 2.0; |
1065 | final double effectiveColumnSpacing = |
1066 | columnSpacing ?? |
1067 | dataTableTheme.columnSpacing ?? |
1068 | theme.dataTableTheme.columnSpacing ?? |
1069 | _columnSpacing; |
1070 | |
1071 | final List<TableColumnWidth> tableColumns = List<TableColumnWidth>.filled( |
1072 | columns.length + (displayCheckboxColumn ? 1 : 0), |
1073 | const _NullTableColumnWidth(), |
1074 | ); |
1075 | final List<TableRow> tableRows = List<TableRow>.generate( |
1076 | rows.length + 1, // the +1 is for the header row |
1077 | (int index) { |
1078 | final bool isSelected = index > 0 && rows[index - 1].selected; |
1079 | final bool isDisabled = |
1080 | index > 0 && anyRowSelectable && rows[index - 1].onSelectChanged == null; |
1081 | final Set<MaterialState> states = <MaterialState>{ |
1082 | if (isSelected) MaterialState.selected, |
1083 | if (isDisabled) MaterialState.disabled, |
1084 | }; |
1085 | final Color? resolvedDataRowColor = |
1086 | index > 0 ? (rows[index - 1].color ?? effectiveDataRowColor)?.resolve(states) : null; |
1087 | final Color? resolvedHeadingRowColor = effectiveHeadingRowColor?.resolve(<MaterialState>{}); |
1088 | final Color? rowColor = index > 0 ? resolvedDataRowColor : resolvedHeadingRowColor; |
1089 | final BorderSide borderSide = Divider.createBorderSide( |
1090 | context, |
1091 | width: |
1092 | dividerThickness ?? |
1093 | dataTableTheme.dividerThickness ?? |
1094 | theme.dataTableTheme.dividerThickness ?? |
1095 | _dividerThickness, |
1096 | ); |
1097 | final Border? border = |
1098 | showBottomBorder |
1099 | ? Border(bottom: borderSide) |
1100 | : index == 0 |
1101 | ? null |
1102 | : Border(top: borderSide); |
1103 | return TableRow( |
1104 | key: index == 0 ? _headingRowKey : rows[index - 1].key, |
1105 | decoration: BoxDecoration( |
1106 | border: border, |
1107 | color: rowColor ?? defaultRowColor.resolve(states), |
1108 | ), |
1109 | children: List<Widget>.filled(tableColumns.length, const _NullWidget()), |
1110 | ); |
1111 | }, |
1112 | ); |
1113 | |
1114 | int rowIndex; |
1115 | |
1116 | int displayColumnIndex = 0; |
1117 | if (displayCheckboxColumn) { |
1118 | tableColumns[0] = FixedColumnWidth( |
1119 | effectiveCheckboxHorizontalMarginStart + |
1120 | Checkbox.width + |
1121 | effectiveCheckboxHorizontalMarginEnd, |
1122 | ); |
1123 | tableRows[0].children[0] = _buildCheckbox( |
1124 | context: context, |
1125 | checked: someChecked ? null : allChecked, |
1126 | onRowTap: null, |
1127 | onCheckboxChanged: (bool? checked) => _handleSelectAll(checked, someChecked), |
1128 | overlayColor: null, |
1129 | tristate: true, |
1130 | ); |
1131 | rowIndex = 1; |
1132 | for (final DataRow row in rows) { |
1133 | final Set<MaterialState> states = <MaterialState>{if (row.selected) MaterialState.selected}; |
1134 | tableRows[rowIndex].children[0] = _buildCheckbox( |
1135 | context: context, |
1136 | checked: row.selected, |
1137 | onRowTap: |
1138 | row.onSelectChanged == null ? null : () => row.onSelectChanged?.call(!row.selected), |
1139 | onCheckboxChanged: row.onSelectChanged, |
1140 | overlayColor: row.color ?? effectiveDataRowColor, |
1141 | rowMouseCursor: |
1142 | row.mouseCursor?.resolve(states) ?? dataTableTheme.dataRowCursor?.resolve(states), |
1143 | tristate: false, |
1144 | ); |
1145 | rowIndex += 1; |
1146 | } |
1147 | displayColumnIndex += 1; |
1148 | } |
1149 | |
1150 | for (int dataColumnIndex = 0; dataColumnIndex < columns.length; dataColumnIndex += 1) { |
1151 | final DataColumn column = columns[dataColumnIndex]; |
1152 | |
1153 | final double paddingStart = switch (dataColumnIndex) { |
1154 | 0 when displayCheckboxColumn && checkboxHorizontalMargin == null => |
1155 | effectiveHorizontalMargin / 2.0, |
1156 | 0 => effectiveHorizontalMargin, |
1157 | _ => effectiveColumnSpacing / 2.0, |
1158 | }; |
1159 | |
1160 | final double paddingEnd; |
1161 | if (dataColumnIndex == columns.length - 1) { |
1162 | paddingEnd = effectiveHorizontalMargin; |
1163 | } else { |
1164 | paddingEnd = effectiveColumnSpacing / 2.0; |
1165 | } |
1166 | |
1167 | final EdgeInsetsDirectional padding = EdgeInsetsDirectional.only( |
1168 | start: paddingStart, |
1169 | end: paddingEnd, |
1170 | ); |
1171 | if (column.columnWidth != null) { |
1172 | tableColumns[displayColumnIndex] = column.columnWidth!; |
1173 | } else if (dataColumnIndex == _onlyTextColumn) { |
1174 | tableColumns[displayColumnIndex] = const IntrinsicColumnWidth(flex: 1.0); |
1175 | } else { |
1176 | tableColumns[displayColumnIndex] = const IntrinsicColumnWidth(); |
1177 | } |
1178 | |
1179 | final Set<MaterialState> headerStates = <MaterialState>{ |
1180 | if (column.onSort == null) MaterialState.disabled, |
1181 | }; |
1182 | tableRows[0].children[displayColumnIndex] = _buildHeadingCell( |
1183 | context: context, |
1184 | padding: padding, |
1185 | label: column.label, |
1186 | tooltip: column.tooltip, |
1187 | numeric: column.numeric, |
1188 | onSort: |
1189 | column.onSort != null |
1190 | ? () => column.onSort!( |
1191 | dataColumnIndex, |
1192 | sortColumnIndex != dataColumnIndex || !sortAscending, |
1193 | ) |
1194 | : null, |
1195 | sorted: dataColumnIndex == sortColumnIndex, |
1196 | ascending: sortAscending, |
1197 | overlayColor: effectiveHeadingRowColor, |
1198 | mouseCursor: |
1199 | column.mouseCursor?.resolve(headerStates) ?? |
1200 | dataTableTheme.headingCellCursor?.resolve(headerStates), |
1201 | headingRowAlignment: |
1202 | column.headingRowAlignment ?? |
1203 | dataTableTheme.headingRowAlignment ?? |
1204 | MainAxisAlignment.start, |
1205 | ); |
1206 | rowIndex = 1; |
1207 | for (final DataRow row in rows) { |
1208 | final Set<MaterialState> states = <MaterialState>{if (row.selected) MaterialState.selected}; |
1209 | final DataCell cell = row.cells[dataColumnIndex]; |
1210 | tableRows[rowIndex].children[displayColumnIndex] = _buildDataCell( |
1211 | context: context, |
1212 | padding: padding, |
1213 | label: cell.child, |
1214 | numeric: column.numeric, |
1215 | placeholder: cell.placeholder, |
1216 | showEditIcon: cell.showEditIcon, |
1217 | onTap: cell.onTap, |
1218 | onDoubleTap: cell.onDoubleTap, |
1219 | onLongPress: cell.onLongPress, |
1220 | onTapCancel: cell.onTapCancel, |
1221 | onTapDown: cell.onTapDown, |
1222 | onSelectChanged: |
1223 | row.onSelectChanged == null ? null : () => row.onSelectChanged?.call(!row.selected), |
1224 | overlayColor: row.color ?? effectiveDataRowColor, |
1225 | onRowLongPress: row.onLongPress, |
1226 | mouseCursor: |
1227 | row.mouseCursor?.resolve(states) ?? dataTableTheme.dataRowCursor?.resolve(states), |
1228 | ); |
1229 | rowIndex += 1; |
1230 | } |
1231 | displayColumnIndex += 1; |
1232 | } |
1233 | |
1234 | return Container( |
1235 | decoration: decoration ?? dataTableTheme.decoration ?? theme.dataTableTheme.decoration, |
1236 | child: Material( |
1237 | type: MaterialType.transparency, |
1238 | borderRadius: border?.borderRadius, |
1239 | clipBehavior: clipBehavior, |
1240 | child: Table( |
1241 | columnWidths: tableColumns.asMap(), |
1242 | defaultVerticalAlignment: TableCellVerticalAlignment.middle, |
1243 | children: tableRows, |
1244 | border: border, |
1245 | ), |
1246 | ), |
1247 | ); |
1248 | } |
1249 | } |
1250 | |
1251 | /// A rectangular area of a Material that responds to touch but clips |
1252 | /// its ink splashes to the current table row of the nearest table. |
1253 | /// |
1254 | /// Must have an ancestor [Material] widget in which to cause ink |
1255 | /// reactions and an ancestor [Table] widget to establish a row. |
1256 | /// |
1257 | /// The [TableRowInkWell] must be in the same coordinate space (modulo |
1258 | /// translations) as the [Table]. If it's rotated or scaled or |
1259 | /// otherwise transformed, it will not be able to describe the |
1260 | /// rectangle of the row in its own coordinate system as a [Rect], and |
1261 | /// thus the splash will not occur. (In general, this is easy to |
1262 | /// achieve: just put the [TableRowInkWell] as the direct child of the |
1263 | /// [Table], and put the other contents of the cell inside it.) |
1264 | /// |
1265 | /// See also: |
1266 | /// |
1267 | /// * [DataTable], which makes use of [TableRowInkWell] when |
1268 | /// [DataRow.onSelectChanged] is defined and [DataCell.onTap] |
1269 | /// is not. |
1270 | class TableRowInkWell extends InkResponse { |
1271 | /// Creates an ink well for a table row. |
1272 | const TableRowInkWell({ |
1273 | super.key, |
1274 | super.child, |
1275 | super.onTap, |
1276 | super.onDoubleTap, |
1277 | super.onLongPress, |
1278 | super.onHighlightChanged, |
1279 | super.onSecondaryTap, |
1280 | super.onSecondaryTapDown, |
1281 | super.overlayColor, |
1282 | super.mouseCursor, |
1283 | }) : super(containedInkWell: true, highlightShape: BoxShape.rectangle); |
1284 | |
1285 | @override |
1286 | RectCallback getRectCallback(RenderBox referenceBox) { |
1287 | return () { |
1288 | RenderObject cell = referenceBox; |
1289 | RenderObject? table = cell.parent; |
1290 | final Matrix4 transform = Matrix4.identity(); |
1291 | while (table is RenderObject && table is! RenderTable) { |
1292 | table.applyPaintTransform(cell, transform); |
1293 | assert(table == cell.parent); |
1294 | cell = table; |
1295 | table = table.parent; |
1296 | } |
1297 | if (table is RenderTable) { |
1298 | final TableCellParentData cellParentData = cell.parentData! as TableCellParentData; |
1299 | assert(cellParentData.y != null); |
1300 | final Rect rect = table.getRowBox(cellParentData.y!); |
1301 | // The rect is in the table's coordinate space. We need to change it to the |
1302 | // TableRowInkWell's coordinate space. |
1303 | table.applyPaintTransform(cell, transform); |
1304 | final Offset? offset = MatrixUtils.getAsTranslation(transform); |
1305 | if (offset != null) { |
1306 | return rect.shift(-offset); |
1307 | } |
1308 | } |
1309 | return Rect.zero; |
1310 | }; |
1311 | } |
1312 | |
1313 | @override |
1314 | bool debugCheckContext(BuildContext context) { |
1315 | assert(debugCheckHasTable(context)); |
1316 | return super.debugCheckContext(context); |
1317 | } |
1318 | } |
1319 | |
1320 | class _SortArrow extends StatefulWidget { |
1321 | const _SortArrow({required this.visible, required this.up, required this.duration}); |
1322 | |
1323 | final bool visible; |
1324 | |
1325 | final bool? up; |
1326 | |
1327 | final Duration duration; |
1328 | |
1329 | @override |
1330 | _SortArrowState createState() => _SortArrowState(); |
1331 | } |
1332 | |
1333 | class _SortArrowState extends State<_SortArrow> with TickerProviderStateMixin { |
1334 | late final AnimationController _opacityController; |
1335 | late final CurvedAnimation _opacityAnimation; |
1336 | |
1337 | late final AnimationController _orientationController; |
1338 | late final Animation<double> _orientationAnimation; |
1339 | double _orientationOffset = 0.0; |
1340 | |
1341 | bool? _up; |
1342 | |
1343 | static final Animatable<double> _turnTween = Tween<double>( |
1344 | begin: 0.0, |
1345 | end: math.pi, |
1346 | ).chain(CurveTween(curve: Curves.easeIn)); |
1347 | |
1348 | @override |
1349 | void initState() { |
1350 | super.initState(); |
1351 | _up = widget.up; |
1352 | _opacityAnimation = CurvedAnimation( |
1353 | parent: _opacityController = AnimationController(duration: widget.duration, vsync: this), |
1354 | curve: Curves.fastOutSlowIn, |
1355 | )..addListener(_rebuild); |
1356 | _opacityController.value = widget.visible ? 1.0 : 0.0; |
1357 | _orientationController = AnimationController(duration: widget.duration, vsync: this); |
1358 | _orientationAnimation = |
1359 | _orientationController.drive(_turnTween) |
1360 | ..addListener(_rebuild) |
1361 | ..addStatusListener(_resetOrientationAnimation); |
1362 | if (widget.visible) { |
1363 | _orientationOffset = widget.up! ? 0.0 : math.pi; |
1364 | } |
1365 | } |
1366 | |
1367 | void _rebuild() { |
1368 | setState(() { |
1369 | // The animations changed, so we need to rebuild. |
1370 | }); |
1371 | } |
1372 | |
1373 | void _resetOrientationAnimation(AnimationStatus status) { |
1374 | if (status.isCompleted) { |
1375 | assert(_orientationAnimation.value == math.pi); |
1376 | _orientationOffset += math.pi; |
1377 | _orientationController.value = 0.0; // TODO(ianh): This triggers a pointless rebuild. |
1378 | } |
1379 | } |
1380 | |
1381 | @override |
1382 | void didUpdateWidget(_SortArrow oldWidget) { |
1383 | super.didUpdateWidget(oldWidget); |
1384 | bool skipArrow = false; |
1385 | final bool? newUp = widget.up ?? _up; |
1386 | if (oldWidget.visible != widget.visible) { |
1387 | if (widget.visible && _opacityController.isDismissed) { |
1388 | _orientationController.stop(); |
1389 | _orientationController.value = 0.0; |
1390 | _orientationOffset = newUp! ? 0.0 : math.pi; |
1391 | skipArrow = true; |
1392 | } |
1393 | if (widget.visible) { |
1394 | _opacityController.forward(); |
1395 | } else { |
1396 | _opacityController.reverse(); |
1397 | } |
1398 | } |
1399 | if ((_up != newUp) && !skipArrow) { |
1400 | if (_orientationController.isDismissed) { |
1401 | _orientationController.forward(); |
1402 | } else { |
1403 | _orientationController.reverse(); |
1404 | } |
1405 | } |
1406 | _up = newUp; |
1407 | } |
1408 | |
1409 | @override |
1410 | void dispose() { |
1411 | _opacityController.dispose(); |
1412 | _orientationController.dispose(); |
1413 | _opacityAnimation.dispose(); |
1414 | super.dispose(); |
1415 | } |
1416 | |
1417 | static const double _arrowIconBaselineOffset = -1.5; |
1418 | static const double _arrowIconSize = 16.0; |
1419 | |
1420 | @override |
1421 | Widget build(BuildContext context) { |
1422 | return FadeTransition( |
1423 | opacity: _opacityAnimation, |
1424 | child: Transform( |
1425 | transform: Matrix4.rotationZ(_orientationOffset + _orientationAnimation.value) |
1426 | ..setTranslationRaw(0.0, _arrowIconBaselineOffset, 0.0), |
1427 | alignment: Alignment.center, |
1428 | child: const Icon(Icons.arrow_upward, size: _arrowIconSize), |
1429 | ), |
1430 | ); |
1431 | } |
1432 | } |
1433 | |
1434 | class _NullTableColumnWidth extends TableColumnWidth { |
1435 | const _NullTableColumnWidth(); |
1436 | |
1437 | @override |
1438 | double maxIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) => |
1439 | throw UnimplementedError(); |
1440 | |
1441 | @override |
1442 | double minIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) => |
1443 | throw UnimplementedError(); |
1444 | } |
1445 | |
1446 | class _NullWidget extends Widget { |
1447 | const _NullWidget(); |
1448 | |
1449 | @override |
1450 | Element createElement() => throw UnimplementedError(); |
1451 | } |
1452 |
Definitions
- DataColumn
- DataColumn
- _debugInteractive
- DataRow
- DataRow
- byIndex
- _debugInteractive
- DataCell
- DataCell
- _debugInteractive
- DataTable
- DataTable
- dataRowHeight
- _initOnlyTextColumn
- _debugInteractive
- _handleSelectAll
- _buildCheckbox
- _buildHeadingCell
- _buildDataCell
- build
- TableRowInkWell
- TableRowInkWell
- getRectCallback
- debugCheckContext
- _SortArrow
- _SortArrow
- createState
- _SortArrowState
- initState
- _rebuild
- _resetOrientationAnimation
- didUpdateWidget
- dispose
- build
- _NullTableColumnWidth
- _NullTableColumnWidth
- maxIntrinsicWidth
- minIntrinsicWidth
- _NullWidget
- _NullWidget
Learn more about Flutter for embedded and desktop on industrialflutter.com