Skip to content
Draft
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
6 changes: 4 additions & 2 deletions example/app/(tabs)/cards.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { type Item, renderItem } from "@/app/cards-renderItem";
import { DO_SCROLL_TEST, DRAW_DISTANCE, ESTIMATED_ITEM_LENGTH } from "@/constants/constants";
import { useScrollTest } from "@/constants/useScrollTest";
import { LegendList, type LegendListRef } from "@legendapp/list";
import { LegendList, type LegendListProps, type LegendListRef } from "@legendapp/list";
import { useRef, useState } from "react";
import { LogBox, Platform, StyleSheet, Text, View } from "react-native";

LogBox.ignoreLogs(["Open debugger"]);

interface CardsProps {
numColumns?: number;
overrideItemLayout?: LegendListProps<any>["overrideItemLayout"];
}

export default function Cards({ numColumns = 1 }: CardsProps) {
export default function Cards({ numColumns = 1, ...props }: CardsProps) {
const listRef = useRef<LegendListRef>(null);

const [data, setData] = useState<Item[]>(
Expand Down Expand Up @@ -59,6 +60,7 @@ export default function Cards({ numColumns = 1 }: CardsProps) {
<Text style={{ color: "white" }}>Empty</Text>
</View>
}
{...props}
// viewabilityConfigCallbackPairs={[
// {
// viewabilityConfig: { id: "viewability", viewAreaCoveragePercentThreshold: 50 },
Expand Down
12 changes: 11 additions & 1 deletion example/app/cards-columns/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,17 @@ import { LogBox, Platform, StyleSheet } from "react-native";
LogBox.ignoreLogs(["Open debugger"]);

export default function CardsColumns() {
return <Cards numColumns={2} />;
return (
<Cards
numColumns={2}
overrideItemLayout={(layout, item, index, numColumns, extraData) => {
if (index === 3) {
layout.span = 2;
// layout.size = 150;
}
}}
/>
);
}

const styles = StyleSheet.create({
Expand Down
4 changes: 2 additions & 2 deletions src/Container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ export const Container = ({
const maintainVisibleContentPosition = use$<boolean>("maintainVisibleContentPosition");
const position = use$<AnchoredPosition>(`containerPosition${id}`) || ANCHORED_POSITION_OUT_OF_VIEW;
const column = use$<number>(`containerColumn${id}`) || 0;
const colSpan = use$<number>(`containerColSpan${id}`) || 1;
const numColumns = use$<number>("numColumns");

const otherAxisPos: DimensionValue | undefined = numColumns > 1 ? `${((column - 1) / numColumns) * 100}%` : 0;
const otherAxisSize: DimensionValue | undefined = numColumns > 1 ? `${(1 / numColumns) * 100}%` : undefined;

const otherAxisSize: DimensionValue | undefined = numColumns > 1 ? `${(colSpan / numColumns) * 100}%` : undefined;
const style: StyleProp<ViewStyle> = horizontal
? {
flexDirection: ItemSeparatorComponent ? "row" : undefined,
Expand Down
82 changes: 63 additions & 19 deletions src/LegendList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const LegendListInner: <T>(props: LegendListProps<T> & { ref?: ForwardedRef<Lege
getEstimatedItemSize,
ListEmptyComponent,
onItemSizeChanged,
overrideItemLayout,
scrollEventThrottle,
refScrollView,
waitForInitialLayout = true,
Expand Down Expand Up @@ -110,16 +111,29 @@ const LegendListInner: <T>(props: LegendListProps<T> & { ref?: ForwardedRef<Lege
};

const getItemSize = (key: string, index: number, data: T) => {
const sizeKnown = refState.current!.sizes.get(key)!;
const state = refState.current!;
const sizeKnown = state.sizes.get(key)!;
if (sizeKnown !== undefined) {
return sizeKnown;
}

const size =
let size =
(getEstimatedItemSize ? getEstimatedItemSize(index, data) : estimatedItemSize) ?? DEFAULT_ITEM_SIZE;

if (overrideItemLayout) {
const layout = { span: 1, size };
overrideItemLayout(layout, data, index, peek$<number>(ctx, "numColumns"), extraData);
if (layout.size !== size) {
size = layout.size;
state.fixedSizes.set(key, size);
}
if (layout.span > 1) {
state.colSpans.set(key, layout.span);
}
}
// TODO: I don't think I like this setting sizes when it's not really known, how to do
// that better and support viewability checking sizes
refState.current!.sizes.set(key, size);
state.sizes.set(key, size);
return size;
};
const calculateInitialOffset = (index = initialScrollIndex) => {
Expand Down Expand Up @@ -183,6 +197,8 @@ const LegendListInner: <T>(props: LegendListProps<T> & { ref?: ForwardedRef<Lege
scrollForNextCalculateItemsInView: undefined,
enableScrollForNextCalculateItemsInView: true,
minIndexSizeChanged: 0,
colSpans: new Map(),
fixedSizes: new Map(),
};
refState.current!.idsInFirstRender = new Set(data.map((_: unknown, i: number) => getId(i)));
if (maintainVisibleContentPosition) {
Expand Down Expand Up @@ -432,18 +448,27 @@ const LegendListInner: <T>(props: LegendListProps<T> & { ref?: ForwardedRef<Lege
return topOffset;
};

let didStartNewRow = false;

// scan data forwards
for (let i = loopStart; i < data!.length; i++) {
const id = getId(i)!;
const size = getItemSize(id, i, data[i]);
const colSpan = state.colSpans.get(id) ?? 1;

let startNewRow = false;

maxSizeInRow = Math.max(maxSizeInRow, size);

if (top === undefined) {
top = getInitialTop(i);
}

if (positions.get(id) !== top) {
if (colSpan > 1 && column + colSpan > numColumns + 1) {
i--;
startNewRow = true;
didStartNewRow = true;
} else if (positions.get(id) !== top) {
positions.set(id, top);
}

Expand All @@ -452,10 +477,10 @@ const LegendListInner: <T>(props: LegendListProps<T> & { ref?: ForwardedRef<Lege
}

if (startNoBuffer === null && top + size > scroll) {
startNoBuffer = i;
startNoBuffer = i - column + 1;
}
if (startBuffered === null && top + size > scroll - scrollBufferTop) {
startBuffered = i;
startBuffered = i - column + 1;
startBufferedId = id;
}
if (startNoBuffer !== null) {
Expand All @@ -469,14 +494,20 @@ const LegendListInner: <T>(props: LegendListProps<T> & { ref?: ForwardedRef<Lege
}
}

column++;
if (column > numColumns) {
column += colSpan;
if (startNewRow || column > numColumns) {
top += maxSizeInRow;
column = 1;
maxSizeInRow = 0;
}

if (!startNewRow) {
didStartNewRow = false;
}
}

const { startBuffered: prevStartBuffered, endBuffered: prevEndBuffered } = state;

Object.assign(state, {
startBuffered,
startBufferedId,
Expand All @@ -487,11 +518,21 @@ const LegendListInner: <T>(props: LegendListProps<T> & { ref?: ForwardedRef<Lege

// Precompute the scroll that will be needed for the range to change
// so it can be skipped if not needed
const nextTop = Math.ceil(startBuffered !== null ? positions.get(startBufferedId!)! + scrollBuffer : 0);
const nextBottom = Math.floor(
endBuffered !== null ? (positions.get(getId(endBuffered! + 1))! || 0) - scrollLength - scrollBuffer : 0,
);
if (state.enableScrollForNextCalculateItemsInView) {
if (startBuffered === prevStartBuffered && endBuffered! < prevEndBuffered) {
// If range is smaller than it used to be, some positions will be wrong. Normally that's fine as they'll get recomputed.
// But it'll break these estimates so we need to clear their positions to they won't break scrollForNextCalculateItemsInView.
for (let i = endBuffered!; i <= prevEndBuffered; i++) {
set$(ctx, `containerPosition${i}`, ANCHORED_POSITION_OUT_OF_VIEW);
}
}

const nextTop = Math.ceil(startBuffered !== null ? positions.get(startBufferedId!)! + scrollBuffer : 0);
const nextBottom = Math.floor(
endBuffered !== null
? positions.get(getId(endBuffered! + 1))! - scrollLength - scrollBuffer || 0
: 0,
);
state.scrollForNextCalculateItemsInView =
nextTop >= 0 && nextBottom >= 0
? {
Expand Down Expand Up @@ -619,6 +660,8 @@ const LegendListInner: <T>(props: LegendListProps<T> & { ref?: ForwardedRef<Lege
}

const prevPos = peek$<AnchoredPosition>(ctx, `containerPosition${i}`);
const colSpan = state.colSpans.get(id) ?? 1;
const prevColSpan = peek$(ctx, `containerColSpan${i}`);
const prevColumn = peek$(ctx, `containerColumn${i}`);
const prevData = peek$(ctx, `containerItemData${i}`);

Expand All @@ -628,7 +671,9 @@ const LegendListInner: <T>(props: LegendListProps<T> & { ref?: ForwardedRef<Lege
if (column >= 0 && column !== prevColumn) {
set$(ctx, `containerColumn${i}`, column);
}

if (colSpan !== prevColSpan) {
set$(ctx, `containerColSpan${i}`, colSpan);
}
if (prevData !== item) {
set$(ctx, `containerItemData${i}`, data[itemIndex]);
}
Expand Down Expand Up @@ -990,21 +1035,20 @@ const LegendListInner: <T>(props: LegendListProps<T> & { ref?: ForwardedRef<Lege
return;
}
const state = refState.current!;
const { sizes, indexByKey, columns, sizesLaidOut } = state;

const { sizes, indexByKey, columns, sizesLaidOut, fixedSizes } = state;
const index = indexByKey.get(itemKey)!;
const numColumns = peek$<number>(ctx, "numColumns");

state.minIndexSizeChanged =
state.minIndexSizeChanged !== undefined ? Math.min(state.minIndexSizeChanged, index) : index;

const row = Math.floor(index / numColumns);
const prevSize = getRowHeight(row);
const prevSize = getItemSize(itemKey, index, data as any);

if (!prevSize || Math.abs(prevSize - size) > 0.5) {
if (!fixedSizes.has(itemKey) && (!prevSize || Math.abs(prevSize - size) > 0.5)) {
let diff: number;

if (numColumns > 1) {
const prevMaxSizeInRow = getRowHeight(row);
sizes.set(itemKey, size);

const column = columns.get(itemKey);
Expand All @@ -1016,7 +1060,7 @@ const LegendListInner: <T>(props: LegendListProps<T> & { ref?: ForwardedRef<Lege
nextMaxSizeInRow = Math.max(nextMaxSizeInRow, size);
}

diff = nextMaxSizeInRow - prevMaxSizeInRow;
diff = nextMaxSizeInRow - prevSize;
} else {
sizes.set(itemKey, size);
diff = size - prevSize;
Expand Down
1 change: 1 addition & 0 deletions src/state.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type ListenerType =
| `containerPosition${number}`
| `containerColumn${number}`
| "containersDidLayout"
| `containerColSpan${number}`
| "extraData"
| "numColumns"
| "lastItemKey"
Expand Down
9 changes: 9 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ export type LegendListPropsBase<
*/
renderScrollComponent?: (props: ScrollViewProps) => React.ReactElement<ScrollViewProps>;
extraData?: any;
overrideItemLayout?: (
layout: { span?: number; size?: number },
item: ItemT,
index: number,
maxColumns: number,
extraData?: any
) => void;
};

export type AnchoredPosition = {
Expand Down Expand Up @@ -110,6 +117,8 @@ export interface InternalState {
scrollForNextCalculateItemsInView: { top: number; bottom: number } | undefined;
enableScrollForNextCalculateItemsInView: boolean;
minIndexSizeChanged: number | undefined;
colSpans: Map<string, number>;
fixedSizes: Map<string, number>;
}

export interface ViewableRange<T> {
Expand Down