From a1a84818fc61d446909eccdec0509100337cb291 Mon Sep 17 00:00:00 2001 From: Michael Bilenko Date: Sun, 5 Jan 2025 15:17:51 +0100 Subject: [PATCH 01/29] Try to add logic --- src/Container.tsx | 27 ++++++++++----------------- src/LegendList.tsx | 16 +++++++++++++--- src/state.tsx | 38 ++++++++++++++++++++++++++++++-------- src/types.ts | 2 +- src/useValue$.ts | 18 ++++-------------- 5 files changed, 58 insertions(+), 43 deletions(-) diff --git a/src/Container.tsx b/src/Container.tsx index 833d50e..b0f345d 100644 --- a/src/Container.tsx +++ b/src/Container.tsx @@ -1,6 +1,7 @@ import React, { useMemo } from "react"; -import { type DimensionValue, type LayoutChangeEvent, type StyleProp, View, type ViewStyle } from "react-native"; +import { Animated, type DimensionValue, type LayoutChangeEvent, type StyleProp, type ViewStyle } from "react-native"; import { peek$, set$, use$, useStateContext } from "./state"; +import { useValue$ } from "./useValue$"; type MeasureMethod = "offscreen" | "invisible"; const MEASURE_METHOD = "invisible" as MeasureMethod; @@ -23,12 +24,13 @@ export const Container = ({ const ctx = useStateContext(); const position = use$(`containerPosition${id}`); const column = use$(`containerColumn${id}`) || 0; - const visible = use$(`containerDidLayout${id}`); + const visible = useValue$(`containerDidLayout${id}`); const numColumns = use$("numColumns"); + const otherAxisPos: DimensionValue | undefined = numColumns > 1 ? `${((column - 1) / numColumns) * 100}%` : 0; const otherAxisSize: DimensionValue | undefined = numColumns > 1 ? `${(1 / numColumns) * 100}%` : undefined; - let style: StyleProp = horizontal + const style: StyleProp = horizontal ? { flexDirection: "row", position: "absolute", @@ -45,14 +47,7 @@ export const Container = ({ top: position, }; - if (MEASURE_METHOD === "invisible") { - style.opacity = visible ? 1 : 0; - } else if (MEASURE_METHOD === "offscreen") { - const additional = horizontal - ? { top: visible ? otherAxisPos : -10000000 } - : { left: visible ? otherAxisPos : -10000000 }; - style = { ...style, ...additional }; - } + style.opacity = visible; const lastItemKey = use$("lastItemKey"); const itemKey = use$(`containerItemKey${id}`); @@ -63,13 +58,13 @@ export const Container = ({ // is not rendered when style changes, only the style prop. // This is a big perf boost to do less work rendering. return ( - { const key = peek$(ctx, `containerItemKey${id}`); if (key !== undefined) { // Round to nearest quater pixel to avoid accumulating rounding errors - const size = Math.floor(event.nativeEvent.layout[horizontal ? "width" : "height"]*8)/8; + const size = Math.floor(event.nativeEvent.layout[horizontal ? "width" : "height"] * 8) / 8; updateItemSize(id, key, size); @@ -78,9 +73,7 @@ export const Container = ({ const measured = peek$(ctx, `containerDidLayout${id}`); if (!measured) { - requestAnimationFrame(() => { - set$(ctx, `containerDidLayout${id}`, true); - }); + set$(ctx, `containerDidLayout${id}`, 1, true); } } }} @@ -89,6 +82,6 @@ export const Container = ({ {renderedItem} {renderedItem && ItemSeparatorComponent && itemKey !== lastItemKey && ItemSeparatorComponent} - + ); }; diff --git a/src/LegendList.tsx b/src/LegendList.tsx index ab068b0..11a0060 100644 --- a/src/LegendList.tsx +++ b/src/LegendList.tsx @@ -152,7 +152,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef(props: LegendListProps & { ref?: ForwardedRef peek$(ctx, "numContainersPooled")) { @@ -536,8 +537,10 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef POSITION_OUT_OF_VIEW && pos !== prevPos) { set$(ctx, `containerPosition${i}`, pos); @@ -545,6 +548,9 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef= 0 && column !== prevColumn) { set$(ctx, `containerColumn${i}`, column); } + if (elementVisible !== prevVisible ) { + set$(ctx, `containerDidLayout${i}`, elementVisible ? 1: 0.5, true); + } } } } @@ -708,6 +714,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef(props: LegendListProps & { ref?: ForwardedRef(props: LegendListProps & { ref?: ForwardedRef(ctx, "numColumns"); @@ -909,8 +917,10 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef void>>; values: Map; + animatedValues: Map; mapViewabilityCallbacks: Map; mapViewabilityValues: Map; mapViewabilityAmountCallbacks: Map; @@ -42,6 +45,7 @@ export function StateProvider({ children }: { children: React.ReactNode }) { const [value] = React.useState(() => ({ listeners: new Map(), values: new Map(), + animatedValues: new Map(), mapViewabilityCallbacks: new Map(), mapViewabilityValues: new Map(), mapViewabilityAmountCallbacks: new Map(), @@ -81,10 +85,28 @@ export function peek$(ctx: StateContext, signalName: ListenerType): T { return values.get(signalName); } -export function set$(ctx: StateContext, signalName: ListenerType, value: any) { +export const getAnimatedValue = (ctx: StateContext, signalName: ListenerType, initialValue?: any) => { + const { animatedValues } = ctx; + let value = animatedValues.get(signalName); + let isNew = false; + if (!value) { + isNew = true; + value = new Animated.Value(initialValue); + animatedValues.set(signalName, value); + } + return [value, isNew] as const; +} + +export function set$(ctx: StateContext, signalName: ListenerType, value: any, animated?: boolean) { const { listeners, values } = ctx; if (values.get(signalName) !== value) { values.set(signalName, value); + if (animated) { + const [animValue, isNew] = getAnimatedValue(ctx, signalName, value); + if (!isNew) { + animValue.setValue(value); + } + } const setListeners = listeners.get(signalName); if (setListeners) { for (const listener of setListeners) { diff --git a/src/types.ts b/src/types.ts index 809365e..e5b0278 100644 --- a/src/types.ts +++ b/src/types.ts @@ -52,7 +52,7 @@ export interface InternalState { positions: Map; columns: Map; sizes: Map; - sizesLaidOut: Map | undefined; + sizesLaidOut: Map; pendingAdjust: number; animFrameLayout: any; isStartReached: boolean; diff --git a/src/useValue$.ts b/src/useValue$.ts index 7b911d7..80d46be 100644 --- a/src/useValue$.ts +++ b/src/useValue$.ts @@ -1,20 +1,10 @@ -import { useMemo, useRef } from 'react'; -import { Animated, useAnimatedValue as _useAnimatedValue } from 'react-native'; -import { listen$, peek$, useStateContext } from './state'; -import type { ListenerType } from './state'; +import { getAnimatedValue, peek$, useStateContext } from "./state"; +import type { ListenerType } from "./state"; -const useAnimatedValue = - _useAnimatedValue || - ((initialValue: number): Animated.Value => { - return useRef(new Animated.Value(initialValue)).current; - }); export function useValue$(key: ListenerType, getValue?: (value: number) => number, key2?: ListenerType) { const ctx = useStateContext(); - const animValue = useAnimatedValue((getValue ? getValue(peek$(ctx, key)) : peek$(ctx, key)) ?? 0); - useMemo(() => { - listen$(ctx, key, (v) => animValue.setValue(getValue ? getValue(v) : v)); - }, []); + const v = peek$(ctx, key) + return getAnimatedValue(ctx, key, (getValue ? getValue(v) : v) ?? 0)[0]; - return animValue; } From c8ea2588b0fbc9deb1aa43ecd0f7e87b10e52e6f Mon Sep 17 00:00:00 2001 From: Michael Bilenko Date: Sun, 5 Jan 2025 15:30:42 +0100 Subject: [PATCH 02/29] hide on unlaid out elements --- src/Container.tsx | 4 ++++ src/LegendList.tsx | 7 ++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Container.tsx b/src/Container.tsx index b0f345d..c89fe0c 100644 --- a/src/Container.tsx +++ b/src/Container.tsx @@ -49,6 +49,7 @@ export const Container = ({ style.opacity = visible; + const lastItemKey = use$("lastItemKey"); const itemKey = use$(`containerItemKey${id}`); @@ -66,6 +67,7 @@ export const Container = ({ // Round to nearest quater pixel to avoid accumulating rounding errors const size = Math.floor(event.nativeEvent.layout[horizontal ? "width" : "height"] * 8) / 8; + console.log("size", key, size); updateItemSize(id, key, size); const otherAxisSize = horizontal ? event.nativeEvent.layout.width : event.nativeEvent.layout.height; @@ -73,7 +75,9 @@ export const Container = ({ const measured = peek$(ctx, `containerDidLayout${id}`); if (!measured) { + setTimeout(() => { set$(ctx, `containerDidLayout${id}`, 1, true); + },0); } } }} diff --git a/src/LegendList.tsx b/src/LegendList.tsx index 11a0060..4ac0645 100644 --- a/src/LegendList.tsx +++ b/src/LegendList.tsx @@ -549,6 +549,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef(props: LegendListProps & { ref?: ForwardedRef 0.5) { let diff: number; @@ -917,8 +920,6 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef(props: LegendListProps & { ref?: ForwardedRef Date: Sun, 5 Jan 2025 15:39:00 +0100 Subject: [PATCH 03/29] cleanup --- src/Container.tsx | 7 ------- src/state.tsx | 16 ++++++++-------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/Container.tsx b/src/Container.tsx index c89fe0c..a0afb70 100644 --- a/src/Container.tsx +++ b/src/Container.tsx @@ -3,9 +3,6 @@ import { Animated, type DimensionValue, type LayoutChangeEvent, type StyleProp, import { peek$, set$, use$, useStateContext } from "./state"; import { useValue$ } from "./useValue$"; -type MeasureMethod = "offscreen" | "invisible"; -const MEASURE_METHOD = "invisible" as MeasureMethod; - export const Container = ({ id, recycleItems, @@ -27,7 +24,6 @@ export const Container = ({ const visible = useValue$(`containerDidLayout${id}`); const numColumns = use$("numColumns"); - const otherAxisPos: DimensionValue | undefined = numColumns > 1 ? `${((column - 1) / numColumns) * 100}%` : 0; const otherAxisSize: DimensionValue | undefined = numColumns > 1 ? `${(1 / numColumns) * 100}%` : undefined; const style: StyleProp = horizontal @@ -49,7 +45,6 @@ export const Container = ({ style.opacity = visible; - const lastItemKey = use$("lastItemKey"); const itemKey = use$(`containerItemKey${id}`); @@ -75,9 +70,7 @@ export const Container = ({ const measured = peek$(ctx, `containerDidLayout${id}`); if (!measured) { - setTimeout(() => { set$(ctx, `containerDidLayout${id}`, 1, true); - },0); } } }} diff --git a/src/state.tsx b/src/state.tsx index b974fd0..8e0128b 100644 --- a/src/state.tsx +++ b/src/state.tsx @@ -3,13 +3,13 @@ import { useSyncExternalStore } from "react"; import { Animated } from "react-native"; import type { ViewAmountToken, ViewToken, ViewabilityAmountCallback, ViewabilityCallback } from "./types"; -export type ContainerAnimatedData = { - position: number; - bottomAnchorPositio?: number; - numColumn: number; - didLayout: boolean; - -} +// This is an implementation of a simple state management system, inspired by Legend State. +// It stores values and listeners in Maps, with peek$ and set$ functions to get and set values. +// The set$ function also triggers the listeners. +// +// This is definitely not general purpose and has one big optimization/caveat: use$ is only ever called +// once for each unique name. So we don't need to manage a Set of listeners or dispose them, +// which saves needing useEffect hooks or managing listeners in a Set. export type ListenerType = | "numContainers" @@ -95,7 +95,7 @@ export const getAnimatedValue = (ctx: StateContext, signalName: ListenerType, in animatedValues.set(signalName, value); } return [value, isNew] as const; -} +}; export function set$(ctx: StateContext, signalName: ListenerType, value: any, animated?: boolean) { const { listeners, values } = ctx; From f4523e0e19dd74bc9fca4e6dbb2fe96e79d41bd9 Mon Sep 17 00:00:00 2001 From: Michael Bilenko Date: Sun, 5 Jan 2025 16:11:04 +0100 Subject: [PATCH 04/29] remove top element blinking issue --- src/Container.tsx | 5 +++-- src/LegendList.tsx | 16 +++++++++++----- src/ScrollAdjustHandler.ts | 2 +- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/Container.tsx b/src/Container.tsx index a0afb70..e8ce45d 100644 --- a/src/Container.tsx +++ b/src/Container.tsx @@ -62,7 +62,6 @@ export const Container = ({ // Round to nearest quater pixel to avoid accumulating rounding errors const size = Math.floor(event.nativeEvent.layout[horizontal ? "width" : "height"] * 8) / 8; - console.log("size", key, size); updateItemSize(id, key, size); const otherAxisSize = horizontal ? event.nativeEvent.layout.width : event.nativeEvent.layout.height; @@ -70,7 +69,9 @@ export const Container = ({ const measured = peek$(ctx, `containerDidLayout${id}`); if (!measured) { - set$(ctx, `containerDidLayout${id}`, 1, true); + requestAnimationFrame(() => { + set$(ctx, `containerDidLayout${id}`, 1, true); + }); } } }} diff --git a/src/LegendList.tsx b/src/LegendList.tsx index 4ac0645..0c0070f 100644 --- a/src/LegendList.tsx +++ b/src/LegendList.tsx @@ -176,7 +176,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef { @@ -229,7 +229,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef(props: LegendListProps & { ref?: ForwardedRef(props: LegendListProps & { ref?: ForwardedRef(props: LegendListProps & { ref?: ForwardedRef(ctx, "stylePaddingTop") || 0; const paddingTop = Math.max(0, Math.floor(scrollLength - totalSize - listPaddingTop)); - set$(ctx, "paddingTop", paddingTop); + set$(ctx, "paddingTop", paddingTop, true); } }; diff --git a/src/ScrollAdjustHandler.ts b/src/ScrollAdjustHandler.ts index 203662a..7679401 100644 --- a/src/ScrollAdjustHandler.ts +++ b/src/ScrollAdjustHandler.ts @@ -21,7 +21,7 @@ export class ScrollAdjustHandler { this.pendingAdjust = adjust; const doAjdust = () => { - set$(this.context, "scrollAdjust", this.pendingAdjust); + set$(this.context, "scrollAdjust", this.pendingAdjust, true); onAdjusted(oldAdjustTop - this.pendingAdjust); this.busy = false; }; From 15443ec1e33ff35f178cbb924cd334633595a0aa Mon Sep 17 00:00:00 2001 From: Michael Bilenko Date: Sun, 5 Jan 2025 17:13:20 +0100 Subject: [PATCH 05/29] refactor - create setAnimated --- src/Container.tsx | 12 ++++-------- src/LegendList.tsx | 22 ++++++++++++---------- src/ScrollAdjustHandler.ts | 4 ++-- src/state.tsx | 20 +++++++++++++++----- 4 files changed, 33 insertions(+), 25 deletions(-) diff --git a/src/Container.tsx b/src/Container.tsx index e8ce45d..c68c635 100644 --- a/src/Container.tsx +++ b/src/Container.tsx @@ -64,15 +64,11 @@ export const Container = ({ updateItemSize(id, key, size); - const otherAxisSize = horizontal ? event.nativeEvent.layout.width : event.nativeEvent.layout.height; - set$(ctx, "otherAxisSize", Math.max(otherAxisSize, peek$(ctx, "otherAxisSize") || 0)); + // const otherAxisSize = horizontal ? event.nativeEvent.layout.width : event.nativeEvent.layout.height; + // set$(ctx, "otherAxisSize", Math.max(otherAxisSize, peek$(ctx, "otherAxisSize") || 0)); - const measured = peek$(ctx, `containerDidLayout${id}`); - if (!measured) { - requestAnimationFrame(() => { - set$(ctx, `containerDidLayout${id}`, 1, true); - }); - } + + } }} > diff --git a/src/LegendList.tsx b/src/LegendList.tsx index 0c0070f..34ecf4d 100644 --- a/src/LegendList.tsx +++ b/src/LegendList.tsx @@ -21,7 +21,7 @@ import { } from "react-native"; import { ListComponent } from "./ListComponent"; import { ScrollAdjustHandler } from "./ScrollAdjustHandler"; -import { type ListenerType, StateProvider, listen$, peek$, set$, useStateContext } from "./state"; +import { type ListenerType, StateProvider, listen$, peek$, set$, setAnimated$, useStateContext } from "./state"; import type { LegendListRecyclingState, LegendListRef, ViewabilityAmountCallback, ViewabilityCallback } from "./types"; import type { InternalState, LegendListProps } from "./types"; import { useInit } from "./useInit"; @@ -176,7 +176,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef { @@ -229,7 +229,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef(props: LegendListProps & { ref?: ForwardedRef(props: LegendListProps & { ref?: ForwardedRef(props: LegendListProps & { ref?: ForwardedRef(ctx, "stylePaddingTop") || 0; const paddingTop = Math.max(0, Math.floor(scrollLength - totalSize - listPaddingTop)); - set$(ctx, "paddingTop", paddingTop, true); + setAnimated$(ctx, "paddingTop", paddingTop); } }; @@ -721,7 +723,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef(props: LegendListProps & { ref?: ForwardedRef { - set$(this.context, "scrollAdjust", this.pendingAdjust, true); + setAnimated$(this.context, "scrollAdjust", this.pendingAdjust); onAdjusted(oldAdjustTop - this.pendingAdjust); this.busy = false; }; diff --git a/src/state.tsx b/src/state.tsx index 8e0128b..5d0cccc 100644 --- a/src/state.tsx +++ b/src/state.tsx @@ -97,16 +97,26 @@ export const getAnimatedValue = (ctx: StateContext, signalName: ListenerType, in return [value, isNew] as const; }; -export function set$(ctx: StateContext, signalName: ListenerType, value: any, animated?: boolean) { +export function set$(ctx: StateContext, signalName: ListenerType, value: any) { const { listeners, values } = ctx; if (values.get(signalName) !== value) { values.set(signalName, value); - if (animated) { - const [animValue, isNew] = getAnimatedValue(ctx, signalName, value); - if (!isNew) { - animValue.setValue(value); + const setListeners = listeners.get(signalName); + if (setListeners) { + for (const listener of setListeners) { + listener(value); } } + } +} +export function setAnimated$(ctx: StateContext, signalName: ListenerType, value: any) { + const { listeners, values } = ctx; + if (values.get(signalName) !== value) { + values.set(signalName, value); + const [animValue, isNew] = getAnimatedValue(ctx, signalName, value); + if (!isNew) { + animValue.setValue(value); + } const setListeners = listeners.get(signalName); if (setListeners) { for (const listener of setListeners) { From f1852f9030690a2c1d87bfaf5276c8fe775fd949 Mon Sep 17 00:00:00 2001 From: Michael Bilenko Date: Sun, 5 Jan 2025 17:17:14 +0100 Subject: [PATCH 06/29] fix var naming --- src/LegendList.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/LegendList.tsx b/src/LegendList.tsx index 34ecf4d..b5902c0 100644 --- a/src/LegendList.tsx +++ b/src/LegendList.tsx @@ -537,14 +537,12 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef(props: LegendListProps & { ref?: ForwardedRef= 0 && column !== prevColumn) { set$(ctx, `containerColumn${i}`, column); } - if (elementVisible !== prevVisible ) { + if (elementVisible !== prevVisible) { // console.log("Setting elementVisible", elementVisible, id, state.sizesLaidOut); - setAnimated$(ctx, `containerDidLayout${i}`, elementVisible ? 1: 0); + setAnimated$(ctx, `containerDidLayout${i}`, elementVisible ? 1 : 0); } } } @@ -929,7 +927,6 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef Date: Thu, 16 Jan 2025 15:49:42 +0100 Subject: [PATCH 07/29] remove animated, because it has to many issues for now --- src/Container.tsx | 31 ++++++++++++++--------- src/LegendList.tsx | 14 +++++------ src/ScrollAdjustHandler.ts | 4 +-- src/state.tsx | 50 +++++++++----------------------------- src/useValue$.ts | 18 +++++++++++--- 5 files changed, 54 insertions(+), 63 deletions(-) diff --git a/src/Container.tsx b/src/Container.tsx index c68c635..5ec807c 100644 --- a/src/Container.tsx +++ b/src/Container.tsx @@ -1,7 +1,9 @@ import React, { useMemo } from "react"; -import { Animated, type DimensionValue, type LayoutChangeEvent, type StyleProp, type ViewStyle } from "react-native"; -import { peek$, set$, use$, useStateContext } from "./state"; -import { useValue$ } from "./useValue$"; +import { type DimensionValue, type LayoutChangeEvent, type StyleProp, View, type ViewStyle } from "react-native"; +import { peek$, use$, useStateContext } from "./state"; + +type MeasureMethod = "offscreen" | "invisible"; +const MEASURE_METHOD = "invisible" as MeasureMethod; export const Container = ({ id, @@ -21,12 +23,12 @@ export const Container = ({ const ctx = useStateContext(); const position = use$(`containerPosition${id}`); const column = use$(`containerColumn${id}`) || 0; - const visible = useValue$(`containerDidLayout${id}`); + const visible = use$(`containerDidLayout${id}`); const numColumns = use$("numColumns"); const otherAxisPos: DimensionValue | undefined = numColumns > 1 ? `${((column - 1) / numColumns) * 100}%` : 0; const otherAxisSize: DimensionValue | undefined = numColumns > 1 ? `${(1 / numColumns) * 100}%` : undefined; - const style: StyleProp = horizontal + let style: StyleProp = horizontal ? { flexDirection: "row", position: "absolute", @@ -43,18 +45,26 @@ export const Container = ({ top: position, }; - style.opacity = visible; + if (MEASURE_METHOD === "invisible") { + style.opacity = visible ? 1 : 0; + } else if (MEASURE_METHOD === "offscreen") { + const additional = horizontal + ? { top: visible ? otherAxisPos : -10000000 } + : { left: visible ? otherAxisPos : -10000000 }; + style = { ...style, ...additional }; + } const lastItemKey = use$("lastItemKey"); const itemKey = use$(`containerItemKey${id}`); + const data = use$(`containerItemData${id}`); // to detect data changes - const renderedItem = useMemo(() => itemKey !== undefined && getRenderedItem(itemKey, id), [itemKey]); + const renderedItem = useMemo(() => itemKey !== undefined && getRenderedItem(itemKey, id), [itemKey, data]); // Use a reactive View to ensure the container element itself // is not rendered when style changes, only the style prop. // This is a big perf boost to do less work rendering. return ( - { const key = peek$(ctx, `containerItemKey${id}`); @@ -66,9 +76,6 @@ export const Container = ({ // const otherAxisSize = horizontal ? event.nativeEvent.layout.width : event.nativeEvent.layout.height; // set$(ctx, "otherAxisSize", Math.max(otherAxisSize, peek$(ctx, "otherAxisSize") || 0)); - - - } }} > @@ -76,6 +83,6 @@ export const Container = ({ {renderedItem} {renderedItem && ItemSeparatorComponent && itemKey !== lastItemKey && ItemSeparatorComponent} - + ); }; diff --git a/src/LegendList.tsx b/src/LegendList.tsx index b5902c0..a3a03ac 100644 --- a/src/LegendList.tsx +++ b/src/LegendList.tsx @@ -21,7 +21,7 @@ import { } from "react-native"; import { ListComponent } from "./ListComponent"; import { ScrollAdjustHandler } from "./ScrollAdjustHandler"; -import { type ListenerType, StateProvider, listen$, peek$, set$, setAnimated$, useStateContext } from "./state"; +import { type ListenerType, StateProvider, listen$, peek$, set$, useStateContext } from "./state"; import type { LegendListRecyclingState, LegendListRef, ViewabilityAmountCallback, ViewabilityCallback } from "./types"; import type { InternalState, LegendListProps } from "./types"; import { useInit } from "./useInit"; @@ -176,7 +176,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef { @@ -229,7 +229,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef(props: LegendListProps & { ref?: ForwardedRef(props: LegendListProps & { ref?: ForwardedRef(ctx, "stylePaddingTop") || 0; const paddingTop = Math.max(0, Math.floor(scrollLength - totalSize - listPaddingTop)); - setAnimated$(ctx, "paddingTop", paddingTop); + set$(ctx, "paddingTop", paddingTop); } }; @@ -721,7 +721,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef(props: LegendListProps & { ref?: ForwardedRef { - setAnimated$(this.context, "scrollAdjust", this.pendingAdjust); + set$(this.context, "scrollAdjust", this.pendingAdjust); onAdjusted(oldAdjustTop - this.pendingAdjust); this.busy = false; }; diff --git a/src/state.tsx b/src/state.tsx index 5d0cccc..16c8b43 100644 --- a/src/state.tsx +++ b/src/state.tsx @@ -1,6 +1,5 @@ import * as React from "react"; import { useSyncExternalStore } from "react"; -import { Animated } from "react-native"; import type { ViewAmountToken, ViewToken, ViewabilityAmountCallback, ViewabilityCallback } from "./types"; // This is an implementation of a simple state management system, inspired by Legend State. @@ -15,6 +14,7 @@ export type ListenerType = | "numContainers" | "numContainersPooled" | `containerItemKey${number}` + | `containerItemData${number}` | `containerPosition${number}` | `containerColumn${number}` | `containerDidLayout${number}` @@ -25,14 +25,12 @@ export type ListenerType = | "stylePaddingTop" | "scrollAdjust" | "headerSize" - | "footerSize" - | "anchorPosition" - | "otherAxisSize"; + | "footerSize"; +// | "otherAxisSize"; export interface StateContext { listeners: Map void>>; values: Map; - animatedValues: Map; mapViewabilityCallbacks: Map; mapViewabilityValues: Map; mapViewabilityAmountCallbacks: Map; @@ -45,7 +43,6 @@ export function StateProvider({ children }: { children: React.ReactNode }) { const [value] = React.useState(() => ({ listeners: new Map(), values: new Map(), - animatedValues: new Map(), mapViewabilityCallbacks: new Map(), mapViewabilityValues: new Map(), mapViewabilityAmountCallbacks: new Map(), @@ -58,12 +55,17 @@ export function useStateContext() { return React.useContext(ContextState)!; } +function createSelectorFunctions(ctx: StateContext, signalName: ListenerType) { + return { + subscribe: (cb: (value: any) => void) => listen$(ctx, signalName, cb), + get: () => peek$(ctx, signalName) as T, + }; +} + export function use$(signalName: ListenerType): T { const ctx = React.useContext(ContextState)!; - const value = useSyncExternalStore( - (onStoreChange) => listen$(ctx, signalName, onStoreChange), - () => ctx.values.get(signalName), - ); + const { subscribe, get } = React.useMemo(() => createSelectorFunctions(ctx, signalName), []); + const value = useSyncExternalStore(subscribe, get); return value; } @@ -85,18 +87,6 @@ export function peek$(ctx: StateContext, signalName: ListenerType): T { return values.get(signalName); } -export const getAnimatedValue = (ctx: StateContext, signalName: ListenerType, initialValue?: any) => { - const { animatedValues } = ctx; - let value = animatedValues.get(signalName); - let isNew = false; - if (!value) { - isNew = true; - value = new Animated.Value(initialValue); - animatedValues.set(signalName, value); - } - return [value, isNew] as const; -}; - export function set$(ctx: StateContext, signalName: ListenerType, value: any) { const { listeners, values } = ctx; if (values.get(signalName) !== value) { @@ -109,19 +99,3 @@ export function set$(ctx: StateContext, signalName: ListenerType, value: any) { } } } -export function setAnimated$(ctx: StateContext, signalName: ListenerType, value: any) { - const { listeners, values } = ctx; - if (values.get(signalName) !== value) { - values.set(signalName, value); - const [animValue, isNew] = getAnimatedValue(ctx, signalName, value); - if (!isNew) { - animValue.setValue(value); - } - const setListeners = listeners.get(signalName); - if (setListeners) { - for (const listener of setListeners) { - listener(value); - } - } - } -} diff --git a/src/useValue$.ts b/src/useValue$.ts index 80d46be..7b911d7 100644 --- a/src/useValue$.ts +++ b/src/useValue$.ts @@ -1,10 +1,20 @@ -import { getAnimatedValue, peek$, useStateContext } from "./state"; -import type { ListenerType } from "./state"; +import { useMemo, useRef } from 'react'; +import { Animated, useAnimatedValue as _useAnimatedValue } from 'react-native'; +import { listen$, peek$, useStateContext } from './state'; +import type { ListenerType } from './state'; +const useAnimatedValue = + _useAnimatedValue || + ((initialValue: number): Animated.Value => { + return useRef(new Animated.Value(initialValue)).current; + }); export function useValue$(key: ListenerType, getValue?: (value: number) => number, key2?: ListenerType) { const ctx = useStateContext(); - const v = peek$(ctx, key) - return getAnimatedValue(ctx, key, (getValue ? getValue(v) : v) ?? 0)[0]; + const animValue = useAnimatedValue((getValue ? getValue(peek$(ctx, key)) : peek$(ctx, key)) ?? 0); + useMemo(() => { + listen$(ctx, key, (v) => animValue.setValue(getValue ? getValue(v) : v)); + }, []); + return animValue; } From a4dac40d97f7cce9cd4451e68f0d1797bf72cab4 Mon Sep 17 00:00:00 2001 From: Michael Bilenko <129734190+mbilenko-florio@users.noreply.github.com> Date: Thu, 16 Jan 2025 18:43:23 +0100 Subject: [PATCH 08/29] intermediate --- src/Container.tsx | 3 ++- src/Containers.tsx | 4 +++- src/LegendList.tsx | 42 +++++++++++++++++++++++------------------- src/ListComponent.tsx | 7 +++++-- src/state.tsx | 5 +++-- src/types.ts | 2 +- 6 files changed, 37 insertions(+), 26 deletions(-) diff --git a/src/Container.tsx b/src/Container.tsx index 5ec807c..d186280 100644 --- a/src/Container.tsx +++ b/src/Container.tsx @@ -23,7 +23,7 @@ export const Container = ({ const ctx = useStateContext(); const position = use$(`containerPosition${id}`); const column = use$(`containerColumn${id}`) || 0; - const visible = use$(`containerDidLayout${id}`); + const visible = use$(`containerVisible${id}`); const numColumns = use$("numColumns"); const otherAxisPos: DimensionValue | undefined = numColumns > 1 ? `${((column - 1) / numColumns) * 100}%` : 0; @@ -60,6 +60,7 @@ export const Container = ({ const renderedItem = useMemo(() => itemKey !== undefined && getRenderedItem(itemKey, id), [itemKey, data]); + console.log("renderItem", itemKey, visible); // Use a reactive View to ensure the container element itself // is not rendered when style changes, only the style prop. // This is a big perf boost to do less work rendering. diff --git a/src/Containers.tsx b/src/Containers.tsx index 67a533e..5e9b793 100644 --- a/src/Containers.tsx +++ b/src/Containers.tsx @@ -39,7 +39,9 @@ export const Containers = React.memo(function Containers({ ); } - const style: StyleProp = horizontal ? { width: animSize } : { height: animSize }; + const style: StyleProp = horizontal + ? { width: animSize } + : { height: animSize}; return {containers}; }); diff --git a/src/LegendList.tsx b/src/LegendList.tsx index 241980f..bfd5cf4 100644 --- a/src/LegendList.tsx +++ b/src/LegendList.tsx @@ -160,8 +160,8 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef getId(i))); if (maintainVisibleContentPosition) { @@ -181,6 +181,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef { @@ -524,7 +525,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef peek$(ctx, "numContainersPooled")) { @@ -576,9 +577,10 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef POSITION_OUT_OF_VIEW && pos !== prevPos) { @@ -589,9 +591,16 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef { + set$(ctx, `containerVisible${i}`, elementVisible); + },0); + } else { + set$(ctx, `containerVisible${i}`, elementVisible); + } + } - + if (prevData !== item) { set$(ctx, `containerItemData${i}`, data[itemIndex]); } @@ -600,11 +609,9 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef 0) { - for (const containerId of layoutsPending) { - set$(ctx, `containerDidLayout${containerId}`, true); - } - layoutsPending.clear(); + if (layoutsPending) { + set$(ctx, 'didFirstMeasure', 1); + state.layoutsPending = false; } if (refState.current!.viewabilityConfigCallbackPairs) { @@ -939,7 +946,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef(props: LegendListProps & { ref?: ForwardedRef(ctx, 'didFirstMeasure'); + if (firstRenderPending) { + state.layoutsPending = true } if (!prevSize || Math.abs(prevSize - size) > 0.5) { @@ -1023,7 +1030,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef { state.animFrameLayout = null; calculateItemsInView(state.scrollVelocity); @@ -1032,10 +1039,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef { diff --git a/src/ListComponent.tsx b/src/ListComponent.tsx index 5748b58..69c04c5 100644 --- a/src/ListComponent.tsx +++ b/src/ListComponent.tsx @@ -10,7 +10,7 @@ import { } from "react-native"; import { Containers } from "./Containers"; -import { peek$, set$, useStateContext } from "./state"; +import { peek$, set$, use$, useStateContext } from "./state"; import type { LegendListProps } from "./types"; import { useValue$ } from "./useValue$"; @@ -69,6 +69,8 @@ export const ListComponent = React.memo(function ListComponent({ const ctx = useStateContext(); const animPaddingTop = useValue$("paddingTop"); const animScrollAdjust = useValue$("scrollAdjust"); + const didFirstRender = use$("didFirstMeasure"); + // TODO: Try this again? This had bad behaviorof sometimes setting the min size to greater than // the screen size @@ -87,10 +89,11 @@ export const ListComponent = React.memo(function ListComponent({ const additionalSize = { marginTop: animScrollAdjust, paddingTop: animPaddingTop }; + return ( ; scrollTimer: Timer | undefined; startReachedBlockedByTimer: boolean; - layoutsPending: Set; + layoutsPending: boolean; scrollForNextCalculateItemsInView: { top: number; bottom: number } | undefined; } From be459e2695bfc999767ac72abe8d9345b37f34c4 Mon Sep 17 00:00:00 2001 From: Michael Bilenko <129734190+mbilenko-florio@users.noreply.github.com> Date: Fri, 17 Jan 2025 15:55:18 +0100 Subject: [PATCH 09/29] Adapt shouldDisplayContent --- src/Container.tsx | 10 +++++----- src/LegendList.tsx | 4 ++-- src/ListComponent.tsx | 6 ++++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Container.tsx b/src/Container.tsx index fd90854..d6e6a1b 100644 --- a/src/Container.tsx +++ b/src/Container.tsx @@ -43,10 +43,10 @@ export const Container = ({ top: position, }; - if (waitForInitialLayout) { - const visible = use$(`containerDidLayout${id}`); - style.opacity = visible ? 1 : 0; - } +// // if (waitForInitialLayout) { +// const visible = use$(`containerDidLayout${id}`); +// style.opacity = visible ? 1 : 0; +// //} const lastItemKey = use$("lastItemKey"); const itemKey = use$(`containerItemKey${id}`); @@ -54,7 +54,7 @@ export const Container = ({ const renderedItem = useMemo(() => itemKey !== undefined && getRenderedItem(itemKey, id), [itemKey, data]); - console.log("renderItem", itemKey, visible); + console.log("renderItem", itemKey); // Use a reactive View to ensure the container element itself // is not rendered when style changes, only the style prop. // This is a big perf boost to do less work rendering. diff --git a/src/LegendList.tsx b/src/LegendList.tsx index aa2a6a4..3655f8a 100644 --- a/src/LegendList.tsx +++ b/src/LegendList.tsx @@ -181,7 +181,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef { @@ -606,7 +606,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef("didFirstMeasure"); + const didFirstRender = use$("didFirstMeasure"); // TODO: Try this again? This had bad behaviorof sometimes setting the min size to greater than @@ -89,11 +89,13 @@ export const ListComponent = React.memo(function ListComponent({ const additionalSize = { marginTop: animScrollAdjust, paddingTop: animPaddingTop }; + const shouldDisplayContent = didFirstRender || !waitForInitialLayout; + return ( Date: Sat, 18 Jan 2025 09:29:30 +0100 Subject: [PATCH 10/29] Use bottom relative position for elements above the anchor --- src/Container.tsx | 47 +++++++++++++++++++++----------- src/LegendList.tsx | 68 ++++++++++++++++++++++------------------------ src/constants.ts | 10 +++++-- src/types.ts | 6 ++++ 4 files changed, 78 insertions(+), 53 deletions(-) diff --git a/src/Container.tsx b/src/Container.tsx index d6e6a1b..14d3e9f 100644 --- a/src/Container.tsx +++ b/src/Container.tsx @@ -1,12 +1,13 @@ import React, { useMemo } from "react"; import { type DimensionValue, type LayoutChangeEvent, type StyleProp, View, type ViewStyle } from "react-native"; +import { ANCHORED_POSITION_OUT_OF_VIEW } from "./constants"; import { peek$, use$, useStateContext } from "./state"; +import type { AnchoredPosition } from "./types"; export const Container = ({ id, recycleItems, horizontal, - waitForInitialLayout, getRenderedItem, updateItemSize, ItemSeparatorComponent, @@ -20,7 +21,7 @@ export const Container = ({ ItemSeparatorComponent?: React.ReactNode; }) => { const ctx = useStateContext(); - const position = use$(`containerPosition${id}`); + const position = use$(`containerPosition${id}`) || ANCHORED_POSITION_OUT_OF_VIEW; const column = use$(`containerColumn${id}`) || 0; const numColumns = use$("numColumns"); @@ -33,31 +34,48 @@ export const Container = ({ top: otherAxisPos, bottom: numColumns > 1 ? null : 0, height: otherAxisSize, - left: position, + left: position.coordinate, } : { position: "absolute", left: otherAxisPos, right: numColumns > 1 ? null : 0, width: otherAxisSize, - top: position, + top: position.coordinate, }; -// // if (waitForInitialLayout) { -// const visible = use$(`containerDidLayout${id}`); -// style.opacity = visible ? 1 : 0; -// //} - const lastItemKey = use$("lastItemKey"); const itemKey = use$(`containerItemKey${id}`); const data = use$(`containerItemData${id}`); // to detect data changes const renderedItem = useMemo(() => itemKey !== undefined && getRenderedItem(itemKey, id), [itemKey, data]); - console.log("renderItem", itemKey); - // Use a reactive View to ensure the container element itself - // is not rendered when style changes, only the style prop. - // This is a big perf boost to do less work rendering. + if (position.type === "bottom") { + // Use a reactive View to ensure the container element itself + // is not rendered when style changes, only the style prop. + // This is a big perf boost to do less work rendering. + return ( + + { + const key = peek$(ctx, `containerItemKey${id}`); + if (key !== undefined) { + // Round to nearest quater pixel to avoid accumulating rounding errors + const size = Math.floor(event.nativeEvent.layout[horizontal ? "width" : "height"] * 8) / 8; + + updateItemSize(id, key, size); + } + }} + > + + {renderedItem} + {renderedItem && ItemSeparatorComponent && itemKey !== lastItemKey && ItemSeparatorComponent} + + + + ); + } return ( diff --git a/src/LegendList.tsx b/src/LegendList.tsx index 3655f8a..951e2f9 100644 --- a/src/LegendList.tsx +++ b/src/LegendList.tsx @@ -22,14 +22,20 @@ import { } from "react-native"; import { ListComponent } from "./ListComponent"; import { ScrollAdjustHandler } from "./ScrollAdjustHandler"; +import { ANCHORED_POSITION_OUT_OF_VIEW, POSITION_OUT_OF_VIEW } from "./constants"; import { type ListenerType, StateProvider, listen$, peek$, set$, useStateContext } from "./state"; -import type { LegendListRecyclingState, LegendListRef, ViewabilityAmountCallback, ViewabilityCallback } from "./types"; +import type { + AnchoredPosition, + LegendListRecyclingState, + LegendListRef, + ViewabilityAmountCallback, + ViewabilityCallback, +} from "./types"; import type { InternalState, LegendListProps } from "./types"; import { useInit } from "./useInit"; import { setupViewability, updateViewableItems } from "./viewability"; const DEFAULT_DRAW_DISTANCE = 250; -const POSITION_OUT_OF_VIEW = -10000000; const DEFAULT_ITEM_SIZE = 100; export const LegendList: (props: LegendListProps & { ref?: ForwardedRef }) => ReactElement = @@ -497,7 +503,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef(ctx, `containerPosition${u}`); + const pos = peek$(ctx, `containerPosition${u}`).top; if (index < startBuffered || index > endBuffered) { const distance = Math.abs(pos - top); @@ -520,8 +526,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef peek$(ctx, "numContainersPooled")) { @@ -553,7 +558,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef endBuffered) { // This is fairly complex because we want to avoid setting container position if it's not even in view // because it will trigger a render - const prevPos = peek$(ctx, `containerPosition${i}`); + const prevPos = peek$(ctx, `containerPosition${i}`).top; const pos = positions.get(id) || 0; const size = getItemSize(id, itemIndex, data[i]); @@ -561,41 +566,35 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef= scroll && pos <= scrollBottom) || (prevPos + size >= scroll && prevPos <= scrollBottom) ) { - set$(ctx, `containerPosition${i}`, POSITION_OUT_OF_VIEW); + set$(ctx, `containerPosition${i}`, ANCHORED_POSITION_OUT_OF_VIEW); } } else { - const pos = positions.get(id) || 0; + const pos: AnchoredPosition = { + type: "top", + coordinate: positions.get(id) || 0, + top: positions.get(id) || 0, + }; const column = columns.get(id) || 1; - const elementIsMeasured = state.sizesLaidOut.get(id) !== undefined; - let elementVisible = true; if (maintainVisibleContentPosition && itemIndex < anchorElementIndex) { - // if element is above anchor, display it only if it's measured - elementVisible = elementIsMeasured; + const nextElementId = getId(itemIndex + 1); + const nextPosition = positions.get(nextElementId); + if (nextPosition == null) { + throw new Error("Next position is null, that should not happen"); + } + pos.coordinate = nextPosition; + pos.type = "bottom"; } - console.log("ElementVisible",id, elementVisible,elementIsMeasured) - const prevPos = peek$(ctx, `containerPosition${i}`); + const prevPos = peek$(ctx, `containerPosition${i}`); const prevColumn = peek$(ctx, `containerColumn${i}`); - const prevVisible = peek$(ctx, `containerVisible${i}`); const prevData = peek$(ctx, `containerItemData${i}`); - if (pos > POSITION_OUT_OF_VIEW && pos !== prevPos) { + if (pos.coordinate > POSITION_OUT_OF_VIEW && pos.top !== prevPos.top) { set$(ctx, `containerPosition${i}`, pos); } if (column >= 0 && column !== prevColumn) { set$(ctx, `containerColumn${i}`, column); } - if (elementVisible !== prevVisible) { - // console.log("Setting elementVisible", elementVisible, id, state.sizesLaidOut); - if (prevVisible === false) { - setTimeout(() => { - set$(ctx, `containerVisible${i}`, elementVisible); - },0); - } else { - set$(ctx, `containerVisible${i}`, elementVisible); - } - - } if (prevData !== item) { set$(ctx, `containerItemData${i}`, data[itemIndex]); @@ -606,8 +605,8 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef(props: LegendListProps & { ref?: ForwardedRef(ctx, `containerItemKey${i}`); if (!keyExtractorProp || (itemKey && state.indexByKey.get(itemKey) === undefined)) { set$(ctx, `containerItemKey${i}`, undefined); - set$(ctx, `containerPosition${i}`, POSITION_OUT_OF_VIEW); + set$(ctx, `containerPosition${i}`, ANCHORED_POSITION_OUT_OF_VIEW); set$(ctx, `containerColumn${i}`, -1); } } @@ -946,8 +945,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef(props: LegendListProps & { ref?: ForwardedRef(ctx, 'didFirstMeasure'); + const firstRenderPending = !peek$(ctx, "didFirstMeasure"); if (firstRenderPending) { - state.layoutsPending = true + state.layoutsPending = true; } if (!prevSize || Math.abs(prevSize - size) > 0.5) { @@ -1031,7 +1029,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef { state.animFrameLayout = null; calculateItemsInView(state.scrollVelocity); diff --git a/src/constants.ts b/src/constants.ts index 4093468..6d2daf2 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,3 +1,9 @@ -import { Platform } from 'react-native'; +import type { AnchoredPosition } from './types'; -export const USE_CONTENT_INSET = Platform.OS === 'ios'; + +export const POSITION_OUT_OF_VIEW = -10000000; +export const ANCHORED_POSITION_OUT_OF_VIEW: AnchoredPosition = { + type: "top", + coordinate: POSITION_OUT_OF_VIEW, + top: POSITION_OUT_OF_VIEW, +}; \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 6f6be26..9e098d1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -52,6 +52,12 @@ export type LegendListPropsBase< }) => void; }; +export type AnchoredPosition = { + type: 'top' | 'bottom'; + coordinate: number; // used fro display + top: number; // used for calculating the position of the container +} + export type LegendListProps = LegendListPropsBase>; export interface InternalState { From 8c75fc8e22deb10fb5483056465504481beaf2a9 Mon Sep 17 00:00:00 2001 From: Michael Bilenko <129734190+mbilenko-florio@users.noreply.github.com> Date: Sat, 18 Jan 2025 09:38:09 +0100 Subject: [PATCH 11/29] undo visiblity change --- src/Container.tsx | 8 +++++++- src/Containers.tsx | 4 +--- src/LegendList.tsx | 23 +++++++++++++---------- src/ListComponent.tsx | 9 ++------- src/state.tsx | 1 + src/types.ts | 4 ++-- 6 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/Container.tsx b/src/Container.tsx index 14d3e9f..9e18913 100644 --- a/src/Container.tsx +++ b/src/Container.tsx @@ -10,6 +10,7 @@ export const Container = ({ horizontal, getRenderedItem, updateItemSize, + waitForInitialLayout, ItemSeparatorComponent, }: { id: number; @@ -21,7 +22,7 @@ export const Container = ({ ItemSeparatorComponent?: React.ReactNode; }) => { const ctx = useStateContext(); - const position = use$(`containerPosition${id}`) || ANCHORED_POSITION_OUT_OF_VIEW; + const position = use$(`containerPosition${id}`) || ANCHORED_POSITION_OUT_OF_VIEW; const column = use$(`containerColumn${id}`) || 0; const numColumns = use$("numColumns"); @@ -44,6 +45,11 @@ export const Container = ({ top: position.coordinate, }; + if (waitForInitialLayout) { + const visible = use$(`containerDidLayout${id}`); + style.opacity = visible ? 1 : 0; + } + const lastItemKey = use$("lastItemKey"); const itemKey = use$(`containerItemKey${id}`); const data = use$(`containerItemData${id}`); // to detect data changes diff --git a/src/Containers.tsx b/src/Containers.tsx index 7e2f35c..e76d314 100644 --- a/src/Containers.tsx +++ b/src/Containers.tsx @@ -42,9 +42,7 @@ export const Containers = React.memo(function Containers({ ); } - const style: StyleProp = horizontal - ? { width: animSize } - : { height: animSize}; + const style: StyleProp = horizontal ? { width: animSize } : { height: animSize }; return {containers}; }); diff --git a/src/LegendList.tsx b/src/LegendList.tsx index 951e2f9..5a89382 100644 --- a/src/LegendList.tsx +++ b/src/LegendList.tsx @@ -160,14 +160,13 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef getId(i))); if (maintainVisibleContentPosition) { @@ -604,9 +603,11 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef 0) { + for (const containerId of layoutsPending) { + set$(ctx, `containerDidLayout${containerId}`, true); + } + layoutsPending.clear(); } if (refState.current!.viewabilityConfigCallbackPairs) { @@ -968,10 +969,9 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef(ctx, "didFirstMeasure"); - if (firstRenderPending) { - state.layoutsPending = true; + const measured = peek$(ctx, `containerDidLayout${containerId}`); + if (!measured) { + state.layoutsPending.add(containerId); } if (!prevSize || Math.abs(prevSize - size) > 0.5) { @@ -1029,7 +1029,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef { state.animFrameLayout = null; calculateItemsInView(state.scrollVelocity); @@ -1042,6 +1042,9 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef("didFirstMeasure"); - // TODO: Try this again? This had bad behaviorof sometimes setting the min size to greater than // the screen size @@ -89,13 +87,10 @@ export const ListComponent = React.memo(function ListComponent({ const additionalSize = { marginTop: animScrollAdjust, paddingTop: animPaddingTop }; - const shouldDisplayContent = didFirstRender || !waitForInitialLayout; - - return ( ; columns: Map; sizes: Map; - sizesLaidOut: Map; + sizesLaidOut?: Map; pendingAdjust: number; animFrameLayout: any; isStartReached: boolean; @@ -103,7 +103,7 @@ export interface InternalState { scrollHistory: Array<{ scroll: number; time: number }>; scrollTimer: Timer | undefined; startReachedBlockedByTimer: boolean; - layoutsPending: boolean; + layoutsPending: Set; scrollForNextCalculateItemsInView: { top: number; bottom: number } | undefined; } From 20d9257fc473165c1b9ae20cdd88bbbbc70a5b80 Mon Sep 17 00:00:00 2001 From: Michael Bilenko <129734190+mbilenko-florio@users.noreply.github.com> Date: Sat, 18 Jan 2025 09:40:33 +0100 Subject: [PATCH 12/29] Revert sizes --- src/Container.tsx | 2 +- src/LegendList.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Container.tsx b/src/Container.tsx index 9e18913..586db29 100644 --- a/src/Container.tsx +++ b/src/Container.tsx @@ -8,9 +8,9 @@ export const Container = ({ id, recycleItems, horizontal, + waitForInitialLayout, getRenderedItem, updateItemSize, - waitForInitialLayout, ItemSeparatorComponent, }: { id: number; diff --git a/src/LegendList.tsx b/src/LegendList.tsx index 5a89382..e1b42b3 100644 --- a/src/LegendList.tsx +++ b/src/LegendList.tsx @@ -186,7 +186,6 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef { @@ -997,6 +996,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef(props: LegendListProps & { ref?: ForwardedRef Date: Sat, 18 Jan 2025 09:41:11 +0100 Subject: [PATCH 13/29] Fix initialization --- src/LegendList.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/LegendList.tsx b/src/LegendList.tsx index e1b42b3..9421879 100644 --- a/src/LegendList.tsx +++ b/src/LegendList.tsx @@ -166,6 +166,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef getId(i))); From dc7352a905f019b8c73d8c37aca0a432cc615c3f Mon Sep 17 00:00:00 2001 From: Michael Bilenko <129734190+mbilenko-florio@users.noreply.github.com> Date: Sat, 18 Jan 2025 09:46:53 +0100 Subject: [PATCH 14/29] add waitForInitialLayout --- src/Container.tsx | 1 + src/LegendList.tsx | 1 + src/ListComponent.tsx | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Container.tsx b/src/Container.tsx index 586db29..7ad639f 100644 --- a/src/Container.tsx +++ b/src/Container.tsx @@ -45,6 +45,7 @@ export const Container = ({ top: position.coordinate, }; + console.log(waitForInitialLayout) if (waitForInitialLayout) { const visible = use$(`containerDidLayout${id}`); style.opacity = visible ? 1 : 0; diff --git a/src/LegendList.tsx b/src/LegendList.tsx index 9421879..18e6879 100644 --- a/src/LegendList.tsx +++ b/src/LegendList.tsx @@ -1197,6 +1197,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef ); diff --git a/src/ListComponent.tsx b/src/ListComponent.tsx index 64ef0d5..9e418a7 100644 --- a/src/ListComponent.tsx +++ b/src/ListComponent.tsx @@ -90,7 +90,7 @@ export const ListComponent = React.memo(function ListComponent({ return ( Date: Sat, 18 Jan 2025 09:51:18 +0100 Subject: [PATCH 15/29] Rename anchoring prop --- src/Container.tsx | 13 +++++++------ src/LegendList.tsx | 6 +++--- src/constants.ts | 2 +- src/types.ts | 2 +- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/Container.tsx b/src/Container.tsx index 7ad639f..3e77691 100644 --- a/src/Container.tsx +++ b/src/Container.tsx @@ -35,17 +35,17 @@ export const Container = ({ top: otherAxisPos, bottom: numColumns > 1 ? null : 0, height: otherAxisSize, - left: position.coordinate, + left: position.relativeCoordinate, } : { position: "absolute", left: otherAxisPos, right: numColumns > 1 ? null : 0, width: otherAxisSize, - top: position.coordinate, + top: position.relativeCoordinate, }; - console.log(waitForInitialLayout) + console.log(waitForInitialLayout); if (waitForInitialLayout) { const visible = use$(`containerDidLayout${id}`); style.opacity = visible ? 1 : 0; @@ -57,10 +57,11 @@ export const Container = ({ const renderedItem = useMemo(() => itemKey !== undefined && getRenderedItem(itemKey, id), [itemKey, data]); + // Use a reactive View to ensure the container element itself + // is not rendered when style changes, only the style prop. + // This is a big perf boost to do less work rendering. + if (position.type === "bottom") { - // Use a reactive View to ensure the container element itself - // is not rendered when style changes, only the style prop. - // This is a big perf boost to do less work rendering. return ( (props: LegendListProps & { ref?: ForwardedRef(props: LegendListProps & { ref?: ForwardedRef(ctx, `containerPosition${i}`); const prevColumn = peek$(ctx, `containerColumn${i}`); const prevData = peek$(ctx, `containerItemData${i}`); - if (pos.coordinate > POSITION_OUT_OF_VIEW && pos.top !== prevPos.top) { + if (pos.relativeCoordinate > POSITION_OUT_OF_VIEW && pos.top !== prevPos.top) { set$(ctx, `containerPosition${i}`, pos); } if (column >= 0 && column !== prevColumn) { diff --git a/src/constants.ts b/src/constants.ts index 6d2daf2..2d81546 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -4,6 +4,6 @@ import type { AnchoredPosition } from './types'; export const POSITION_OUT_OF_VIEW = -10000000; export const ANCHORED_POSITION_OUT_OF_VIEW: AnchoredPosition = { type: "top", - coordinate: POSITION_OUT_OF_VIEW, + relativeCoordinate: POSITION_OUT_OF_VIEW, top: POSITION_OUT_OF_VIEW, }; \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 2a97e19..d925f8f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -54,7 +54,7 @@ export type LegendListPropsBase< export type AnchoredPosition = { type: 'top' | 'bottom'; - coordinate: number; // used fro display + relativeCoordinate: number; // used for display top: number; // used for calculating the position of the container } From 25428fed66a0288cb84af85984e32ccab20cc930 Mon Sep 17 00:00:00 2001 From: Michael Bilenko <129734190+mbilenko-florio@users.noreply.github.com> Date: Sat, 18 Jan 2025 09:54:41 +0100 Subject: [PATCH 16/29] realign sizes --- src/Container.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Container.tsx b/src/Container.tsx index 3e77691..a00d34e 100644 --- a/src/Container.tsx +++ b/src/Container.tsx @@ -65,7 +65,7 @@ export const Container = ({ return ( { const key = peek$(ctx, `containerItemKey${id}`); if (key !== undefined) { From 5d46b08cc7f27506f2b2717f1e7cbfb9a8f71bc2 Mon Sep 17 00:00:00 2001 From: Michael Bilenko <129734190+mbilenko-florio@users.noreply.github.com> Date: Sat, 18 Jan 2025 10:22:32 +0100 Subject: [PATCH 17/29] fix --- example/app/bidirectional-infinite-list/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/example/app/bidirectional-infinite-list/index.tsx b/example/app/bidirectional-infinite-list/index.tsx index 6f7231c..06716f7 100644 --- a/example/app/bidirectional-infinite-list/index.tsx +++ b/example/app/bidirectional-infinite-list/index.tsx @@ -100,6 +100,7 @@ export default function BidirectionalInfiniteList() { }, 500); } }} + numColumns={2} /> ); From 8c650b7f1370ff93b68884d4301b9690b77c015f Mon Sep 17 00:00:00 2001 From: Michael Bilenko <129734190+mbilenko-florio@users.noreply.github.com> Date: Sat, 18 Jan 2025 10:23:13 +0100 Subject: [PATCH 18/29] remove console.log --- src/Container.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Container.tsx b/src/Container.tsx index a00d34e..3494ea4 100644 --- a/src/Container.tsx +++ b/src/Container.tsx @@ -28,6 +28,7 @@ export const Container = ({ const otherAxisPos: DimensionValue | undefined = numColumns > 1 ? `${((column - 1) / numColumns) * 100}%` : 0; const otherAxisSize: DimensionValue | undefined = numColumns > 1 ? `${(1 / numColumns) * 100}%` : undefined; + const style: StyleProp = horizontal ? { flexDirection: "row", @@ -45,7 +46,6 @@ export const Container = ({ top: position.relativeCoordinate, }; - console.log(waitForInitialLayout); if (waitForInitialLayout) { const visible = use$(`containerDidLayout${id}`); style.opacity = visible ? 1 : 0; From 9f0300d2c2ef1ff90cb60fa7409a54d0503f3502 Mon Sep 17 00:00:00 2001 From: Michael Bilenko <129734190+mbilenko-florio@users.noreply.github.com> Date: Sat, 18 Jan 2025 11:22:19 +0100 Subject: [PATCH 19/29] try to separate styles --- src/Container.tsx | 77 +++++++++++++++++++++-------------------------- 1 file changed, 35 insertions(+), 42 deletions(-) diff --git a/src/Container.tsx b/src/Container.tsx index 3494ea4..ec00e80 100644 --- a/src/Container.tsx +++ b/src/Container.tsx @@ -31,21 +31,24 @@ export const Container = ({ const style: StyleProp = horizontal ? { - flexDirection: "row", position: "absolute", - top: otherAxisPos, - bottom: numColumns > 1 ? null : 0, - height: otherAxisSize, left: position.relativeCoordinate, } : { position: "absolute", - left: otherAxisPos, - right: numColumns > 1 ? null : 0, - width: otherAxisSize, top: position.relativeCoordinate, }; + const otherAxisStyle = horizontal ? { + top: otherAxisPos, + bottom: numColumns > 1 ? null : 0, + height: otherAxisSize, + }: { + left: otherAxisPos, + right: numColumns > 1 ? null : 0, + width: otherAxisSize, + } + if (waitForInitialLayout) { const visible = use$(`containerDidLayout${id}`); style.opacity = visible ? 1 : 0; @@ -57,50 +60,40 @@ export const Container = ({ const renderedItem = useMemo(() => itemKey !== undefined && getRenderedItem(itemKey, id), [itemKey, data]); - // Use a reactive View to ensure the container element itself - // is not rendered when style changes, only the style prop. - // This is a big perf boost to do less work rendering. + const onLayout = (event: LayoutChangeEvent) => { + const key = peek$(ctx, `containerItemKey${id}`); + if (key !== undefined) { + // Round to nearest quater pixel to avoid accumulating rounding errors + const size = Math.floor(event.nativeEvent.layout[horizontal ? "width" : "height"] * 8) / 8; + + updateItemSize(id, key, size); + } + }; + + + + const contentFragment = ( + + {renderedItem} + {renderedItem && ItemSeparatorComponent && itemKey !== lastItemKey && ItemSeparatorComponent} + + ); if (position.type === "bottom") { return ( - { - const key = peek$(ctx, `containerItemKey${id}`); - if (key !== undefined) { - // Round to nearest quater pixel to avoid accumulating rounding errors - const size = Math.floor(event.nativeEvent.layout[horizontal ? "width" : "height"] * 8) / 8; - - updateItemSize(id, key, size); - } - }} - > - - {renderedItem} - {renderedItem && ItemSeparatorComponent && itemKey !== lastItemKey && ItemSeparatorComponent} - + + {contentFragment} ); } + // Use a reactive View to ensure the container element itself + // is not rendered when style changes, only the style prop. + // This is a big perf boost to do less work rendering. return ( - { - const key = peek$(ctx, `containerItemKey${id}`); - if (key !== undefined) { - // Round to nearest quater pixel to avoid accumulating rounding errors - const size = Math.floor(event.nativeEvent.layout[horizontal ? "width" : "height"] * 8) / 8; - - updateItemSize(id, key, size); - } - }} - > - - {renderedItem} - {renderedItem && ItemSeparatorComponent && itemKey !== lastItemKey && ItemSeparatorComponent} - + + {contentFragment} ); }; From 6492de38d24ea39866f62254909501b2918bc374 Mon Sep 17 00:00:00 2001 From: Michael Bilenko <129734190+mbilenko-florio@users.noreply.github.com> Date: Sat, 18 Jan 2025 12:26:05 +0100 Subject: [PATCH 20/29] fix container --- src/Container.tsx | 27 +++++++++++++-------------- src/LegendList.tsx | 8 ++------ 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/src/Container.tsx b/src/Container.tsx index ec00e80..ba9eb4d 100644 --- a/src/Container.tsx +++ b/src/Container.tsx @@ -1,5 +1,6 @@ import React, { useMemo } from "react"; -import { type DimensionValue, type LayoutChangeEvent, type StyleProp, View, type ViewStyle } from "react-native"; +import { type DimensionValue, type LayoutChangeEvent, type StyleProp, type ViewStyle } from "react-native"; +import { LeanView } from "./LeanView"; import { ANCHORED_POSITION_OUT_OF_VIEW } from "./constants"; import { peek$, use$, useStateContext } from "./state"; import type { AnchoredPosition } from "./types"; @@ -70,8 +71,6 @@ export const Container = ({ } }; - - const contentFragment = ( {renderedItem} @@ -79,21 +78,21 @@ export const Container = ({ ); - if (position.type === "bottom") { - return ( - - - {contentFragment} - - - ); - } + // if (position.type === "bottom") { + // return ( + // + // + // {contentFragment} + // + // + // ); + // } // Use a reactive View to ensure the container element itself // is not rendered when style changes, only the style prop. // This is a big perf boost to do less work rendering. return ( - + {contentFragment} - + ); }; diff --git a/src/LegendList.tsx b/src/LegendList.tsx index 56abe4f..1293d70 100644 --- a/src/LegendList.tsx +++ b/src/LegendList.tsx @@ -576,12 +576,8 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef(ctx, `containerPosition${i}`); From 213ebb113e756f241bdd551d7d2f468d84db8270 Mon Sep 17 00:00:00 2001 From: Michael Bilenko <129734190+mbilenko-florio@users.noreply.github.com> Date: Sat, 18 Jan 2025 12:29:03 +0100 Subject: [PATCH 21/29] fix columns --- example/app/bidirectional-infinite-list/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/example/app/bidirectional-infinite-list/index.tsx b/example/app/bidirectional-infinite-list/index.tsx index 06716f7..6f7231c 100644 --- a/example/app/bidirectional-infinite-list/index.tsx +++ b/example/app/bidirectional-infinite-list/index.tsx @@ -100,7 +100,6 @@ export default function BidirectionalInfiniteList() { }, 500); } }} - numColumns={2} /> ); From ff757bd9c2ad302f3f7cfd2379605ec451240a12 Mon Sep 17 00:00:00 2001 From: Michael Bilenko <129734190+mbilenko-florio@users.noreply.github.com> Date: Sat, 18 Jan 2025 12:35:27 +0100 Subject: [PATCH 22/29] fix containers issue --- src/Container.tsx | 42 +++++++++++++++++++----------------------- src/LegendList.tsx | 2 +- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/Container.tsx b/src/Container.tsx index ba9eb4d..3d66bb8 100644 --- a/src/Container.tsx +++ b/src/Container.tsx @@ -1,6 +1,5 @@ import React, { useMemo } from "react"; -import { type DimensionValue, type LayoutChangeEvent, type StyleProp, type ViewStyle } from "react-native"; -import { LeanView } from "./LeanView"; +import { type DimensionValue, type LayoutChangeEvent, type StyleProp, View, type ViewStyle } from "react-native"; import { ANCHORED_POSITION_OUT_OF_VIEW } from "./constants"; import { peek$, use$, useStateContext } from "./state"; import type { AnchoredPosition } from "./types"; @@ -33,22 +32,20 @@ export const Container = ({ const style: StyleProp = horizontal ? { position: "absolute", + top: otherAxisPos, + bottom: numColumns > 1 ? null : 0, + height: otherAxisSize, left: position.relativeCoordinate, } : { position: "absolute", + left: otherAxisPos, + right: numColumns > 1 ? null : 0, + width: otherAxisSize, top: position.relativeCoordinate, }; - const otherAxisStyle = horizontal ? { - top: otherAxisPos, - bottom: numColumns > 1 ? null : 0, - height: otherAxisSize, - }: { - left: otherAxisPos, - right: numColumns > 1 ? null : 0, - width: otherAxisSize, - } + if (waitForInitialLayout) { const visible = use$(`containerDidLayout${id}`); @@ -66,7 +63,6 @@ export const Container = ({ if (key !== undefined) { // Round to nearest quater pixel to avoid accumulating rounding errors const size = Math.floor(event.nativeEvent.layout[horizontal ? "width" : "height"] * 8) / 8; - updateItemSize(id, key, size); } }; @@ -78,21 +74,21 @@ export const Container = ({ ); - // if (position.type === "bottom") { - // return ( - // - // - // {contentFragment} - // - // - // ); - // } + if (position.type === "bottom") { + return ( + + + {contentFragment} + + + ); + } // Use a reactive View to ensure the container element itself // is not rendered when style changes, only the style prop. // This is a big perf boost to do less work rendering. return ( - + {contentFragment} - + ); }; diff --git a/src/LegendList.tsx b/src/LegendList.tsx index 1293d70..2852f05 100644 --- a/src/LegendList.tsx +++ b/src/LegendList.tsx @@ -577,7 +577,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef(ctx, `containerPosition${i}`); From 680eedbc88238f1c8236103d931c29496a8818eb Mon Sep 17 00:00:00 2001 From: Michael Bilenko <129734190+mbilenko-florio@users.noreply.github.com> Date: Sat, 18 Jan 2025 12:52:56 +0100 Subject: [PATCH 23/29] add comments --- src/Container.tsx | 3 +++ src/LegendList.tsx | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Container.tsx b/src/Container.tsx index 3d66bb8..e3c1c93 100644 --- a/src/Container.tsx +++ b/src/Container.tsx @@ -64,6 +64,9 @@ export const Container = ({ // Round to nearest quater pixel to avoid accumulating rounding errors const size = Math.floor(event.nativeEvent.layout[horizontal ? "width" : "height"] * 8) / 8; updateItemSize(id, key, size); + + // const otherAxisSize = horizontal ? event.nativeEvent.layout.width : event.nativeEvent.layout.height; + // set$(ctx, "otherAxisSize", Math.max(otherAxisSize, peek$(ctx, "otherAxisSize") || 0)); } }; diff --git a/src/LegendList.tsx b/src/LegendList.tsx index 2852f05..78a96e5 100644 --- a/src/LegendList.tsx +++ b/src/LegendList.tsx @@ -575,11 +575,17 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef(ctx, `containerPosition${i}`); const prevColumn = peek$(ctx, `containerColumn${i}`); const prevData = peek$(ctx, `containerItemData${i}`); From bd11855f4b409765639f8f67a2f387f66c3e6b4e Mon Sep 17 00:00:00 2001 From: Michael Bilenko <129734190+mbilenko-florio@users.noreply.github.com> Date: Sat, 18 Jan 2025 21:25:32 +0100 Subject: [PATCH 24/29] use lean view --- example/app/bidirectional-infinite-list/index.tsx | 3 +-- src/Container.tsx | 15 ++++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/example/app/bidirectional-infinite-list/index.tsx b/example/app/bidirectional-infinite-list/index.tsx index 6f7231c..6a6317c 100644 --- a/example/app/bidirectional-infinite-list/index.tsx +++ b/example/app/bidirectional-infinite-list/index.tsx @@ -52,7 +52,7 @@ export default function BidirectionalInfiniteList() { // }, 2000); // }, []); - const { top, bottom } = useSafeAreaInsets(); + const { bottom } = useSafeAreaInsets(); return ( @@ -76,7 +76,6 @@ export default function BidirectionalInfiniteList() { drawDistance={DRAW_DISTANCE} maintainVisibleContentPosition recycleItems={true} - ListHeaderComponent={} ListFooterComponent={} onStartReached={(props) => { const time = performance.now(); diff --git a/src/Container.tsx b/src/Container.tsx index e3c1c93..0374451 100644 --- a/src/Container.tsx +++ b/src/Container.tsx @@ -1,5 +1,6 @@ import React, { useMemo } from "react"; -import { type DimensionValue, type LayoutChangeEvent, type StyleProp, View, type ViewStyle } from "react-native"; +import { type DimensionValue, type LayoutChangeEvent, type StyleProp, type ViewStyle } from "react-native"; +import { LeanView } from "./LeanView"; import { ANCHORED_POSITION_OUT_OF_VIEW } from "./constants"; import { peek$, use$, useStateContext } from "./state"; import type { AnchoredPosition } from "./types"; @@ -79,19 +80,19 @@ export const Container = ({ if (position.type === "bottom") { return ( - - + + {contentFragment} - - + + ); } // Use a reactive View to ensure the container element itself // is not rendered when style changes, only the style prop. // This is a big perf boost to do less work rendering. return ( - + {contentFragment} - + ); }; From e4a3ff6992311db38d8be106a5322a86937770ab Mon Sep 17 00:00:00 2001 From: Michael Bilenko <129734190+mbilenko-florio@users.noreply.github.com> Date: Mon, 20 Jan 2025 19:33:11 +0100 Subject: [PATCH 25/29] Remove comment, add conditional container --- src/Container.tsx | 16 ++++++++++------ src/LegendList.tsx | 2 +- src/ListComponent.tsx | 1 + src/state.tsx | 1 + 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Container.tsx b/src/Container.tsx index 0374451..69ae4fc 100644 --- a/src/Container.tsx +++ b/src/Container.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from "react"; -import { type DimensionValue, type LayoutChangeEvent, type StyleProp, type ViewStyle } from "react-native"; +import type { DimensionValue, LayoutChangeEvent, StyleProp, ViewStyle } from "react-native"; import { LeanView } from "./LeanView"; import { ANCHORED_POSITION_OUT_OF_VIEW } from "./constants"; import { peek$, use$, useStateContext } from "./state"; @@ -23,6 +23,7 @@ export const Container = ({ ItemSeparatorComponent?: React.ReactNode; }) => { const ctx = useStateContext(); + const maintainVisibleContentPosition = use$("maintainVisibleContentPosition"); const position = use$(`containerPosition${id}`) || ANCHORED_POSITION_OUT_OF_VIEW; const column = use$(`containerColumn${id}`) || 0; const numColumns = use$("numColumns"); @@ -46,8 +47,6 @@ export const Container = ({ top: position.relativeCoordinate, }; - - if (waitForInitialLayout) { const visible = use$(`containerDidLayout${id}`); style.opacity = visible ? 1 : 0; @@ -65,7 +64,7 @@ export const Container = ({ // Round to nearest quater pixel to avoid accumulating rounding errors const size = Math.floor(event.nativeEvent.layout[horizontal ? "width" : "height"] * 8) / 8; updateItemSize(id, key, size); - + // const otherAxisSize = horizontal ? event.nativeEvent.layout.width : event.nativeEvent.layout.height; // set$(ctx, "otherAxisSize", Math.max(otherAxisSize, peek$(ctx, "otherAxisSize") || 0)); } @@ -78,10 +77,15 @@ export const Container = ({ ); - if (position.type === "bottom") { + // If maintainVisibleContentPosition is enabled, we need a way items to grow upwards + if (maintainVisibleContentPosition) { + const anchorStyle: StyleProp = + position.type === "top" + ? { position: "absolute", top: 0, left: 0, right: 0 } + : { position: "absolute", bottom: 0, left: 0, right: 0 }; return ( - + {contentFragment} diff --git a/src/LegendList.tsx b/src/LegendList.tsx index 95231aa..48361f6 100644 --- a/src/LegendList.tsx +++ b/src/LegendList.tsx @@ -186,6 +186,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef { @@ -580,7 +581,6 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef void; maintainVisibleContentPosition: boolean; renderScrollComponent?: (props: ScrollViewProps) => React.ReactElement; + } const getComponent = (Component: React.ComponentType | React.ReactElement) => { diff --git a/src/state.tsx b/src/state.tsx index 607d9fb..f22c9b6 100644 --- a/src/state.tsx +++ b/src/state.tsx @@ -28,6 +28,7 @@ export type ListenerType = | "headerSize" | "footerSize" | "didFirstMeasure" + | "maintainVisibleContentPosition" // | "otherAxisSize"; export interface StateContext { From dbfc729eeaae3624efc4ea56c332081e07f450ed Mon Sep 17 00:00:00 2001 From: Michael Bilenko <129734190+mbilenko-florio@users.noreply.github.com> Date: Thu, 23 Jan 2025 09:46:02 +0100 Subject: [PATCH 26/29] remove did first measure --- src/state.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/state.tsx b/src/state.tsx index f22c9b6..902745c 100644 --- a/src/state.tsx +++ b/src/state.tsx @@ -27,7 +27,6 @@ export type ListenerType = | "scrollAdjust" | "headerSize" | "footerSize" - | "didFirstMeasure" | "maintainVisibleContentPosition" // | "otherAxisSize"; From 35adf4e9cbd88c9800b1c9faf10405a78ee1f859 Mon Sep 17 00:00:00 2001 From: Michael Bilenko <129734190+mbilenko-florio@users.noreply.github.com> Date: Thu, 23 Jan 2025 09:46:58 +0100 Subject: [PATCH 27/29] revert change --- src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index 7ce300a..a77b10e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -74,7 +74,7 @@ export interface InternalState { positions: Map; columns: Map; sizes: Map; - sizesLaidOut?: Map; + sizesLaidOut: Map | undefined; pendingAdjust: number; animFrameLayout: any; isStartReached: boolean; From 03aa34255258ab2dfb0b24d0b1d881f7db9f7890 Mon Sep 17 00:00:00 2001 From: Michael Bilenko <129734190+mbilenko-florio@users.noreply.github.com> Date: Thu, 23 Jan 2025 09:48:41 +0100 Subject: [PATCH 28/29] remove containervisible --- src/state.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/state.tsx b/src/state.tsx index 902745c..b88ca84 100644 --- a/src/state.tsx +++ b/src/state.tsx @@ -18,7 +18,6 @@ export type ListenerType = | `containerPosition${number}` | `containerColumn${number}` | `containerDidLayout${number}` - | `containerVisible${number}` | "numColumns" | `lastItemKey` | "totalSize" From cf2fb0ac244f18733ca2a56d53607bbc2c098cdb Mon Sep 17 00:00:00 2001 From: Michael Bilenko <129734190+mbilenko-florio@users.noreply.github.com> Date: Thu, 23 Jan 2025 15:31:05 +0100 Subject: [PATCH 29/29] Make waitForInitialLayout default to true --- src/LegendList.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/LegendList.tsx b/src/LegendList.tsx index 8bc4b0b..11df857 100644 --- a/src/LegendList.tsx +++ b/src/LegendList.tsx @@ -74,6 +74,7 @@ const LegendListInner: (props: LegendListProps & { ref?: ForwardedRef(props: LegendListProps & { ref?: ForwardedRef );