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
52 changes: 52 additions & 0 deletions src/__tests__/MasonryLayoutManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,4 +199,56 @@ describe("MasonryLayoutManager", () => {
expect(getAllLayouts(manager).length).toBe(0);
});
});

describe("Visibility with uneven column heights (optimizeItemArrangement: false)", () => {
const unevenParams = {
windowSize,
maxColumns: 2,
optimizeItemArrangement: false,
};

// Creates a masonry layout with different column heights.
const createUnevenMasonryLayout = () => {
const manager = createLayoutManager(
LayoutManagerType.MASONRY,
unevenParams
);
const layoutInfos = [
createMockLayoutInfo(0, 200, 100),
createMockLayoutInfo(1, 200, 50),
createMockLayoutInfo(2, 200, 100),
createMockLayoutInfo(3, 200, 50),
createMockLayoutInfo(4, 200, 100),
createMockLayoutInfo(5, 200, 50),
createMockLayoutInfo(6, 200, 100),
createMockLayoutInfo(7, 200, 50),
];
manager.modifyLayout(layoutInfos, 8);
return manager;
};

it("should find items visible at the top of the layout", () => {
const manager = createUnevenMasonryLayout();

const visible = manager.getVisibleLayouts(0, 100);

expect(visible.includes(0)).toBe(true);
expect(visible.includes(1)).toBe(true);
expect(visible.includes(3)).toBe(true);
expect(visible.includes(6)).toBe(false);
});

it("should find visible items when viewport is in the middle (column heights diverge)", () => {
const manager = createUnevenMasonryLayout();

const visible = manager.getVisibleLayouts(100, 200);

expect(visible.includes(2)).toBe(true);
expect(visible.includes(5)).toBe(true);
expect(visible.includes(7)).toBe(true);
expect(visible.includes(0)).toBe(false);
expect(visible.includes(1)).toBe(false);

});
});
});
90 changes: 90 additions & 0 deletions src/recyclerview/layout-managers/MasonryLayoutManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
RVLayoutInfo,
RVLayoutManager,
} from "./LayoutManager";
import { ConsecutiveNumbers } from "../helpers/ConsecutiveNumbers";
import { findFirstVisibleIndex, findLastVisibleIndex } from "../utils/findVisibleIndex";

/**
* MasonryLayoutManager implementation that arranges items in a masonry/pinterest-style layout.
Expand All @@ -22,6 +24,16 @@ export class RVMasonryLayoutManagerImpl extends RVLayoutManager {
/** If there's a span change for masonry layout, we need to recompute all the widths */
private fullRelayoutRequired = false;

/**
* Sorted copy of layout indices, ordered by Y position.
* Only used when optimizeItemArrangement is false, where sequential placement can cause
* significant column height differences.
*/
private sortedLayoutIndices: number[] = [];

/** Layouts array sorted by y position, matching sortedLayoutIndices order. */
private sortedLayouts: RVLayout[] = [];

constructor(params: LayoutParams, previousLayoutManager?: RVLayoutManager) {
super(params, previousLayoutManager);
this.boundedSize = params.windowSize.width;
Expand Down Expand Up @@ -76,6 +88,11 @@ export class RVMasonryLayoutManagerImpl extends RVLayoutManager {
this.fullRelayoutRequired = false;
return 0;
}

// Rebuild sorted indices since dimensions changed
if (!this.optimizeItemArrangement) {
this.updateSortedLayoutIndices();
}
}

/**
Expand Down Expand Up @@ -155,6 +172,11 @@ export class RVMasonryLayoutManagerImpl extends RVLayoutManager {
this.placeItemSequentially(layout, span);
}
}

// Rebuild sorted indices since positions changed
if (!this.optimizeItemArrangement) {
this.updateSortedLayoutIndices();
}
}

/**
Expand Down Expand Up @@ -320,4 +342,72 @@ export class RVMasonryLayoutManagerImpl extends RVLayoutManager {
}
}
}

/**
* Builds the sorted layout indices array.
* Sorts indices by position to enable correct binary search for masonry layouts
* where item index order doesn't match position order. Called after layout changes.
*/
private updateSortedLayoutIndices(): void {
this.sortedLayoutIndices = Array.from(
{ length: this.layouts.length },
(_, i) => i
);

// Sort indices by Y position
const dimension = this.horizontal ? "x" : "y";
this.sortedLayoutIndices.sort((a, b) => {
return this.layouts[a][dimension] - this.layouts[b][dimension];
});

// Build sorted layouts array matching the sorted indices order
this.sortedLayouts = this.sortedLayoutIndices.map(
(i) => this.layouts[i]
);
}

/**
* Overrides getVisibleLayouts to use sorted layouts for correct binary search.
* Only applies when optimizeItemArrangement is false.
*/
getVisibleLayouts(
unboundDimensionStart: number,
unboundDimensionEnd: number
): ConsecutiveNumbers {
// When optimization is enabled, column heights stay balanced,
// so the base class binary search is sufficient.
if (this.optimizeItemArrangement) {
return super.getVisibleLayouts(unboundDimensionStart, unboundDimensionEnd);
}

if (this.layouts.length === 0) {
return ConsecutiveNumbers.EMPTY;
}

const firstSortedIndex = findFirstVisibleIndex(
this.sortedLayouts,
unboundDimensionStart,
this.horizontal
);
const lastSortedIndex = findLastVisibleIndex(
this.sortedLayouts,
unboundDimensionEnd,
this.horizontal
);

if (firstSortedIndex === -1 || lastSortedIndex === -1) {
return ConsecutiveNumbers.EMPTY;
}

// Map sorted range back to original indices and find min/max
let minIndex = this.sortedLayoutIndices[firstSortedIndex];
let maxIndex = minIndex;
for (let i = firstSortedIndex + 1; i <= lastSortedIndex; i++) {
const idx = this.sortedLayoutIndices[i];
if (idx < minIndex) minIndex = idx;
if (idx > maxIndex) maxIndex = idx;
}

return new ConsecutiveNumbers(minIndex, maxIndex);
}
}