fix(layout): keep tall masonry items in the visible range - #2461
Open
dennytosp wants to merge 1 commit into
Open
fix(layout): keep tall masonry items in the visible range#2461dennytosp wants to merge 1 commit into
dennytosp wants to merge 1 commit into
Conversation
getVisibleLayouts() binary searches for the first item whose end passes the viewport start. That needs item ends to grow with the index, which holds for a linear or grid layout because each item starts where the previous row ended. Masonry breaks it: an item goes in the shortest column, so a tall one can start above the viewport and reach well past a short item placed after it. The search settles on a later index, or on nothing at all, and every item from the tall one up to it is dropped from the engaged range and never renders. Item starts are still ordered, since an item is placed at the height of the shortest column and column heights only grow. Search on those instead: the first item starting after the viewport begins is visible by definition and bounds the answer from above, then walk back over the items that could still reach the viewport start, stopping once one cannot reach it even at the tallest height seen. Sequential placement (optimizeItemArrangement: false) leaves item starts unordered too, so neither search applies there; that path is untouched and still goes through the base implementation. Related to Shopify#2103
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
RVLayoutManager.getVisibleLayouts()finds the first visible item by binary searching for the first index whose end passes the viewport start (findVisibleIndex.ts:38). A binary search needs that predicate to be monotonic in the index, which means item ends have to grow with the index.They do in a linear or grid layout — each item starts where the previous row ended. They do not in masonry: an item is placed in the shortest column, so a tall item can start above the viewport and reach well past a short item placed after it. The search then settles on a later index, or walks past the tall item entirely and returns nothing, and every item from the tall one up to where it landed is dropped from the range.
That range is what gets rendered —
EngagedIndicesTracker.updateScrollOffset()returns it as the engaged indices (EngagedIndicesTracker.ts:166) — so a dropped item is a blank space in its column. ThedrawDistancebuffer hides it for ordinary item sizes, which is likely why it has been hard to pin down; an item taller than the viewport plus the buffer is not covered.I found this while differential-testing
getVisibleLayouts()against a brute force scan. Every layout manager, 1–4 columns, with and without spans, four item-size regimes, varied viewport heights, offsets swept past both ends — 665,568 windows:Linear and grid are exact both before and after, so the change is confined to the layout that was actually wrong.
RecyclerViewManager.computeItemViewability()already routes masonry through the wider engaged range with the comment "Using higher buffer for masonry to avoid missing items" — the same symptom, worked around one layer up.Fix
Item starts are still ordered in masonry, because an item goes at the height of the shortest column and column heights only grow. So search on those instead:
The tallest height is kept as a running maximum; over-estimating only widens the walk, it can never hide an item. Measured across 165,898 calls in the sweep: mean 4.1 iterations, max 15. Cost is one binary search swapped for another plus a handful of array reads.
Verification
src/__tests__/MasonryVisibleRange.test.ts— two hand-built cases, the differential sweep, and one test throughEngagedIndicesTrackerproving the item is actually engaged. All four fail onmainand pass here:mainyarn test191 passed / 15 suites,yarn type-checkandyarn lintclean.Not addressed
Related to #2103, but this does not fix that issue's reproduction. #2103 is reported with
optimizeItemArrangement={false}, and that path fills columns in turn rather than by height, so item starts are unordered too:Neither binary search is valid there, and the same sweep still reports 4,072 / 7,861 windows wrong on that path. I left it going through the base implementation rather than guess at it — columns diverge without bound in sequential mode, so a correct fix needs a per-column search, which is a larger change than this one. The reporter of #2103 identified this same root cause; this PR fixes the half of it that sits on the default code path, which their repro does not exercise.