diff --git a/example/app/(tabs)/index.tsx b/example/app/(tabs)/index.tsx
index 99ef7c82..2fd1d66b 100644
--- a/example/app/(tabs)/index.tsx
+++ b/example/app/(tabs)/index.tsx
@@ -107,6 +107,10 @@ const data: ListElement[] = [
title: "Cards FlatList",
url: "/cards-flatlist",
},
+ {
+ title: "Reanimated scroll state on UI",
+ url: "/reanimated-scroll-state",
+ },
].map(
(v, i) =>
({
diff --git a/example/app/reanimated-scroll-state/index.tsx b/example/app/reanimated-scroll-state/index.tsx
new file mode 100644
index 00000000..e3fdfc21
--- /dev/null
+++ b/example/app/reanimated-scroll-state/index.tsx
@@ -0,0 +1,141 @@
+import { AnimatedLegendList } from "@legendapp/list/reanimated";
+import { type TCountryCode, countries, getEmojiFlag } from "countries-list";
+import { useCallback, useState } from "react";
+import { Pressable, SafeAreaView, StatusBar, StyleSheet, Text, View } from "react-native";
+import { Gesture, GestureDetector } from "react-native-gesture-handler";
+import { useAnimatedScrollHandler, useSharedValue } from "react-native-reanimated";
+import type {} from "react-native-safe-area-context";
+
+type Country = {
+ id: string;
+ name: string;
+ flag: string;
+};
+
+const DATA: Country[] = Object.entries(countries)
+ .map(([code, country]) => ({
+ id: code,
+ name: country.name,
+ flag: getEmojiFlag(code as TCountryCode),
+ }))
+ .sort((a, b) => a.name.localeCompare(b.name));
+
+type ItemProps = {
+ item: Country;
+ onPress: () => void;
+ isSelected: boolean;
+};
+
+const Item = ({ item, onPress, isSelected }: ItemProps) => (
+ [styles.item, isSelected && styles.selectedItem, pressed && styles.pressedItem]}
+ >
+
+ {item.flag}
+
+
+
+ {item.name}
+ ({item.id})
+
+
+
+);
+
+const App = () => {
+ const [selectedId, setSelectedId] = useState();
+
+ const scrollY = useSharedValue(0);
+
+ const onScroll = useAnimatedScrollHandler({
+ onScroll: (event) => {
+ console.log("onScroll", event);
+
+ scrollY.value = event.contentOffset.y;
+ },
+ });
+
+ const tapGesture = Gesture.Tap().onStart(({ absoluteY }) => {
+ "worklet";
+ const adjustedY = absoluteY + scrollY.value;
+ console.log("📍 Tap at scroll-adjusted Y:", adjustedY);
+ });
+
+ const keyExtractor = useCallback((item: Country) => item.id, []);
+
+ const renderItem = useCallback(
+ ({ item }: { item: Country }) => {
+ const isSelected = item.id === selectedId;
+ return - setSelectedId(item.id)} isSelected={isSelected} />;
+ },
+ [selectedId],
+ );
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default App;
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ marginTop: StatusBar.currentHeight || 0,
+ backgroundColor: "#f5f5f5",
+ },
+ item: {
+ paddingHorizontal: 16,
+ paddingVertical: 6,
+ flexDirection: "row",
+ alignItems: "center",
+ backgroundColor: "#fff",
+ borderRadius: 12,
+ },
+ selectedItem: {},
+ pressedItem: {},
+ flagContainer: {
+ marginRight: 16,
+ width: 40,
+ height: 40,
+ borderRadius: 20,
+ backgroundColor: "#f8f9fa",
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ flag: {
+ fontSize: 28,
+ },
+ contentContainer: {
+ flex: 1,
+ justifyContent: "center",
+ },
+ title: {
+ fontSize: 16,
+ color: "#333",
+ fontWeight: "500",
+ },
+ selectedText: {
+ color: "#1976d2",
+ fontWeight: "600",
+ },
+ countryCode: {
+ fontSize: 14,
+ color: "#666",
+ fontWeight: "400",
+ },
+});
diff --git a/src/LegendList.tsx b/src/LegendList.tsx
index cfb3fdb8..0c039b31 100644
--- a/src/LegendList.tsx
+++ b/src/LegendList.tsx
@@ -16,7 +16,7 @@ import { DebugView } from "./DebugView";
import { ListComponent } from "./ListComponent";
import { ScrollAdjustHandler } from "./ScrollAdjustHandler";
import { ANCHORED_POSITION_OUT_OF_VIEW, ENABLE_DEBUG_VIEW, IsNewArchitecture, POSITION_OUT_OF_VIEW } from "./constants";
-import { comparatorByDistance, comparatorDefault, roundSize, warnDevOnce } from "./helpers";
+import { comparatorByDistance, comparatorDefault, isReanimatedScroll, roundSize, warnDevOnce } from "./helpers";
import { StateProvider, getContentSize, peek$, set$, useStateContext } from "./state";
import type {
AnchoredPosition,
@@ -1727,8 +1727,14 @@ const LegendListInner = typedForwardRef(function LegendListInner(
checkAtBottom();
checkAtTop();
- if (!fromSelf) {
- state.onScroll?.(event as NativeSyntheticEvent);
+ if (!fromSelf && state.onScroll) {
+ const scrollHandler = state.onScroll;
+
+ if (isReanimatedScroll(scrollHandler)) {
+ scrollHandler?.value?.(event as NativeSyntheticEvent);
+ } else if (typeof scrollHandler === "function") {
+ scrollHandler(event as NativeSyntheticEvent);
+ }
}
},
[],
diff --git a/src/helpers.ts b/src/helpers.ts
index 8ce4e8a3..99e8e51f 100644
--- a/src/helpers.ts
+++ b/src/helpers.ts
@@ -1,3 +1,8 @@
+import type { NativeScrollEvent, NativeSyntheticEvent } from "react-native";
+import type { SharedValue } from "react-native-reanimated";
+
+type ScrollHandler = (e: NativeSyntheticEvent) => void;
+
// biome-ignore lint/complexity/noBannedTypes:
export function isFunction(obj: unknown): obj is Function {
return typeof obj === "function";
@@ -30,3 +35,12 @@ export function comparatorDefault(a: number, b: number) {
export function byIndex(a: { index: number }) {
return a.index;
}
+
+export function isReanimatedScroll(value: unknown): value is SharedValue {
+ if (typeof value === "object" && value !== null && "value" in value) {
+ const val = (value as { value: unknown }).value;
+ return typeof val === "function";
+ }
+
+ return false;
+}
diff --git a/src/types.ts b/src/types.ts
index 5af5a77b..137f2d4c 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -8,6 +8,7 @@ import type {
} from "react-native";
import type { ScrollView, StyleProp, ViewStyle } from "react-native";
import type Animated from "react-native-reanimated";
+import type { SharedValue } from "react-native-reanimated";
import type { ScrollAdjustHandler } from "./ScrollAdjustHandler";
export type LegendListPropsBase<
@@ -317,7 +318,9 @@ export interface InternalState {
avg: number;
}
>;
- onScroll: ((event: NativeSyntheticEvent) => void) | undefined;
+ onScroll?:
+ | ((event: NativeSyntheticEvent) => void)
+ | SharedValue<((event: NativeSyntheticEvent) => void) | undefined>;
}
export interface ViewableRange {