Skip to content
Open
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
59 changes: 59 additions & 0 deletions src/__tests__/LinearLayoutManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,4 +288,63 @@ describe("LinearLayoutManager", () => {
expect(getAllLayouts(manager).length).toBe(3);
});
});
describe("Tail sync after a partial recompute", () => {
// A recompute pass is capped at maxItemsToProcess items. When the items it
// touched end up positioned past the (untouched) last item, the layout
// manager has to recompute the tail as well, otherwise the reported content
// size is stale and the tail overlaps the items that were just moved.
const itemCount = 300;
const measuredCount = 25;
const measuredSize = 1000;

it("recomputes the tail of a vertical list", () => {
const manager = createLayoutManager(
LayoutManagerType.LINEAR,
defaultParams
);
manager.modifyLayout([], itemCount);

manager.modifyLayout(
Array.from({ length: measuredCount }, (_, index) =>
createMockLayoutInfo(index, 400, measuredSize)
),
itemCount
);

const layouts = getAllLayouts(manager);
for (let index = 1; index < itemCount; index++) {
expect(layouts[index].y).toBeGreaterThanOrEqual(layouts[index - 1].y);
}
const lastLayout = layouts[itemCount - 1];
expect(manager.getLayoutSize().height).toBe(
lastLayout.y + lastLayout.height
);
});

it("recomputes the tail of a horizontal list", () => {
const manager = createLayoutManager(
LayoutManagerType.LINEAR,
horizontalParams
);
manager.modifyLayout([], itemCount);

manager.modifyLayout(
Array.from({ length: measuredCount }, (_, index) =>
createMockLayoutInfo(index, measuredSize, 300)
),
itemCount
);

const layouts = getAllLayouts(manager);
// Every item must still start at or after the previous one - a stale tail
// shows up here as item n sitting on top of much earlier items.
for (let index = 1; index < itemCount; index++) {
expect(layouts[index].x).toBeGreaterThanOrEqual(layouts[index - 1].x);
}
const lastLayout = layouts[itemCount - 1];
expect(manager.getLayoutSize().width).toBe(
lastLayout.x + lastLayout.width
);
});
});
});
18 changes: 15 additions & 3 deletions src/recyclerview/layout-managers/LayoutManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,15 +359,27 @@ 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) {
// Since layout managers derive the content size from the last indices we
// need to make sure they're not too much out of sync. The position that
// grows along the list is x for horizontal layouts and y otherwise.
const lastPosition = this.getScrollAxisPosition(this.layouts[lastIndex]);
const endPosition = this.getScrollAxisPosition(this.layouts[endIndex]);
if (lastPosition < endPosition) {
this.recomputeLayouts(this.lastSkippedLayoutIndex, lastIndex);
this.lastSkippedLayoutIndex = Number.MAX_VALUE;
}
}
}

/**
* Returns the position of a layout along the scroll axis.
* @param layout Layout to read the position from
* @returns x for horizontal layouts, y otherwise
*/
private getScrollAxisPosition(layout: RVLayout): number {
return this.horizontal ? layout.x : layout.y;
}

/**
* Computes size estimates and finds the minimum recompute index.
* @param layoutInfo Array of layout information for items
Expand Down
Loading