Skip to content

Commit 0dd2410

Browse files
authored
[two_dimensional_scrollables] Add alignment to TreeView and TableView (#11353)
This PR implements declarative alignment for the content of TableView and TreeView widgets when that content is smaller than the viewport extent. Users can now use the alignment property to center or otherwise position the entire table/tree within the viewport. Alignment correctly reverts to start for axes that exceed viewport dimensions. Fixes flutter/flutter#170349 - TableView - Added an alignment property of type AlignmentGeometry (defaults to Alignment.topLeft). - Full support for both horizontal and vertical alignment. - Supports AlignmentDirectional to correctly handle TextDirection (LTR/RTL). - Works with pinned rows and columns, as well as reversed axis directions. - TreeView - Added an alignment property of type AlignmentGeometry (defaults to Alignment.topLeft). - Caveat: Currently only supports the vertical component of the alignment. The tree remains aligned to the horizontal "start" to maintain consistent indentation logic and avoid layout jumps caused by dynamic, lazily-loaded node widths. ## Pre-Review Checklist **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
1 parent cc67446 commit 0dd2410

7 files changed

Lines changed: 872 additions & 30 deletions

File tree

packages/two_dimensional_scrollables/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.4.0
2+
3+
* Added `alignment` property to `TableView` and `TreeView` to align content within the viewport when it is smaller than the viewport extent.
4+
15
## 0.3.9
26

37
* Fixes TableSpan borders being flipped when one or both axis directions are reversed.

packages/two_dimensional_scrollables/lib/src/table_view/table.dart

Lines changed: 100 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ class TableView extends TwoDimensionalScrollView {
116116
super.dragStartBehavior,
117117
super.keyboardDismissBehavior,
118118
super.clipBehavior,
119+
this.alignment = Alignment.topLeft,
119120
});
120121

121122
/// Creates a [TableView] of widgets that are created on demand.
@@ -155,6 +156,7 @@ class TableView extends TwoDimensionalScrollView {
155156
required TableSpanBuilder columnBuilder,
156157
required TableSpanBuilder rowBuilder,
157158
required TableViewCellBuilder cellBuilder,
159+
this.alignment = Alignment.topLeft,
158160
}) : assert(pinnedRowCount >= 0),
159161
assert(rowCount == null || rowCount >= 0),
160162
assert(rowCount == null || rowCount >= pinnedRowCount),
@@ -199,6 +201,7 @@ class TableView extends TwoDimensionalScrollView {
199201
required TableSpanBuilder columnBuilder,
200202
required TableSpanBuilder rowBuilder,
201203
List<List<TableViewCell>> cells = const <List<TableViewCell>>[],
204+
this.alignment = Alignment.topLeft,
202205
}) : assert(pinnedRowCount >= 0),
203206
assert(pinnedColumnCount >= 0),
204207
super(
@@ -211,6 +214,11 @@ class TableView extends TwoDimensionalScrollView {
211214
),
212215
);
213216

217+
/// The alignment of the table within the viewport when there is extra space.
218+
///
219+
/// Defaults to [Alignment.topLeft].
220+
final AlignmentGeometry alignment;
221+
214222
@override
215223
TableViewport buildViewport(
216224
BuildContext context,
@@ -226,6 +234,7 @@ class TableView extends TwoDimensionalScrollView {
226234
mainAxis: mainAxis,
227235
cacheExtent: cacheExtent,
228236
clipBehavior: clipBehavior,
237+
alignment: alignment,
229238
);
230239
}
231240
}
@@ -245,8 +254,12 @@ class TableViewport extends TwoDimensionalViewport {
245254
required super.mainAxis,
246255
super.cacheExtent,
247256
super.clipBehavior,
257+
this.alignment = Alignment.topLeft,
248258
});
249259

260+
/// The alignment of the table within the viewport when there is extra space.
261+
final AlignmentGeometry alignment;
262+
250263
@override
251264
RenderTwoDimensionalViewport createRenderObject(BuildContext context) {
252265
return RenderTableViewport(
@@ -259,6 +272,8 @@ class TableViewport extends TwoDimensionalViewport {
259272
clipBehavior: clipBehavior,
260273
delegate: delegate as TableCellDelegateMixin,
261274
childManager: context as TwoDimensionalChildManager,
275+
alignment: alignment,
276+
textDirection: Directionality.maybeOf(context),
262277
);
263278
}
264279

@@ -275,7 +290,9 @@ class TableViewport extends TwoDimensionalViewport {
275290
..mainAxis = mainAxis
276291
..cacheExtent = cacheExtent
277292
..clipBehavior = clipBehavior
278-
..delegate = delegate as TableCellDelegateMixin;
293+
..delegate = delegate as TableCellDelegateMixin
294+
..alignment = alignment
295+
..textDirection = Directionality.maybeOf(context);
279296
}
280297
}
281298

@@ -299,7 +316,10 @@ class RenderTableViewport extends RenderTwoDimensionalViewport {
299316
required super.childManager,
300317
super.cacheExtent,
301318
super.clipBehavior,
302-
});
319+
AlignmentGeometry alignment = Alignment.topLeft,
320+
TextDirection? textDirection,
321+
}) : _alignment = alignment,
322+
_textDirection = textDirection;
303323

304324
@override
305325
TableCellDelegateMixin get delegate =>
@@ -309,6 +329,31 @@ class RenderTableViewport extends RenderTwoDimensionalViewport {
309329
super.delegate = value;
310330
}
311331

332+
/// The alignment of the table within the viewport when there is extra space.
333+
AlignmentGeometry get alignment => _alignment;
334+
AlignmentGeometry _alignment;
335+
set alignment(AlignmentGeometry value) {
336+
if (_alignment == value) {
337+
return;
338+
}
339+
_alignment = value;
340+
markNeedsLayout();
341+
}
342+
343+
/// The text direction with which to resolve [alignment].
344+
TextDirection? get textDirection => _textDirection;
345+
TextDirection? _textDirection;
346+
set textDirection(TextDirection? value) {
347+
if (_textDirection == value) {
348+
return;
349+
}
350+
_textDirection = value;
351+
markNeedsLayout();
352+
}
353+
354+
double _hAlignmentOffset = 0.0;
355+
double _vAlignmentOffset = 0.0;
356+
312357
// Skipped vicinities for the current frame based on merged cells.
313358
// This prevents multiple build calls for the same cell that spans multiple
314359
// vicinities.
@@ -852,6 +897,33 @@ class RenderTableViewport extends RenderTwoDimensionalViewport {
852897
_updateFirstAndLastVisibleCell();
853898
}
854899

900+
final Alignment resolvedAlignment = alignment.resolve(textDirection);
901+
_hAlignmentOffset = 0.0;
902+
if (!_columnsAreInfinite && _columnMetrics.isNotEmpty) {
903+
final double totalWidth =
904+
_pinnedColumnsExtent +
905+
_columnMetrics[delegate.columnCount! - 1]!.trailingOffset;
906+
if (totalWidth < viewportDimension.width) {
907+
_hAlignmentOffset =
908+
(viewportDimension.width - totalWidth) *
909+
(resolvedAlignment.x + 1.0) /
910+
2.0;
911+
}
912+
}
913+
914+
_vAlignmentOffset = 0.0;
915+
if (!_rowsAreInfinite && _rowMetrics.isNotEmpty) {
916+
final double totalHeight =
917+
_pinnedRowsExtent +
918+
_rowMetrics[delegate.rowCount! - 1]!.trailingOffset;
919+
if (totalHeight < viewportDimension.height) {
920+
_vAlignmentOffset =
921+
(viewportDimension.height - totalHeight) *
922+
(resolvedAlignment.y + 1.0) /
923+
2.0;
924+
}
925+
}
926+
855927
if (_firstNonPinnedCell == null &&
856928
_lastPinnedRow == null &&
857929
_lastPinnedColumn == null) {
@@ -862,19 +934,21 @@ class RenderTableViewport extends RenderTwoDimensionalViewport {
862934
final double? offsetIntoColumn = _firstNonPinnedColumn != null
863935
? horizontalOffset.pixels -
864936
_columnMetrics[_firstNonPinnedColumn]!.leadingOffset -
865-
_pinnedColumnsExtent
937+
_pinnedColumnsExtent -
938+
_hAlignmentOffset
866939
: null;
867940
final double? offsetIntoRow = _firstNonPinnedRow != null
868941
? verticalOffset.pixels -
869942
_rowMetrics[_firstNonPinnedRow]!.leadingOffset -
870-
_pinnedRowsExtent
943+
_pinnedRowsExtent -
944+
_vAlignmentOffset
871945
: null;
872946
if (_lastPinnedRow != null && _lastPinnedColumn != null) {
873947
// Layout cells that are contained in both pinned rows and columns
874948
_layoutCells(
875949
start: TableVicinity.zero,
876950
end: TableVicinity(column: _lastPinnedColumn!, row: _lastPinnedRow!),
877-
offset: Offset.zero,
951+
offset: Offset(-_hAlignmentOffset, -_vAlignmentOffset),
878952
);
879953
}
880954

@@ -886,7 +960,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport {
886960
_layoutCells(
887961
start: TableVicinity(column: _firstNonPinnedColumn!, row: 0),
888962
end: TableVicinity(column: _lastNonPinnedColumn!, row: _lastPinnedRow!),
889-
offset: Offset(offsetIntoColumn!, 0),
963+
offset: Offset(offsetIntoColumn!, -_vAlignmentOffset),
890964
);
891965
}
892966
if (_lastPinnedColumn != null && _firstNonPinnedRow != null) {
@@ -897,7 +971,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport {
897971
_layoutCells(
898972
start: TableVicinity(column: 0, row: _firstNonPinnedRow!),
899973
end: TableVicinity(column: _lastPinnedColumn!, row: _lastNonPinnedRow!),
900-
offset: Offset(0, offsetIntoRow!),
974+
offset: Offset(-_hAlignmentOffset, offsetIntoRow!),
901975
);
902976
}
903977
if (_firstNonPinnedCell != null) {
@@ -1176,19 +1250,20 @@ class RenderTableViewport extends RenderTwoDimensionalViewport {
11761250
// follows row or column major ordering. Here is slightly different
11771251
// as we break the cells up into 4 main paint passes to clip for overlap.
11781252

1253+
final bool reversedH = axisDirectionIsReversed(horizontalAxisDirection);
1254+
final bool reversedV = axisDirectionIsReversed(verticalAxisDirection);
1255+
11791256
if (_firstNonPinnedCell != null) {
11801257
// Paint all visible un-pinned cells
11811258
assert(_lastNonPinnedCell != null);
11821259
_clipCellsHandle.layer = context.pushClipRect(
11831260
needsCompositing,
11841261
offset,
11851262
Rect.fromLTWH(
1186-
axisDirectionIsReversed(horizontalAxisDirection)
1187-
? 0.0
1188-
: _pinnedColumnsExtent,
1189-
axisDirectionIsReversed(verticalAxisDirection)
1190-
? 0.0
1191-
: _pinnedRowsExtent,
1263+
(reversedH ? 0.0 : _pinnedColumnsExtent) +
1264+
(reversedH ? -_hAlignmentOffset : _hAlignmentOffset),
1265+
(reversedV ? 0.0 : _pinnedRowsExtent) +
1266+
(reversedV ? -_vAlignmentOffset : _vAlignmentOffset),
11921267
viewportDimension.width - _pinnedColumnsExtent,
11931268
viewportDimension.height - _pinnedRowsExtent,
11941269
),
@@ -1214,12 +1289,13 @@ class RenderTableViewport extends RenderTwoDimensionalViewport {
12141289
needsCompositing,
12151290
offset,
12161291
Rect.fromLTWH(
1217-
axisDirectionIsReversed(horizontalAxisDirection)
1218-
? viewportDimension.width - _pinnedColumnsExtent
1219-
: 0.0,
1220-
axisDirectionIsReversed(verticalAxisDirection)
1221-
? 0.0
1222-
: _pinnedRowsExtent,
1292+
reversedH
1293+
? viewportDimension.width -
1294+
_pinnedColumnsExtent -
1295+
_hAlignmentOffset
1296+
: _hAlignmentOffset,
1297+
(reversedV ? 0.0 : _pinnedRowsExtent) +
1298+
(reversedV ? -_vAlignmentOffset : _vAlignmentOffset),
12231299
_pinnedColumnsExtent,
12241300
viewportDimension.height - _pinnedRowsExtent,
12251301
),
@@ -1248,12 +1324,11 @@ class RenderTableViewport extends RenderTwoDimensionalViewport {
12481324
needsCompositing,
12491325
offset,
12501326
Rect.fromLTWH(
1251-
axisDirectionIsReversed(horizontalAxisDirection)
1252-
? 0.0
1253-
: _pinnedColumnsExtent,
1254-
axisDirectionIsReversed(verticalAxisDirection)
1255-
? viewportDimension.height - _pinnedRowsExtent
1256-
: 0.0,
1327+
(reversedH ? 0.0 : _pinnedColumnsExtent) +
1328+
(reversedH ? -_hAlignmentOffset : _hAlignmentOffset),
1329+
reversedV
1330+
? viewportDimension.height - _pinnedRowsExtent - _vAlignmentOffset
1331+
: _vAlignmentOffset,
12571332
viewportDimension.width - _pinnedColumnsExtent,
12581333
_pinnedRowsExtent,
12591334
),

packages/two_dimensional_scrollables/lib/src/tree_view/render_tree.dart

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,13 @@ class RenderTreeViewport extends RenderTwoDimensionalViewport {
3838
required super.childManager,
3939
super.cacheExtent,
4040
super.clipBehavior,
41+
AlignmentGeometry alignment = Alignment.topLeft,
42+
TextDirection? textDirection,
4143
}) : _activeAnimations = activeAnimations,
4244
_rowDepths = rowDepths,
4345
_indentation = indentation,
46+
_alignment = alignment,
47+
_textDirection = textDirection,
4448
assert(indentation >= 0),
4549
assert(
4650
verticalAxisDirection == AxisDirection.down &&
@@ -56,6 +60,30 @@ class RenderTreeViewport extends RenderTwoDimensionalViewport {
5660
super.delegate = value;
5761
}
5862

63+
/// The alignment of the tree within the viewport when there is extra space.
64+
AlignmentGeometry get alignment => _alignment;
65+
AlignmentGeometry _alignment;
66+
set alignment(AlignmentGeometry value) {
67+
if (_alignment == value) {
68+
return;
69+
}
70+
_alignment = value;
71+
markNeedsLayout();
72+
}
73+
74+
/// The text direction with which to resolve [alignment].
75+
TextDirection? get textDirection => _textDirection;
76+
TextDirection? _textDirection;
77+
set textDirection(TextDirection? value) {
78+
if (_textDirection == value) {
79+
return;
80+
}
81+
_textDirection = value;
82+
markNeedsLayout();
83+
}
84+
85+
double _vAlignmentOffset = 0.0;
86+
5987
/// The currently active [TreeViewNode] animations.
6088
///
6189
/// Since the index of animating nodes can change at any time, the unique key
@@ -348,6 +376,19 @@ class RenderTreeViewport extends RenderTwoDimensionalViewport {
348376
_updateFirstAndLastVisibleRow();
349377
}
350378

379+
final Alignment resolvedAlignment = alignment.resolve(textDirection);
380+
_vAlignmentOffset = 0.0;
381+
if (_rowMetrics.isNotEmpty) {
382+
final double totalHeight =
383+
_rowMetrics[_rowMetrics.length - 1]!.trailingOffset;
384+
if (totalHeight < viewportDimension.height) {
385+
_vAlignmentOffset =
386+
(viewportDimension.height - totalHeight) *
387+
(resolvedAlignment.y + 1.0) /
388+
2.0;
389+
}
390+
}
391+
351392
if (_firstRow == null) {
352393
assert(_lastRow == null);
353394
return;
@@ -356,7 +397,9 @@ class RenderTreeViewport extends RenderTwoDimensionalViewport {
356397

357398
_Span rowSpan;
358399
double rowOffset =
359-
-verticalOffset.pixels + _rowMetrics[_firstRow!]!.leadingOffset;
400+
-verticalOffset.pixels +
401+
_rowMetrics[_firstRow!]!.leadingOffset +
402+
_vAlignmentOffset;
360403
for (int row = _firstRow!; row <= _lastRow!; row++) {
361404
rowSpan = _rowMetrics[row]!;
362405
final double rowHeight = rowSpan.extent;
@@ -489,11 +532,11 @@ class RenderTreeViewport extends RenderTwoDimensionalViewport {
489532
final double trailingOffset =
490533
_rowMetrics[segment.trailingIndex]!.trailingOffset;
491534
final rect = Rect.fromPoints(
492-
Offset(0.0, leadingOffset - verticalOffset.pixels),
535+
Offset(0.0, leadingOffset - verticalOffset.pixels + _vAlignmentOffset),
493536
Offset(
494537
viewportDimension.width,
495538
math.min(
496-
trailingOffset - verticalOffset.pixels,
539+
trailingOffset - verticalOffset.pixels + _vAlignmentOffset,
497540
viewportDimension.height,
498541
),
499542
),

0 commit comments

Comments
 (0)