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';
7library;
8
9import 'dart:math' as math;
10
11import 'package:flutter/rendering.dart';
12import 'package:flutter/widgets.dart';
13
14import 'checkbox.dart';
15import 'constants.dart';
16import 'data_table_theme.dart';
17import 'debug.dart';
18import 'divider.dart';
19import 'dropdown.dart';
20import 'icons.dart';
21import 'ink_well.dart';
22import 'material.dart';
23import 'material_state.dart';
24import 'theme.dart';
25import 'tooltip.dart';
26
27// Examples can assume:
28// late BuildContext context;
29// late List _columns;
30// late List _rows;
31
32/// Signature for [DataColumn.onSort] callback.
33typedef 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
41class 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
144class 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
277class 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/// ![](https://flutter.github.io/assets-for-api-docs/assets/material/data_table.png)
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>
434class 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.
1270class 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
1320class _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
1333class _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
1434class _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
1446class _NullWidget extends Widget {
1447 const _NullWidget();
1448
1449 @override
1450 Element createElement() => throw UnimplementedError();
1451}
1452

Provided by KDAB

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