Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions packages/two_dimensional_scrollables/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

* 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,29 @@ 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 +773,15 @@ 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 +803,15 @@ 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
Original file line number Diff line number Diff line change
Expand Up @@ -2130,6 +2130,44 @@ 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));

// single rows & columns
await tester.pumpWidget(
MaterialApp(
home: getTableView(columnCount: 1, rowCount: 1),
),
);
await tester.pumpAndSettle();
expect(find.text('R0:C0'), findsOneWidget);
expect(find.text('R0:C1'), findsNothing);
await tester.pumpWidget(Container());
});
});
});

Expand Down