diff --git a/packages/two_dimensional_scrollables/CHANGELOG.md b/packages/two_dimensional_scrollables/CHANGELOG.md index b4a04b4830b5..a201be420b1f 100644 --- a/packages/two_dimensional_scrollables/CHANGELOG.md +++ b/packages/two_dimensional_scrollables/CHANGELOG.md @@ -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 diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart index df7359f824c2..3f26dd00dfd3 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -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 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 @@ -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; } @@ -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; } diff --git a/packages/two_dimensional_scrollables/pubspec.yaml b/packages/two_dimensional_scrollables/pubspec.yaml index 2bec6d659397..c17706a2af68 100644 --- a/packages/two_dimensional_scrollables/pubspec.yaml +++ b/packages/two_dimensional_scrollables/pubspec.yaml @@ -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+ diff --git a/packages/two_dimensional_scrollables/test/table_view/table_test.dart b/packages/two_dimensional_scrollables/test/table_view/table_test.dart index 08a34332e154..50ec9d4640db 100644 --- a/packages/two_dimensional_scrollables/test/table_view/table_test.dart +++ b/packages/two_dimensional_scrollables/test/table_view/table_test.dart @@ -2130,6 +2130,35 @@ void main() { ), ); }); + + testWidgets('Binary search correctly finds first/last non-pinned cells', ( + WidgetTester tester, + ) async { + Future 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)); + }); }); });