-
Notifications
You must be signed in to change notification settings - Fork 360
fix(layout): avoid binary search for masonry visible item detection #2207
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
9c10766
20c935c
043d78d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| import { ConsecutiveNumbers } from "../helpers/ConsecutiveNumbers"; | ||
|
|
||
| import { | ||
| LayoutParams, | ||
| RVDimension, | ||
|
|
@@ -18,6 +20,8 @@ export class RVMasonryLayoutManagerImpl extends RVLayoutManager { | |
| private columnHeights: number[]; | ||
| /** Current column index for sequential placement */ | ||
| private currentColumn = 0; | ||
| /** Per-column sorted lists of item indices for efficient visibility detection */ | ||
| private columnItems: number[][] = []; | ||
|
|
||
| /** If there's a span change for masonry layout, we need to recompute all the widths */ | ||
| private fullRelayoutRequired = false; | ||
|
|
@@ -155,6 +159,8 @@ export class RVMasonryLayoutManagerImpl extends RVLayoutManager { | |
| this.placeItemSequentially(layout, span); | ||
| } | ||
| } | ||
| // Rebuild per-column item mappings for efficient visibility detection | ||
| this.rebuildColumnItems(); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -321,6 +327,96 @@ export class RVMasonryLayoutManagerImpl extends RVLayoutManager { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Rebuilds per-column item index arrays from layout positions. | ||
| * Within each column, items are sorted by y-position (guaranteed by | ||
| * placement order since column heights only increase). | ||
| */ | ||
| private rebuildColumnItems(): void { | ||
| const columnWidth = this.boundedSize / this.maxColumns; | ||
| this.columnItems = Array.from({ length: this.maxColumns }, () => []); | ||
| for (let i = 0; i < this.layouts.length; i++) { | ||
| const layout = this.layouts[i]; | ||
| const col = Math.min( | ||
| Math.round(layout.x / columnWidth), | ||
| this.maxColumns - 1 | ||
| ); | ||
| this.columnItems[col].push(i); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Override getVisibleLayouts to use per-column binary search instead of | ||
| * a single binary search on the full layouts array. In masonry layout, | ||
| * y-positions are NOT sorted by index (items are placed in columns), but | ||
| * they ARE sorted within each column. Binary searching each column gives | ||
| * O(numColumns * log(n/numColumns)) performance. | ||
| */ | ||
| getVisibleLayouts( | ||
| unboundDimensionStart: number, | ||
| unboundDimensionEnd: number | ||
| ): ConsecutiveNumbers { | ||
| if (this.layouts.length === 0) { | ||
| return ConsecutiveNumbers.EMPTY; | ||
| } | ||
|
|
||
| let firstVisibleIndex = -1; | ||
| let lastVisibleIndex = -1; | ||
|
|
||
| for (const items of this.columnItems) { | ||
| if (items.length === 0) continue; | ||
|
Comment on lines
+355
to
+367
|
||
|
|
||
| // Binary search for first visible item in this column: | ||
| // find first item where y + height > unboundDimensionStart | ||
| let lo = 0; | ||
| let hi = items.length - 1; | ||
| let colFirst = -1; | ||
| while (lo <= hi) { | ||
| const mid = (lo + hi) >>> 1; | ||
| const layout = this.layouts[items[mid]]; | ||
| if (layout.y + layout.height > unboundDimensionStart) { | ||
| colFirst = mid; | ||
| hi = mid - 1; | ||
| } else { | ||
| lo = mid + 1; | ||
| } | ||
| } | ||
| if (colFirst === -1) continue; | ||
|
|
||
| // Binary search for last visible item in this column: | ||
| // find last item where y < unboundDimensionEnd | ||
| lo = colFirst; | ||
| hi = items.length - 1; | ||
| let colLast = -1; | ||
| while (lo <= hi) { | ||
| const mid = (lo + hi) >>> 1; | ||
| const layout = this.layouts[items[mid]]; | ||
| if (layout.y < unboundDimensionEnd) { | ||
| colLast = mid; | ||
| lo = mid + 1; | ||
| } else { | ||
| hi = mid - 1; | ||
|
Comment on lines
+392
to
+398
|
||
| } | ||
| } | ||
| if (colLast === -1) continue; | ||
|
|
||
| // Update global min/max with item indices from this column | ||
| const colFirstIndex = items[colFirst]; | ||
| const colLastIndex = items[colLast]; | ||
| if (firstVisibleIndex === -1 || colFirstIndex < firstVisibleIndex) { | ||
| firstVisibleIndex = colFirstIndex; | ||
| } | ||
| if (lastVisibleIndex === -1 || colLastIndex > lastVisibleIndex) { | ||
| lastVisibleIndex = colLastIndex; | ||
| } | ||
| } | ||
|
|
||
| if (firstVisibleIndex !== -1 && lastVisibleIndex !== -1) { | ||
| return new ConsecutiveNumbers(firstVisibleIndex, lastVisibleIndex); | ||
| } | ||
| return ConsecutiveNumbers.EMPTY; | ||
| } | ||
|
|
||
| // TODO: For masonry, the "last row" is the last item in each column. | ||
| // Override isInLastRow if ItemSeparatorComponent support is needed | ||
| // for masonry layouts. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR title/description says masonry visibility detection is fixed by “avoiding binary search” / doing a “linear scan”, but this implementation still uses binary search (per-column). Either update the PR description/title to match what’s implemented, or adjust the implementation/inline comment so they’re consistent.