diff --git a/docs/docs/03-api-reference/01-scroll-view-with-headers.mdx b/docs/docs/03-api-reference/01-scroll-view-with-headers.mdx index f39f7ca..f11af9d 100644 --- a/docs/docs/03-api-reference/01-scroll-view-with-headers.mdx +++ b/docs/docs/03-api-reference/01-scroll-view-with-headers.mdx @@ -112,3 +112,22 @@ Defaults to `1`. Whether or not the LargeHeaderComponent should fade in and out. Defaults to `false`. **Note**: This is only available in version >= 0.10.0. + +### onScrollWorklet + +A custom worklet that allows custom tracking scroll container's +state (i.e., its scroll contentInset, contentOffset, etc.). Please +ensure that this function is a [worklet](https://docs.swmansion.com/react-native-reanimated/docs/2.x/fundamentals/worklets/). + +Since the library uses the `onScroll` prop to handle animations internally and [reanimated +does not currently allow for multiple onScroll handlers](https://github.com/software-mansion/react-native-reanimated/discussions/1763), +you must use this property to track the state of the scroll container's state. + +An example is shown below: + +```tsx +const scrollHandlerWorklet = (evt: NativeScrollEvent) => { + 'worklet'; + console.log('offset: ', evt.contentOffset); +}; +``` diff --git a/docs/docs/03-api-reference/02-flat-list-with-headers.mdx b/docs/docs/03-api-reference/02-flat-list-with-headers.mdx index 6c31cc5..9bdccaa 100644 --- a/docs/docs/03-api-reference/02-flat-list-with-headers.mdx +++ b/docs/docs/03-api-reference/02-flat-list-with-headers.mdx @@ -112,3 +112,22 @@ Defaults to `1`. Whether or not the LargeHeaderComponent should fade in and out. Defaults to `false`. **Note**: This is only available in version >= 0.10.0. + +### onScrollWorklet + +A custom worklet that allows custom tracking scroll container's +state (i.e., its scroll contentInset, contentOffset, etc.). Please +ensure that this function is a [worklet](https://docs.swmansion.com/react-native-reanimated/docs/2.x/fundamentals/worklets/). + +Since the library uses the `onScroll` prop to handle animations internally and [reanimated +does not currently allow for multiple onScroll handlers](https://github.com/software-mansion/react-native-reanimated/discussions/1763), +you must use this property to track the state of the scroll container's state. + +An example is shown below: + +```tsx +const scrollHandlerWorklet = (evt: NativeScrollEvent) => { + 'worklet'; + console.log('offset: ', evt.contentOffset); +}; +``` diff --git a/docs/docs/03-api-reference/03-section-list-with-headers.mdx b/docs/docs/03-api-reference/03-section-list-with-headers.mdx index eb266a3..90b84d4 100644 --- a/docs/docs/03-api-reference/03-section-list-with-headers.mdx +++ b/docs/docs/03-api-reference/03-section-list-with-headers.mdx @@ -112,3 +112,22 @@ Defaults to `1`. Whether or not the LargeHeaderComponent should fade in and out. Defaults to `false`. **Note**: This is only available in version >= 0.10.0. + +### onScrollWorklet + +A custom worklet that allows custom tracking scroll container's +state (i.e., its scroll contentInset, contentOffset, etc.). Please +ensure that this function is a [worklet](https://docs.swmansion.com/react-native-reanimated/docs/2.x/fundamentals/worklets/). + +Since the library uses the `onScroll` prop to handle animations internally and [reanimated +does not currently allow for multiple onScroll handlers](https://github.com/software-mansion/react-native-reanimated/discussions/1763), +you must use this property to track the state of the scroll container's state. + +An example is shown below: + +```tsx +const scrollHandlerWorklet = (evt: NativeScrollEvent) => { + 'worklet'; + console.log('offset: ', evt.contentOffset); +}; +``` diff --git a/docs/docs/03-api-reference/04-flash-list-with-headers.mdx b/docs/docs/03-api-reference/04-flash-list-with-headers.mdx index 10856bf..4a8125a 100644 --- a/docs/docs/03-api-reference/04-flash-list-with-headers.mdx +++ b/docs/docs/03-api-reference/04-flash-list-with-headers.mdx @@ -117,3 +117,22 @@ Defaults to `1`. Whether or not the LargeHeaderComponent should fade in and out. Defaults to `false`. **Note**: This is only available in version >= 0.10.0. + +### onScrollWorklet + +A custom worklet that allows custom tracking scroll container's +state (i.e., its scroll contentInset, contentOffset, etc.). Please +ensure that this function is a [worklet](https://docs.swmansion.com/react-native-reanimated/docs/2.x/fundamentals/worklets/). + +Since the library uses the `onScroll` prop to handle animations internally and [reanimated +does not currently allow for multiple onScroll handlers](https://github.com/software-mansion/react-native-reanimated/discussions/1763), +you must use this property to track the state of the scroll container's state. + +An example is shown below: + +```tsx +const scrollHandlerWorklet = (evt: NativeScrollEvent) => { + 'worklet'; + console.log('offset: ', evt.contentOffset); +}; +``` diff --git a/example/src/navigation/AppNavigation.tsx b/example/src/navigation/AppNavigation.tsx index 02cdb13..5de322f 100644 --- a/example/src/navigation/AppNavigation.tsx +++ b/example/src/navigation/AppNavigation.tsx @@ -13,6 +13,7 @@ import { AbsoluteHeaderBlurSurfaceUsageScreen, ArbitraryYTransitionHeaderUsageScreen, InvertedUsageScreen, + CustomOnScrollWorkletUsageScreen, } from '../screens'; const Stack = createNativeStackNavigator(); @@ -43,5 +44,9 @@ export default () => ( name="ArbitraryYTransitionHeaderUsageScreen" component={ArbitraryYTransitionHeaderUsageScreen} /> + ); diff --git a/example/src/navigation/types.ts b/example/src/navigation/types.ts index bf9fbe7..de45a51 100644 --- a/example/src/navigation/types.ts +++ b/example/src/navigation/types.ts @@ -13,6 +13,7 @@ export type RootStackParamList = { AbsoluteHeaderBlurSurfaceUsageScreen: undefined; ArbitraryYTransitionHeaderUsageScreen: undefined; InvertedUsageScreen: undefined; + CustomOnScrollWorkletUsageScreen: undefined; }; // Overrides the typing for useNavigation in @react-navigation/native to support the internal @@ -71,3 +72,8 @@ export type InvertedUsageScreenNavigationProps = NativeStackScreenProps< RootStackParamList, 'InvertedUsageScreen' >; + +export type CustomOnScrollWorkletUsageScreenNavigationProps = NativeStackScreenProps< + RootStackParamList, + 'CustomOnScrollWorkletUsageScreen' +>; diff --git a/example/src/screens/Home.tsx b/example/src/screens/Home.tsx index efc4471..56f2136 100644 --- a/example/src/screens/Home.tsx +++ b/example/src/screens/Home.tsx @@ -65,6 +65,12 @@ const SCREEN_LIST_CONFIG: ScreenConfigItem[] = [ description: 'An example of a header that transitions based on the scroll position of the ScrollView, instead of passing the height of the large header before animating.', }, + { + name: 'Custom onScroll Worklet Example', + route: 'CustomOnScrollWorkletUsageScreen', + description: + "A simple example with a custom worklet that tracks the scroll container's offset.", + }, ]; const HeaderComponent: React.FC = ({ showNavBar }) => { diff --git a/example/src/screens/index.ts b/example/src/screens/index.ts index 0accd9a..7a6f865 100644 --- a/example/src/screens/index.ts +++ b/example/src/screens/index.ts @@ -11,3 +11,4 @@ export { default as SurfaceComponentUsageScreen } from './usage/SurfaceComponent export { default as TwitterProfileScreen } from './usage/TwitterProfile'; export { default as AbsoluteHeaderBlurSurfaceUsageScreen } from './usage/AbsoluteHeaderBlurSurface'; export { default as ArbitraryYTransitionHeaderUsageScreen } from './usage/ArbitraryYTransitionHeader'; +export { default as CustomOnScrollWorkletUsageScreen } from './usage/CustomWorklet'; diff --git a/example/src/screens/usage/CustomWorklet.tsx b/example/src/screens/usage/CustomWorklet.tsx new file mode 100644 index 0000000..cc693bf --- /dev/null +++ b/example/src/screens/usage/CustomWorklet.tsx @@ -0,0 +1,82 @@ +import React, { useMemo } from 'react'; +import { NativeScrollEvent, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useNavigation } from '@react-navigation/native'; +import { Header, LargeHeader, ScrollViewWithHeaders } from '@codeherence/react-native-header'; +import type { ScrollHeaderProps, ScrollLargeHeaderProps } from '@codeherence/react-native-header'; +import { range } from '../../utils'; +import { Avatar, BackButton } from '../../components'; +import { RANDOM_IMAGE_NUM } from '../../constants'; +import type { CustomOnScrollWorkletUsageScreenNavigationProps } from '../../navigation'; + +const HeaderComponent: React.FC = ({ showNavBar }) => { + const navigation = useNavigation(); + const onPressProfile = () => navigation.navigate('Profile'); + + return ( +
+ Header + + } + headerRight={ + + + + } + headerRightFadesIn + headerLeft={} + /> + ); +}; + +const LargeHeaderComponent: React.FC = () => { + const navigation = useNavigation(); + const onPressProfile = () => navigation.navigate('Profile'); + + return ( + + + + + + ); +}; + +const CustomWorklet: React.FC = () => { + const { bottom } = useSafeAreaInsets(); + + const data = useMemo(() => range({ end: 100 }), []); + + // Example worklet that can be used to track the scroll container's state. + const scrollHandlerWorklet = (evt: NativeScrollEvent) => { + 'worklet'; + console.log('offset: ', evt.contentOffset); + }; + + return ( + + + {data.map((i) => ( + Scroll to see header animation + ))} + + + ); +}; + +export default CustomWorklet; + +const styles = StyleSheet.create({ + children: { marginTop: 16, paddingHorizontal: 16 }, + navBarTitle: { fontSize: 16, fontWeight: 'bold' }, + largeHeaderStyle: { flexDirection: 'row-reverse' }, +}); diff --git a/src/components/containers/FlashList.tsx b/src/components/containers/FlashList.tsx index 5c50041..03f85da 100644 --- a/src/components/containers/FlashList.tsx +++ b/src/components/containers/FlashList.tsx @@ -18,6 +18,8 @@ const AnimatedFlashList = Animated.createAnimatedComponent(FlashList) as React.C unknown >; +type FlashListWithHeadersProps = Omit, 'onScroll'>; + const FlashListWithHeadersInputComp = ( { largeHeaderShown, @@ -28,12 +30,14 @@ const FlashListWithHeadersInputComp = ( onLargeHeaderLayout, onScrollBeginDrag, onScrollEndDrag, + onScrollWorklet, onMomentumScrollBegin, onMomentumScrollEnd, ignoreLeftSafeArea, ignoreRightSafeArea, disableAutoFixScroll = false, - /** At the moment, we will not allow overriding of this since the scrollHandler needs it. */ + // We use this to ensure that the onScroll property isn't accidentally used. + // @ts-ignore onScroll: _unusedOnScroll, absoluteHeader = false, initialAbsoluteHeaderHeight = 0, @@ -44,9 +48,15 @@ const FlashListWithHeadersInputComp = ( scrollIndicatorInsets = {}, inverted, ...rest - }: AnimatedFlashListType, + }: FlashListWithHeadersProps, ref: React.Ref> ) => { + if (_unusedOnScroll) { + throw new Error( + "The 'onScroll' property is not supported. Please use onScrollWorklet to track the scroll container's state." + ); + } + const insets = useSafeAreaInsets(); const scrollRef = useAnimatedRef(); useImperativeHandle(ref, () => scrollRef.current); @@ -69,6 +79,7 @@ const FlashListWithHeadersInputComp = ( initialAbsoluteHeaderHeight, headerFadeInThreshold, inverted: !!inverted, + onScrollWorklet, }); return ( @@ -154,7 +165,7 @@ const FlashListWithHeadersInputComp = ( // The typecast is needed to make the component generic. const FlashListWithHeaders = React.forwardRef(FlashListWithHeadersInputComp) as ( - props: AnimatedFlashListType & { ref?: React.Ref> } + props: FlashListWithHeadersProps & { ref?: React.Ref> } ) => React.ReactElement; export default FlashListWithHeaders; diff --git a/src/components/containers/FlatList.tsx b/src/components/containers/FlatList.tsx index a59b81b..f4105a7 100644 --- a/src/components/containers/FlatList.tsx +++ b/src/components/containers/FlatList.tsx @@ -14,6 +14,11 @@ type AnimatedFlatListType = React.ComponentClass< type AnimatedFlatListBaseProps = React.ComponentProps>; type AnimatedFlatListProps = AnimatedFlatListBaseProps; +type FlatListWithHeadersProps = Omit< + AnimatedFlatListProps & SharedScrollContainerProps, + 'onScroll' +>; + const FlatListWithHeadersInputComp = ( { largeHeaderShown, @@ -24,12 +29,14 @@ const FlatListWithHeadersInputComp = ( onLargeHeaderLayout, onScrollBeginDrag, onScrollEndDrag, + onScrollWorklet, onMomentumScrollBegin, onMomentumScrollEnd, ignoreLeftSafeArea, ignoreRightSafeArea, disableAutoFixScroll = false, - /** At the moment, we will not allow overriding of this since the scrollHandler needs it. */ + // We use this to ensure that the onScroll property isn't accidentally used. + // @ts-ignore onScroll: _unusedOnScroll, absoluteHeader = false, initialAbsoluteHeaderHeight = 0, @@ -40,9 +47,15 @@ const FlatListWithHeadersInputComp = ( scrollIndicatorInsets = {}, inverted, ...rest - }: AnimatedFlatListProps & SharedScrollContainerProps, + }: FlatListWithHeadersProps, ref: React.Ref | null> ) => { + if (_unusedOnScroll) { + throw new Error( + "The 'onScroll' property is not supported. Please use onScrollWorklet to track the scroll container's state." + ); + } + const insets = useSafeAreaInsets(); const scrollRef = useAnimatedRef>(); useImperativeHandle(ref, () => scrollRef.current); @@ -65,6 +78,7 @@ const FlatListWithHeadersInputComp = ( initialAbsoluteHeaderHeight, headerFadeInThreshold, inverted: !!inverted, + onScrollWorklet, }); return ( @@ -150,8 +164,7 @@ const FlatListWithHeadersInputComp = ( // The typecast is needed to make the component generic. const FlatListWithHeaders = React.forwardRef(FlatListWithHeadersInputComp) as ( - props: AnimatedFlatListProps & - SharedScrollContainerProps & { ref?: React.Ref> } + props: FlatListWithHeadersProps & { ref?: React.Ref> } ) => React.ReactElement; export default FlatListWithHeaders; diff --git a/src/components/containers/ScrollView.tsx b/src/components/containers/ScrollView.tsx index a7d72e5..d45359f 100644 --- a/src/components/containers/ScrollView.tsx +++ b/src/components/containers/ScrollView.tsx @@ -13,6 +13,11 @@ type AnimatedScrollViewProps = React.ComponentProps & children?: React.ReactNode; }; +type ScrollViewWithHeadersProps = Omit< + AnimatedScrollViewProps & SharedScrollContainerProps, + 'onScroll' +>; + const ScrollViewWithHeadersInputComp = ( { largeHeaderShown, @@ -23,14 +28,16 @@ const ScrollViewWithHeadersInputComp = ( onLargeHeaderLayout, ignoreLeftSafeArea, ignoreRightSafeArea, + // We use this to ensure that the onScroll property isn't accidentally used. + // @ts-ignore + onScroll: _unusedOnScroll, onScrollBeginDrag, onScrollEndDrag, + onScrollWorklet, onMomentumScrollBegin, onMomentumScrollEnd, disableAutoFixScroll = false, children, - /** At the moment, we will not allow overriding of this since the scrollHandler needs it. */ - onScroll: _unusedOnScroll, absoluteHeader = false, initialAbsoluteHeaderHeight = 0, contentContainerStyle, @@ -39,9 +46,15 @@ const ScrollViewWithHeadersInputComp = ( scrollIndicatorInsets = {}, disableLargeHeaderFadeAnim = false, ...rest - }: AnimatedScrollViewProps & SharedScrollContainerProps, + }: ScrollViewWithHeadersProps, ref: React.Ref ) => { + if (_unusedOnScroll) { + throw new Error( + "The 'onScroll' property is not supported. Please use onScrollWorklet to track the scroll container's state." + ); + } + const insets = useSafeAreaInsets(); const scrollRef = useAnimatedRef(); useImperativeHandle(ref, () => scrollRef.current); @@ -63,6 +76,7 @@ const ScrollViewWithHeadersInputComp = ( absoluteHeader, initialAbsoluteHeaderHeight, headerFadeInThreshold, + onScrollWorklet, }); return ( @@ -145,10 +159,9 @@ const ScrollViewWithHeadersInputComp = ( ); }; -const ScrollViewWithHeaders = React.forwardRef< - Animated.ScrollView, - AnimatedScrollViewProps & SharedScrollContainerProps ->(ScrollViewWithHeadersInputComp); +const ScrollViewWithHeaders = React.forwardRef( + ScrollViewWithHeadersInputComp +); export default ScrollViewWithHeaders; diff --git a/src/components/containers/SectionList.tsx b/src/components/containers/SectionList.tsx index 3e264b6..5495255 100644 --- a/src/components/containers/SectionList.tsx +++ b/src/components/containers/SectionList.tsx @@ -18,6 +18,11 @@ const AnimatedSectionList = Animated.createAnimatedComponent(SectionList) as Rea any >; +type SectionListWithHeadersProps = Omit< + AnimatedSectionListType, + 'onScroll' +>; + const SectionListWithHeadersInputComp = ( { largeHeaderShown, @@ -28,12 +33,14 @@ const SectionListWithHeadersInputComp = , + }: SectionListWithHeadersProps, ref: React.Ref ) => { + if (_unusedOnScroll) { + throw new Error( + "The 'onScroll' property is not supported. Please use onScrollWorklet to track the scroll container's state." + ); + } + const insets = useSafeAreaInsets(); const scrollRef = useAnimatedRef(); useImperativeHandle(ref, () => scrollRef.current); @@ -69,6 +82,7 @@ const SectionListWithHeadersInputComp = ( - props: AnimatedSectionListType & { ref?: React.Ref } + props: SectionListWithHeadersProps & { ref?: React.Ref } ) => React.ReactElement; export default SectionListWithHeaders; diff --git a/src/components/containers/types.ts b/src/components/containers/types.ts index 6187c46..77bf004 100644 --- a/src/components/containers/types.ts +++ b/src/components/containers/types.ts @@ -120,10 +120,19 @@ export type SharedScrollContainerProps = { */ onMomentumScrollEnd?: (event: NativeSyntheticEvent) => void; /** - * This property is not supported at the moment. If you would like to listen to - * scroll events, use the useScrollViewOffset hook with a ref. - */ - onScroll?: React.ComponentProps['onScroll']; + * A custom worklet that allows custom tracking scroll container's + * state (i.e., its scroll contentInset, contentOffset, etc.). Please + * ensure that this function is a [worklet](https://docs.swmansion.com/react-native-reanimated/docs/2.x/fundamentals/worklets/). + * + * @example + * ``` + * const scrollHandlerWorklet = (evt: NativeScrollEvent) => { + * 'worklet'; + * console.log('offset: ', evt.contentOffset); + * }; + * ``` + */ + onScrollWorklet?: (evt: NativeScrollEvent) => void; /** * This property controls whether or not the header component is absolutely positioned. * This is useful if you want to render a header component that allows for transparency. diff --git a/src/components/containers/useScrollContainerLogic.ts b/src/components/containers/useScrollContainerLogic.ts index dec2e5e..a4b536c 100644 --- a/src/components/containers/useScrollContainerLogic.ts +++ b/src/components/containers/useScrollContainerLogic.ts @@ -1,5 +1,5 @@ import { useCallback, useMemo, useState } from 'react'; -import { LayoutChangeEvent } from 'react-native'; +import { LayoutChangeEvent, NativeScrollEvent } from 'react-native'; import Animated, { interpolate, runOnUI, @@ -70,6 +70,12 @@ interface UseScrollContainerLogicArgs { * Whether or not the scroll container is inverted. */ inverted?: boolean; + /** + * A custom worklet that allows custom tracking scroll container's + * state (i.e., its scroll contentInset, contentOffset, etc.). Please + * ensure that this function is a [worklet](https://docs.swmansion.com/react-native-reanimated/docs/2.x/fundamentals/worklets/). + */ + onScrollWorklet?: (evt: NativeScrollEvent) => void; } /** @@ -88,14 +94,20 @@ export const useScrollContainerLogic = ({ initialAbsoluteHeaderHeight = 0, headerFadeInThreshold = 1, inverted, + onScrollWorklet, }: UseScrollContainerLogicArgs) => { const [absoluteHeaderHeight, setAbsoluteHeaderHeight] = useState(initialAbsoluteHeaderHeight); const scrollY = useSharedValue(0); const largeHeaderHeight = useSharedValue(0); - const scrollHandler = useAnimatedScrollHandler((event) => { - scrollY.value = event.contentOffset.y; - }); + const scrollHandler = useAnimatedScrollHandler( + (event) => { + if (onScrollWorklet) onScrollWorklet(event); + + scrollY.value = event.contentOffset.y; + }, + [onScrollWorklet] + ); const showNavBar = useDerivedValue(() => { if (!largeHeaderExists) return withTiming(scrollY.value <= 0 ? 0 : 1, { duration: 250 });