Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
36 changes: 36 additions & 0 deletions src/__tests__/LinearLayoutManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,42 @@ describe("LinearLayoutManager", () => {
});
});

describe("Position consistency after partial recomputation", () => {
it("should maintain monotonic y positions when early items grow", () => {
// Create a large list with uniform heights
const itemCount = 500;
const manager = createPopulatedLayoutManager(
LayoutManagerType.LINEAR,
itemCount,
defaultParams,
400,
100
);

// Verify initial positions are correct
const initialLayouts = getAllLayouts(manager);
expect(initialLayouts[499].y).toBe(49900);

// Now simulate items 0-9 being re-measured with much larger heights.
// This will cause the average height estimate to increase, making
// the recomputed range's positions shift forward while items beyond
// the recomputed range still have their old (lower) positions.
const updatedLayoutInfos = [];
for (let i = 0; i < 10; i++) {
updatedLayoutInfos.push(createMockLayoutInfo(i, 400, 300));
}
manager.modifyLayout(updatedLayoutInfos, itemCount);

// Verify all positions are monotonically increasing (no backwards jumps)
const layouts = getAllLayouts(manager);
for (let i = 1; i < itemCount; i++) {
expect(layouts[i].y).toBeGreaterThanOrEqual(
layouts[i - 1].y + layouts[i - 1].height
);
}
});
});

describe("Stale layoutInfo handling", () => {
it("should filter out stale indices when layouts are truncated", () => {
// Start with 5 items
Expand Down
15 changes: 12 additions & 3 deletions src/recyclerview/layout-managers/LayoutManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,9 +359,18 @@ export abstract class RVLayoutManager {
this.lastSkippedLayoutIndex
);
const lastIndex = this.layouts.length - 1;
// Since layout managers derive height from last indices we need to make
// sure they're not too much out of sync.
if (this.layouts[lastIndex].y < this.layouts[endIndex].y) {
// Check if the item immediately after the recomputed range has a stale
// position that goes backwards relative to the last recomputed item.
// When dynamic item heights cause the average estimate to grow, items
// in the recomputed range shift forward while items beyond it keep
// their old (lower) positions. This breaks the monotonic ordering
// that the binary search in getVisibleLayouts relies on, causing
// items to overlap and jump during fast scrolling.
const endLayout = this.layouts[endIndex];
const nextLayout = this.layouts[endIndex + 1];
const endPos = this.horizontal ? endLayout.x : endLayout.y;
const nextPos = this.horizontal ? nextLayout.x : nextLayout.y;
if (nextPos < endPos) {
this.recomputeLayouts(this.lastSkippedLayoutIndex, lastIndex);
this.lastSkippedLayoutIndex = Number.MAX_VALUE;
}
Expand Down