|
2 | 2 | // Use of this source code is governed by a BSD-style license that can be
|
3 | 3 | // found in the LICENSE file.
|
4 | 4 |
|
| 5 | +import 'package:flutter/foundation.dart'; |
5 | 6 | import 'package:flutter/widgets.dart';
|
6 | 7 |
|
7 | 8 | import 'table.dart';
|
@@ -31,13 +32,220 @@ class TableVicinity extends ChildVicinity {
|
31 | 32 | /// Equivalent to the [xIndex].
|
32 | 33 | int get column => xIndex;
|
33 | 34 |
|
| 35 | + /// The origin vicinity of the [TableView], (0,0). |
| 36 | + static const TableVicinity zero = TableVicinity(row: 0, column: 0); |
| 37 | + |
| 38 | + /// Returns a new [TableVicinity], copying over the row and column fields with |
| 39 | + /// those provided, or maintaining the original values. |
| 40 | + TableVicinity copyWith({ |
| 41 | + int? row, |
| 42 | + int? column, |
| 43 | + }) { |
| 44 | + return TableVicinity( |
| 45 | + row: row ?? this.row, |
| 46 | + column: column ?? this.column, |
| 47 | + ); |
| 48 | + } |
| 49 | + |
34 | 50 | @override
|
35 | 51 | String toString() => '(row: $row, column: $column)';
|
36 | 52 | }
|
37 | 53 |
|
38 | 54 | /// Parent data structure used by [RenderTableViewport].
|
39 | 55 | class TableViewParentData extends TwoDimensionalViewportParentData {
|
40 |
| - // TODO(Piinks): Add back merged cells here, https://github.com/flutter/flutter/issues/131224 |
41 | 56 | /// Converts the [ChildVicinity] to a [TableVicinity] for ease of use.
|
42 | 57 | TableVicinity get tableVicinity => vicinity as TableVicinity;
|
| 58 | + |
| 59 | + /// Represents the row index where a merged cell in the table begins. |
| 60 | + /// |
| 61 | + /// Defaults to null, meaning a non-merged cell. A value must be provided if |
| 62 | + /// a value is provided for [rowMergeSpan]. |
| 63 | + int? rowMergeStart; |
| 64 | + |
| 65 | + /// Represents the number of rows spanned by a merged cell. |
| 66 | + /// |
| 67 | + /// Defaults to null, meaning the cell is not merged. A value must be provided |
| 68 | + /// if a value is provided for [rowMergeStart]. |
| 69 | + int? rowMergeSpan; |
| 70 | + |
| 71 | + /// Represents the column index where a merged cell in the table begins. |
| 72 | + /// |
| 73 | + /// Defaults to null, meaning a non-merged cell. A value must be provided if |
| 74 | + /// a value is provided for [columnMergeSpan]. |
| 75 | + int? columnMergeStart; |
| 76 | + |
| 77 | + /// Represents the number of columns spanned by a merged cell. |
| 78 | + /// |
| 79 | + /// Defaults to null, meaning the cell is not merged. A value must be provided |
| 80 | + /// if a value is provided for [columnMergeStart]. |
| 81 | + int? columnMergeSpan; |
| 82 | + |
| 83 | + @override |
| 84 | + String toString() { |
| 85 | + String mergeDetails = ''; |
| 86 | + if (rowMergeStart != null || columnMergeStart != null) { |
| 87 | + mergeDetails += ', merged'; |
| 88 | + } |
| 89 | + if (rowMergeStart != null) { |
| 90 | + mergeDetails += ', rowMergeStart=$rowMergeStart, ' |
| 91 | + 'rowMergeSpan=$rowMergeSpan'; |
| 92 | + } |
| 93 | + if (columnMergeStart != null) { |
| 94 | + mergeDetails += ', columnMergeStart=$columnMergeStart, ' |
| 95 | + 'columnMergeSpan=$columnMergeSpan'; |
| 96 | + } |
| 97 | + return super.toString() + mergeDetails; |
| 98 | + } |
| 99 | +} |
| 100 | + |
| 101 | +/// Creates a cell of the [TableView], along with information regarding merged |
| 102 | +/// cells and [RepaintBoundary]s. |
| 103 | +/// |
| 104 | +/// When merging cells in a [TableView], the same child should be returned from |
| 105 | +/// every vicinity the merged cell contains. The `build` method will only be |
| 106 | +/// called once for a merged cell, but since the table's children are lazily |
| 107 | +/// laid out, returning the same child ensures the merged cell can be built no |
| 108 | +/// matter which part of it is visible. |
| 109 | +class TableViewCell extends StatelessWidget { |
| 110 | + /// Creates a widget that controls how a child of a [TableView] spans across |
| 111 | + /// multiple rows or columns. |
| 112 | + const TableViewCell({ |
| 113 | + super.key, |
| 114 | + this.rowMergeStart, |
| 115 | + this.rowMergeSpan, |
| 116 | + this.columnMergeStart, |
| 117 | + this.columnMergeSpan, |
| 118 | + this.addRepaintBoundaries = true, |
| 119 | + required this.child, |
| 120 | + }) : assert( |
| 121 | + (rowMergeStart == null && rowMergeSpan == null) || |
| 122 | + (rowMergeStart != null && rowMergeSpan != null), |
| 123 | + 'Row merge start and span must both be set, or both unset.', |
| 124 | + ), |
| 125 | + assert(rowMergeStart == null || rowMergeStart >= 0), |
| 126 | + assert(rowMergeSpan == null || rowMergeSpan > 0), |
| 127 | + assert( |
| 128 | + (columnMergeStart == null && columnMergeSpan == null) || |
| 129 | + (columnMergeStart != null && columnMergeSpan != null), |
| 130 | + 'Column merge start and span must both be set, or both unset.', |
| 131 | + ), |
| 132 | + assert(columnMergeStart == null || columnMergeStart >= 0), |
| 133 | + assert(columnMergeSpan == null || columnMergeSpan > 0); |
| 134 | + |
| 135 | + /// The child contained in this cell of the [TableView]. |
| 136 | + final Widget child; |
| 137 | + |
| 138 | + /// Represents the row index where a merged cell in the table begins. |
| 139 | + /// |
| 140 | + /// Defaults to null, meaning a non-merged cell. A value must be provided if |
| 141 | + /// a value is provided for [rowMergeSpan]. |
| 142 | + final int? rowMergeStart; |
| 143 | + |
| 144 | + /// Represents the number of rows spanned by a merged cell. |
| 145 | + /// |
| 146 | + /// Defaults to null, meaning the cell is not merged. A value must be provided |
| 147 | + /// if a value is provided for [rowMergeStart]. |
| 148 | + final int? rowMergeSpan; |
| 149 | + |
| 150 | + /// Represents the column index where a merged cell in the table begins. |
| 151 | + /// |
| 152 | + /// Defaults to null, meaning a non-merged cell. A value must be provided if |
| 153 | + /// a value is provided for [columnMergeSpan]. |
| 154 | + final int? columnMergeStart; |
| 155 | + |
| 156 | + /// Represents the number of columns spanned by a merged cell. |
| 157 | + /// |
| 158 | + /// Defaults to null, meaning the cell is not merged. A value must be provided |
| 159 | + /// if a value is provided for [columnMergeStart]. |
| 160 | + final int? columnMergeSpan; |
| 161 | + |
| 162 | + /// Whether to wrap each child in a [RepaintBoundary]. |
| 163 | + /// |
| 164 | + /// Typically, children in a scrolling container are wrapped in repaint |
| 165 | + /// boundaries so that they do not need to be repainted as the list scrolls. |
| 166 | + /// If the children are easy to repaint (e.g., solid color blocks or a short |
| 167 | + /// snippet of text), it might be more efficient to not add a repaint boundary |
| 168 | + /// and instead always repaint the children during scrolling. |
| 169 | + /// |
| 170 | + /// Defaults to true. |
| 171 | + final bool addRepaintBoundaries; |
| 172 | + |
| 173 | + @override |
| 174 | + Widget build(BuildContext context) { |
| 175 | + Widget child = this.child; |
| 176 | + |
| 177 | + if (addRepaintBoundaries) { |
| 178 | + child = RepaintBoundary(child: child); |
| 179 | + } |
| 180 | + |
| 181 | + return _TableViewCell( |
| 182 | + rowMergeStart: rowMergeStart, |
| 183 | + rowMergeSpan: rowMergeSpan, |
| 184 | + columnMergeStart: columnMergeStart, |
| 185 | + columnMergeSpan: columnMergeSpan, |
| 186 | + child: child, |
| 187 | + ); |
| 188 | + } |
| 189 | +} |
| 190 | + |
| 191 | +class _TableViewCell extends ParentDataWidget<TableViewParentData> { |
| 192 | + const _TableViewCell({ |
| 193 | + this.rowMergeStart, |
| 194 | + this.rowMergeSpan, |
| 195 | + this.columnMergeStart, |
| 196 | + this.columnMergeSpan, |
| 197 | + required super.child, |
| 198 | + }); |
| 199 | + |
| 200 | + final int? rowMergeStart; |
| 201 | + final int? rowMergeSpan; |
| 202 | + final int? columnMergeStart; |
| 203 | + final int? columnMergeSpan; |
| 204 | + |
| 205 | + @override |
| 206 | + void applyParentData(RenderObject renderObject) { |
| 207 | + final TableViewParentData parentData = |
| 208 | + renderObject.parentData! as TableViewParentData; |
| 209 | + bool needsLayout = false; |
| 210 | + if (parentData.rowMergeStart != rowMergeStart) { |
| 211 | + assert(rowMergeStart == null || rowMergeStart! >= 0); |
| 212 | + parentData.rowMergeStart = rowMergeStart; |
| 213 | + needsLayout = true; |
| 214 | + } |
| 215 | + if (parentData.rowMergeSpan != rowMergeSpan) { |
| 216 | + assert(rowMergeSpan == null || rowMergeSpan! > 0); |
| 217 | + parentData.rowMergeSpan = rowMergeSpan; |
| 218 | + needsLayout = true; |
| 219 | + } |
| 220 | + if (parentData.columnMergeStart != columnMergeStart) { |
| 221 | + assert(columnMergeStart == null || columnMergeStart! >= 0); |
| 222 | + parentData.columnMergeStart = columnMergeStart; |
| 223 | + needsLayout = true; |
| 224 | + } |
| 225 | + if (parentData.columnMergeSpan != columnMergeSpan) { |
| 226 | + assert(columnMergeSpan == null || columnMergeSpan! > 0); |
| 227 | + parentData.columnMergeSpan = columnMergeSpan; |
| 228 | + needsLayout = true; |
| 229 | + } |
| 230 | + |
| 231 | + if (needsLayout) { |
| 232 | + renderObject.parent?.markNeedsLayout(); |
| 233 | + } |
| 234 | + } |
| 235 | + |
| 236 | + @override |
| 237 | + Type get debugTypicalAncestorWidgetClass => TableViewport; |
| 238 | + |
| 239 | + @override |
| 240 | + void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| 241 | + super.debugFillProperties(properties); |
| 242 | + if (rowMergeStart != null) { |
| 243 | + properties.add(IntProperty('rowMergeStart', rowMergeStart)); |
| 244 | + properties.add(IntProperty('rowMergeSpan', rowMergeSpan)); |
| 245 | + } |
| 246 | + if (columnMergeStart != null) { |
| 247 | + properties.add(IntProperty('columnMergeStart', columnMergeStart)); |
| 248 | + properties.add(IntProperty('columnMergeSpan', columnMergeSpan)); |
| 249 | + } |
| 250 | + } |
43 | 251 | }
|
0 commit comments