Skip to content

Commit f8718b4

Browse files
ythomope-younan
andauthored
feat: use user-provided onScroll (#22)
Co-authored-by: eyounan <me@eyounan.com>
1 parent b2aa504 commit f8718b4

File tree

15 files changed

+273
-25
lines changed

15 files changed

+273
-25
lines changed

docs/docs/03-api-reference/01-scroll-view-with-headers.mdx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,22 @@ Defaults to `1`.
112112
Whether or not the LargeHeaderComponent should fade in and out. Defaults to `false`.
113113

114114
**Note**: This is only available in version >= 0.10.0.
115+
116+
### onScrollWorklet
117+
118+
A custom worklet that allows custom tracking scroll container's
119+
state (i.e., its scroll contentInset, contentOffset, etc.). Please
120+
ensure that this function is a [worklet](https://docs.swmansion.com/react-native-reanimated/docs/2.x/fundamentals/worklets/).
121+
122+
Since the library uses the `onScroll` prop to handle animations internally and [reanimated
123+
does not currently allow for multiple onScroll handlers](https://github.com/software-mansion/react-native-reanimated/discussions/1763),
124+
you must use this property to track the state of the scroll container's state.
125+
126+
An example is shown below:
127+
128+
```tsx
129+
const scrollHandlerWorklet = (evt: NativeScrollEvent) => {
130+
'worklet';
131+
console.log('offset: ', evt.contentOffset);
132+
};
133+
```

docs/docs/03-api-reference/02-flat-list-with-headers.mdx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,22 @@ Defaults to `1`.
112112
Whether or not the LargeHeaderComponent should fade in and out. Defaults to `false`.
113113

114114
**Note**: This is only available in version >= 0.10.0.
115+
116+
### onScrollWorklet
117+
118+
A custom worklet that allows custom tracking scroll container's
119+
state (i.e., its scroll contentInset, contentOffset, etc.). Please
120+
ensure that this function is a [worklet](https://docs.swmansion.com/react-native-reanimated/docs/2.x/fundamentals/worklets/).
121+
122+
Since the library uses the `onScroll` prop to handle animations internally and [reanimated
123+
does not currently allow for multiple onScroll handlers](https://github.com/software-mansion/react-native-reanimated/discussions/1763),
124+
you must use this property to track the state of the scroll container's state.
125+
126+
An example is shown below:
127+
128+
```tsx
129+
const scrollHandlerWorklet = (evt: NativeScrollEvent) => {
130+
'worklet';
131+
console.log('offset: ', evt.contentOffset);
132+
};
133+
```

docs/docs/03-api-reference/03-section-list-with-headers.mdx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,22 @@ Defaults to `1`.
112112
Whether or not the LargeHeaderComponent should fade in and out. Defaults to `false`.
113113

114114
**Note**: This is only available in version >= 0.10.0.
115+
116+
### onScrollWorklet
117+
118+
A custom worklet that allows custom tracking scroll container's
119+
state (i.e., its scroll contentInset, contentOffset, etc.). Please
120+
ensure that this function is a [worklet](https://docs.swmansion.com/react-native-reanimated/docs/2.x/fundamentals/worklets/).
121+
122+
Since the library uses the `onScroll` prop to handle animations internally and [reanimated
123+
does not currently allow for multiple onScroll handlers](https://github.com/software-mansion/react-native-reanimated/discussions/1763),
124+
you must use this property to track the state of the scroll container's state.
125+
126+
An example is shown below:
127+
128+
```tsx
129+
const scrollHandlerWorklet = (evt: NativeScrollEvent) => {
130+
'worklet';
131+
console.log('offset: ', evt.contentOffset);
132+
};
133+
```

docs/docs/03-api-reference/04-flash-list-with-headers.mdx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,22 @@ Defaults to `1`.
117117
Whether or not the LargeHeaderComponent should fade in and out. Defaults to `false`.
118118

119119
**Note**: This is only available in version >= 0.10.0.
120+
121+
### onScrollWorklet
122+
123+
A custom worklet that allows custom tracking scroll container's
124+
state (i.e., its scroll contentInset, contentOffset, etc.). Please
125+
ensure that this function is a [worklet](https://docs.swmansion.com/react-native-reanimated/docs/2.x/fundamentals/worklets/).
126+
127+
Since the library uses the `onScroll` prop to handle animations internally and [reanimated
128+
does not currently allow for multiple onScroll handlers](https://github.com/software-mansion/react-native-reanimated/discussions/1763),
129+
you must use this property to track the state of the scroll container's state.
130+
131+
An example is shown below:
132+
133+
```tsx
134+
const scrollHandlerWorklet = (evt: NativeScrollEvent) => {
135+
'worklet';
136+
console.log('offset: ', evt.contentOffset);
137+
};
138+
```

example/src/navigation/AppNavigation.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
AbsoluteHeaderBlurSurfaceUsageScreen,
1414
ArbitraryYTransitionHeaderUsageScreen,
1515
InvertedUsageScreen,
16+
CustomOnScrollWorkletUsageScreen,
1617
} from '../screens';
1718

1819
const Stack = createNativeStackNavigator<RootStackParamList>();
@@ -43,5 +44,9 @@ export default () => (
4344
name="ArbitraryYTransitionHeaderUsageScreen"
4445
component={ArbitraryYTransitionHeaderUsageScreen}
4546
/>
47+
<Stack.Screen
48+
name="CustomOnScrollWorkletUsageScreen"
49+
component={CustomOnScrollWorkletUsageScreen}
50+
/>
4651
</Stack.Navigator>
4752
);

example/src/navigation/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type RootStackParamList = {
1313
AbsoluteHeaderBlurSurfaceUsageScreen: undefined;
1414
ArbitraryYTransitionHeaderUsageScreen: undefined;
1515
InvertedUsageScreen: undefined;
16+
CustomOnScrollWorkletUsageScreen: undefined;
1617
};
1718

1819
// Overrides the typing for useNavigation in @react-navigation/native to support the internal
@@ -71,3 +72,8 @@ export type InvertedUsageScreenNavigationProps = NativeStackScreenProps<
7172
RootStackParamList,
7273
'InvertedUsageScreen'
7374
>;
75+
76+
export type CustomOnScrollWorkletUsageScreenNavigationProps = NativeStackScreenProps<
77+
RootStackParamList,
78+
'CustomOnScrollWorkletUsageScreen'
79+
>;

example/src/screens/Home.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ const SCREEN_LIST_CONFIG: ScreenConfigItem[] = [
6565
description:
6666
'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.',
6767
},
68+
{
69+
name: 'Custom onScroll Worklet Example',
70+
route: 'CustomOnScrollWorkletUsageScreen',
71+
description:
72+
"A simple example with a custom worklet that tracks the scroll container's offset.",
73+
},
6874
];
6975

7076
const HeaderComponent: React.FC<ScrollHeaderProps> = ({ showNavBar }) => {

example/src/screens/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ export { default as SurfaceComponentUsageScreen } from './usage/SurfaceComponent
1111
export { default as TwitterProfileScreen } from './usage/TwitterProfile';
1212
export { default as AbsoluteHeaderBlurSurfaceUsageScreen } from './usage/AbsoluteHeaderBlurSurface';
1313
export { default as ArbitraryYTransitionHeaderUsageScreen } from './usage/ArbitraryYTransitionHeader';
14+
export { default as CustomOnScrollWorkletUsageScreen } from './usage/CustomWorklet';
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import React, { useMemo } from 'react';
2+
import { NativeScrollEvent, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
3+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
4+
import { useNavigation } from '@react-navigation/native';
5+
import { Header, LargeHeader, ScrollViewWithHeaders } from '@codeherence/react-native-header';
6+
import type { ScrollHeaderProps, ScrollLargeHeaderProps } from '@codeherence/react-native-header';
7+
import { range } from '../../utils';
8+
import { Avatar, BackButton } from '../../components';
9+
import { RANDOM_IMAGE_NUM } from '../../constants';
10+
import type { CustomOnScrollWorkletUsageScreenNavigationProps } from '../../navigation';
11+
12+
const HeaderComponent: React.FC<ScrollHeaderProps> = ({ showNavBar }) => {
13+
const navigation = useNavigation();
14+
const onPressProfile = () => navigation.navigate('Profile');
15+
16+
return (
17+
<Header
18+
showNavBar={showNavBar}
19+
headerCenterFadesIn={false}
20+
headerCenter={
21+
<Text style={styles.navBarTitle} numberOfLines={1}>
22+
Header
23+
</Text>
24+
}
25+
headerRight={
26+
<TouchableOpacity onPress={onPressProfile}>
27+
<Avatar size="sm" source={{ uri: `https://i.pravatar.cc/128?img=${RANDOM_IMAGE_NUM}` }} />
28+
</TouchableOpacity>
29+
}
30+
headerRightFadesIn
31+
headerLeft={<BackButton />}
32+
/>
33+
);
34+
};
35+
36+
const LargeHeaderComponent: React.FC<ScrollLargeHeaderProps> = () => {
37+
const navigation = useNavigation();
38+
const onPressProfile = () => navigation.navigate('Profile');
39+
40+
return (
41+
<LargeHeader headerStyle={styles.largeHeaderStyle}>
42+
<TouchableOpacity onPress={onPressProfile}>
43+
<Avatar size="sm" source={{ uri: `https://i.pravatar.cc/128?img=${RANDOM_IMAGE_NUM}` }} />
44+
</TouchableOpacity>
45+
</LargeHeader>
46+
);
47+
};
48+
49+
const CustomWorklet: React.FC<CustomOnScrollWorkletUsageScreenNavigationProps> = () => {
50+
const { bottom } = useSafeAreaInsets();
51+
52+
const data = useMemo(() => range({ end: 100 }), []);
53+
54+
// Example worklet that can be used to track the scroll container's state.
55+
const scrollHandlerWorklet = (evt: NativeScrollEvent) => {
56+
'worklet';
57+
console.log('offset: ', evt.contentOffset);
58+
};
59+
60+
return (
61+
<ScrollViewWithHeaders
62+
HeaderComponent={HeaderComponent}
63+
LargeHeaderComponent={LargeHeaderComponent}
64+
contentContainerStyle={{ paddingBottom: bottom }}
65+
onScrollWorklet={scrollHandlerWorklet}
66+
>
67+
<View style={styles.children}>
68+
{data.map((i) => (
69+
<Text key={`text-${i}`}>Scroll to see header animation</Text>
70+
))}
71+
</View>
72+
</ScrollViewWithHeaders>
73+
);
74+
};
75+
76+
export default CustomWorklet;
77+
78+
const styles = StyleSheet.create({
79+
children: { marginTop: 16, paddingHorizontal: 16 },
80+
navBarTitle: { fontSize: 16, fontWeight: 'bold' },
81+
largeHeaderStyle: { flexDirection: 'row-reverse' },
82+
});

src/components/containers/FlashList.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ const AnimatedFlashList = Animated.createAnimatedComponent(FlashList) as React.C
1818
unknown
1919
>;
2020

21+
type FlashListWithHeadersProps<ItemT> = Omit<AnimatedFlashListType<ItemT>, 'onScroll'>;
22+
2123
const FlashListWithHeadersInputComp = <ItemT extends any = any>(
2224
{
2325
largeHeaderShown,
@@ -28,12 +30,14 @@ const FlashListWithHeadersInputComp = <ItemT extends any = any>(
2830
onLargeHeaderLayout,
2931
onScrollBeginDrag,
3032
onScrollEndDrag,
33+
onScrollWorklet,
3134
onMomentumScrollBegin,
3235
onMomentumScrollEnd,
3336
ignoreLeftSafeArea,
3437
ignoreRightSafeArea,
3538
disableAutoFixScroll = false,
36-
/** At the moment, we will not allow overriding of this since the scrollHandler needs it. */
39+
// We use this to ensure that the onScroll property isn't accidentally used.
40+
// @ts-ignore
3741
onScroll: _unusedOnScroll,
3842
absoluteHeader = false,
3943
initialAbsoluteHeaderHeight = 0,
@@ -44,9 +48,15 @@ const FlashListWithHeadersInputComp = <ItemT extends any = any>(
4448
scrollIndicatorInsets = {},
4549
inverted,
4650
...rest
47-
}: AnimatedFlashListType<ItemT>,
51+
}: FlashListWithHeadersProps<ItemT>,
4852
ref: React.Ref<FlashList<ItemT>>
4953
) => {
54+
if (_unusedOnScroll) {
55+
throw new Error(
56+
"The 'onScroll' property is not supported. Please use onScrollWorklet to track the scroll container's state."
57+
);
58+
}
59+
5060
const insets = useSafeAreaInsets();
5161
const scrollRef = useAnimatedRef<any>();
5262
useImperativeHandle(ref, () => scrollRef.current);
@@ -69,6 +79,7 @@ const FlashListWithHeadersInputComp = <ItemT extends any = any>(
6979
initialAbsoluteHeaderHeight,
7080
headerFadeInThreshold,
7181
inverted: !!inverted,
82+
onScrollWorklet,
7283
});
7384

7485
return (
@@ -154,7 +165,7 @@ const FlashListWithHeadersInputComp = <ItemT extends any = any>(
154165

155166
// The typecast is needed to make the component generic.
156167
const FlashListWithHeaders = React.forwardRef(FlashListWithHeadersInputComp) as <ItemT = any>(
157-
props: AnimatedFlashListType<ItemT> & { ref?: React.Ref<FlashList<ItemT>> }
168+
props: FlashListWithHeadersProps<ItemT> & { ref?: React.Ref<FlashList<ItemT>> }
158169
) => React.ReactElement;
159170

160171
export default FlashListWithHeaders;

0 commit comments

Comments
 (0)