Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
8e827ab
[two_dimensional_scrollables] Fix tableview janks with >250k rows
wangfeihang Jan 7, 2026
a7ddef7
Merge branch 'main' into fix_tableview_janks_250krow
wangfeihang Jan 7, 2026
05afa54
[two_dimensional_scrollables] Fix tableview janks with >250k rows
wangfeihang Jan 7, 2026
da820aa
[two_dimensional_scrollables] Fix tableview janks with >250k rows, fi…
wangfeihang Jan 8, 2026
06379d7
Merge branch 'main' into fix_tableview_janks_250krow
wangfeihang Jan 8, 2026
9c9b305
Merge branch 'main' into fix_tableview_janks_250krow
wangfeihang Jan 9, 2026
5d8a353
[two_dimensional_scrollables] Fix tableview janks with >250k rows, fi…
wangfeihang Jan 10, 2026
fe80037
[two_dimensional_scrollables] Fix tableview janks with >250k rows, fi…
wangfeihang Jan 10, 2026
c2fd71e
Merge branch 'main' into fix_tableview_janks_250krow
wangfeihang Jan 11, 2026
d03d6b6
Merge branch 'flutter:main' into fix_tableview_janks_250krow
wangfeihang Jan 13, 2026
20de631
[two_dimensional_scrollables] Fix tableview janks with >250k rows, fi…
wangfeihang Jan 13, 2026
1b5f61a
Merge branch 'main' into fix_tableview_janks_250krow
wangfeihang Jan 14, 2026
f9387bf
Merge branch 'main' into fix_tableview_janks_250krow
wangfeihang Jan 15, 2026
fbb5ff7
Merge branch 'main' into fix_tableview_janks_250krow
wangfeihang Jan 16, 2026
d0ecf1b
[two_dimensional_scrollables] Fix tableview janks with >250k rows, fi…
wangfeihang Jan 17, 2026
6f5f15a
Merge branch 'main' into fix_tableview_janks_250krow
wangfeihang Jan 17, 2026
78cf1b3
Merge branch 'main' into fix_tableview_janks_250krow
wangfeihang Jan 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/two_dimensional_scrollables/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
## NEXT
## 0.3.8

* Updates minimum supported SDK version to Flutter 3.35/Dart 3.9.
* Updates examples to use the new RadioGroup API instead of deprecated Radio parameters.
* Optimizes tableview janks with >250k rows.

## 0.3.7

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,32 @@ class RenderTableViewport extends RenderTwoDimensionalViewport {
return verticalOffset.applyContentDimensions(0.0, maxVerticalScrollExtent);
}

/// Binary search to find the first index with [_Span] matching the condition.
/// [map]: Index-[_Span] map, [condition]: Match rule
/// Returns the first matched index or null if not found.
int? _binarySearchFirstFromMap(
Map<int, _Span> map,
bool Function(_Span) condition,
) {
if (map.isEmpty) {
return null;
}
var low = 0;
int high = map.length - 1;
int? result;
while (low <= high) {
final int mid = low + ((high - low) >> 1);
final _Span span = map[mid]!;
if (condition(span)) {
result = mid;
high = mid - 1;
} else {
low = mid + 1;
}
}
return result;
}

// Uses the cached metrics to update the currently visible cells. If the
// number of rows or columns are infinite, the layout is computed lazily, so
// this will call for an update to the metrics if we have scrolled beyond the
Expand All @@ -750,21 +776,17 @@ class RenderTableViewport extends RenderTwoDimensionalViewport {
}
_firstNonPinnedColumn = null;
_lastNonPinnedColumn = null;
for (var column = 0; column < _columnMetrics.length; column++) {
if (_columnMetrics[column]!.isPinned) {
continue;
}
final double endOfColumn = _columnMetrics[column]!.trailingOffset;
if (endOfColumn >= _targetLeadingColumnPixel &&
_firstNonPinnedColumn == null) {
_firstNonPinnedColumn = column;
}
if (endOfColumn >= _targetTrailingColumnPixel &&
_lastNonPinnedColumn == null) {
_lastNonPinnedColumn = column;
break;
}
}
// Binary search replaces for-loop to reduce computation.
_firstNonPinnedColumn = _binarySearchFirstFromMap(
_columnMetrics,
(span) =>
!span.isPinned && span.trailingOffset >= _targetLeadingColumnPixel,
);
_lastNonPinnedColumn = _binarySearchFirstFromMap(
_columnMetrics,
(span) =>
!span.isPinned && span.trailingOffset >= _targetTrailingColumnPixel,
);
if (_firstNonPinnedColumn != null) {
_lastNonPinnedColumn ??= _columnMetrics.length - 1;
}
Expand All @@ -786,19 +808,16 @@ class RenderTableViewport extends RenderTwoDimensionalViewport {
}
_firstNonPinnedRow = null;
_lastNonPinnedRow = null;
for (var row = 0; row < _rowMetrics.length; row++) {
if (_rowMetrics[row]!.isPinned) {
continue;
}
final double endOfRow = _rowMetrics[row]!.trailingOffset;
if (endOfRow >= _targetLeadingRowPixel && _firstNonPinnedRow == null) {
_firstNonPinnedRow = row;
}
if (endOfRow >= _targetTrailingRowPixel && _lastNonPinnedRow == null) {
_lastNonPinnedRow = row;
break;
}
}
// Binary search replaces for-loop to reduce computation.
_firstNonPinnedRow = _binarySearchFirstFromMap(
_rowMetrics,
(span) => !span.isPinned && span.trailingOffset >= _targetLeadingRowPixel,
);
_lastNonPinnedRow = _binarySearchFirstFromMap(
_rowMetrics,
(span) =>
!span.isPinned && span.trailingOffset >= _targetTrailingRowPixel,
);
if (_firstNonPinnedRow != null) {
_lastNonPinnedRow ??= _rowMetrics.length - 1;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/two_dimensional_scrollables/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: two_dimensional_scrollables
description: Widgets that scroll using the two dimensional scrolling foundation.
version: 0.3.7
version: 0.3.8
repository: https://github.com/flutter/packages/tree/main/packages/two_dimensional_scrollables
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+two_dimensional_scrollables%22+

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2130,6 +2130,35 @@ void main() {
),
);
});

testWidgets('Binary search correctly finds first/last non-pinned cells', (
WidgetTester tester,
) async {
Future<void> runScrollTest(Widget tableView) async {
await tester.pumpWidget(MaterialApp(home: tableView));
await tester.pumpAndSettle();
expect(verticalController.position.pixels, 0.0);
expect(horizontalController.position.pixels, 0.0);
expect(find.text('R0:C0'), findsOneWidget);
expect(find.text('R4:C5'), findsOneWidget);
// No columns laid out beyond column 5.
expect(find.text('R0:C6'), findsNothing);
// Change the vertical scroll offset, validate more rows were
verticalController.jumpTo(1000000.0);
await tester.pump();
expect(find.text('R5000:C0'), findsOneWidget);
expect(find.text('R5004:C0'), findsOneWidget);
expect(find.text('R4990:C0'), findsNothing); // Not laid out
expect(find.text('R5007:C0'), findsNothing); // Not laid out
await tester.pumpWidget(Container());
}

// infinite rows & columns
await runScrollTest(getTableView());

// finite rows & columns
await runScrollTest(getTableView(rowCount: 10000, columnCount: 200));
});
});
});

Expand Down