react-native-animated-header-flat-list 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +224 -0
  3. package/lib/commonjs/components/AnimatedHeaderFlatList.js +195 -0
  4. package/lib/commonjs/components/AnimatedHeaderFlatList.js.map +1 -0
  5. package/lib/commonjs/hooks/useAnimatedHeaderFlatListAnimatedStyles.js +102 -0
  6. package/lib/commonjs/hooks/useAnimatedHeaderFlatListAnimatedStyles.js.map +1 -0
  7. package/lib/commonjs/index.js +13 -0
  8. package/lib/commonjs/index.js.map +1 -0
  9. package/lib/module/components/AnimatedHeaderFlatList.js +189 -0
  10. package/lib/module/components/AnimatedHeaderFlatList.js.map +1 -0
  11. package/lib/module/hooks/useAnimatedHeaderFlatListAnimatedStyles.js +97 -0
  12. package/lib/module/hooks/useAnimatedHeaderFlatListAnimatedStyles.js.map +1 -0
  13. package/lib/module/index.js +5 -0
  14. package/lib/module/index.js.map +1 -0
  15. package/lib/typescript/commonjs/package.json +1 -0
  16. package/lib/typescript/commonjs/src/components/AnimatedHeaderFlatList.d.ts +18 -0
  17. package/lib/typescript/commonjs/src/components/AnimatedHeaderFlatList.d.ts.map +1 -0
  18. package/lib/typescript/commonjs/src/hooks/useAnimatedHeaderFlatListAnimatedStyles.d.ts +24 -0
  19. package/lib/typescript/commonjs/src/hooks/useAnimatedHeaderFlatListAnimatedStyles.d.ts.map +1 -0
  20. package/lib/typescript/commonjs/src/index.d.ts +3 -0
  21. package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
  22. package/lib/typescript/module/package.json +1 -0
  23. package/lib/typescript/module/src/components/AnimatedHeaderFlatList.d.ts +18 -0
  24. package/lib/typescript/module/src/components/AnimatedHeaderFlatList.d.ts.map +1 -0
  25. package/lib/typescript/module/src/hooks/useAnimatedHeaderFlatListAnimatedStyles.d.ts +24 -0
  26. package/lib/typescript/module/src/hooks/useAnimatedHeaderFlatListAnimatedStyles.d.ts.map +1 -0
  27. package/lib/typescript/module/src/index.d.ts +3 -0
  28. package/lib/typescript/module/src/index.d.ts.map +1 -0
  29. package/package.json +199 -0
  30. package/src/components/AnimatedHeaderFlatList.tsx +299 -0
  31. package/src/hooks/useAnimatedHeaderFlatListAnimatedStyles.ts +179 -0
  32. package/src/index.tsx +3 -0
@@ -0,0 +1,299 @@
1
+ import React from 'react';
2
+ import {
3
+ StyleSheet,
4
+ View,
5
+ type LayoutChangeEvent,
6
+ type ListRenderItemInfo,
7
+ type StyleProp,
8
+ type TextStyle,
9
+ type ViewStyle,
10
+ } from 'react-native';
11
+ import { useLayoutEffect, useCallback, useMemo } from 'react';
12
+ import type { FlatListPropsWithLayout } from 'react-native-reanimated';
13
+ import { type NavigationProp } from '@react-navigation/native';
14
+ import { useAnimatedHeaderFlatListAnimatedStyles } from '../hooks/useAnimatedHeaderFlatListAnimatedStyles';
15
+ import Animated from 'react-native-reanimated';
16
+
17
+ // Types
18
+ interface Props {
19
+ navigation: NavigationProp<any>;
20
+ title: string;
21
+ headerTitleStyle?: StyleProp<TextStyle>;
22
+ navigationTitleStyle?: StyleProp<TextStyle>;
23
+ HeaderBackground: React.ComponentType<any>;
24
+ HeaderContent?: React.ComponentType<any>;
25
+ StickyComponent?: React.ComponentType<any>;
26
+ style?: StyleProp<ViewStyle>;
27
+ }
28
+
29
+ type AnimatedHeaderFlatListProps<T> = Omit<
30
+ FlatListPropsWithLayout<T>,
31
+ keyof Props
32
+ > &
33
+ Props;
34
+
35
+ export function AnimatedHeaderFlatList<T>({
36
+ navigation,
37
+ title,
38
+ headerTitleStyle,
39
+ navigationTitleStyle,
40
+ HeaderBackground,
41
+ HeaderContent,
42
+ StickyComponent,
43
+ style,
44
+ ...flatListProps
45
+ }: AnimatedHeaderFlatListProps<T>) {
46
+ const getFontSizeFromStyle = useCallback(
47
+ (textStyle: StyleProp<TextStyle>) => {
48
+ if (!textStyle) return undefined;
49
+ if (Array.isArray(textStyle)) {
50
+ for (const styleItem of textStyle) {
51
+ if (
52
+ styleItem &&
53
+ typeof styleItem === 'object' &&
54
+ 'fontSize' in styleItem
55
+ ) {
56
+ return styleItem.fontSize;
57
+ }
58
+ }
59
+ } else if (typeof textStyle === 'object' && 'fontSize' in textStyle) {
60
+ return textStyle.fontSize;
61
+ }
62
+ return undefined;
63
+ },
64
+ []
65
+ );
66
+
67
+ const {
68
+ scrollHandler,
69
+ navigationBarHeight,
70
+ headerLayout,
71
+ setHeaderLayout,
72
+ setHeaderTitleLayout,
73
+ stickyComponentLayout,
74
+ setStickyComponentLayout,
75
+ navigationTitleAnimatedStyle,
76
+ headerTitleAnimatedStyle,
77
+ stickyHeaderAnimatedStyle,
78
+ headerContentAnimatedStyle,
79
+ headerBackgroundAnimatedStyle,
80
+ } = useAnimatedHeaderFlatListAnimatedStyles({
81
+ headerTitleFontSize: getFontSizeFromStyle(headerTitleStyle),
82
+ navigationTitleFontSize: getFontSizeFromStyle(navigationTitleStyle),
83
+ });
84
+
85
+ const navigationTitle = useCallback(
86
+ () => (
87
+ <Animated.Text
88
+ style={[
89
+ navigationTitleAnimatedStyle,
90
+ navigationTitleStyle,
91
+ styles.titleStyle,
92
+ ]}
93
+ numberOfLines={1}
94
+ >
95
+ {title}
96
+ </Animated.Text>
97
+ ),
98
+ [navigationTitleAnimatedStyle, navigationTitleStyle, title]
99
+ );
100
+
101
+ useLayoutEffect(() => {
102
+ navigation.setOptions({
103
+ headerShown: true,
104
+ headerStyle: styles.navigationBar,
105
+ headerShadowVisible: false,
106
+ headerTransparent: true,
107
+ headerTitle: navigationTitle,
108
+ headerTitleAlign: 'center',
109
+ });
110
+ }, [navigationTitle, navigation]);
111
+
112
+ const ListHeaderComponent = useMemo(() => {
113
+ return (
114
+ <View style={styles.headerWrapper}>
115
+ <View
116
+ style={[styles.headerContainer, { top: -navigationBarHeight }]}
117
+ onLayout={(event: LayoutChangeEvent) => {
118
+ setHeaderLayout({
119
+ ...event.nativeEvent.layout,
120
+ height: event.nativeEvent.layout.height + navigationBarHeight,
121
+ });
122
+ }}
123
+ >
124
+ <Animated.View style={headerBackgroundAnimatedStyle}>
125
+ <HeaderBackground />
126
+ </Animated.View>
127
+
128
+ {HeaderContent && (
129
+ <Animated.View
130
+ style={[
131
+ headerContentAnimatedStyle,
132
+ styles.headerContentContainer,
133
+ ]}
134
+ >
135
+ <HeaderContent />
136
+ </Animated.View>
137
+ )}
138
+
139
+ <Animated.Text
140
+ onLayout={(event: LayoutChangeEvent) => {
141
+ setHeaderTitleLayout(event.nativeEvent.layout);
142
+ }}
143
+ numberOfLines={1}
144
+ style={[
145
+ headerTitleAnimatedStyle,
146
+ styles.headerTitle,
147
+ headerTitleStyle,
148
+ ]}
149
+ >
150
+ {title}
151
+ </Animated.Text>
152
+ </View>
153
+ </View>
154
+ );
155
+ }, [
156
+ navigationBarHeight,
157
+ headerBackgroundAnimatedStyle,
158
+ HeaderBackground,
159
+ HeaderContent,
160
+ headerContentAnimatedStyle,
161
+ headerTitleAnimatedStyle,
162
+ headerTitleStyle,
163
+ title,
164
+ setHeaderLayout,
165
+ setHeaderTitleLayout,
166
+ ]);
167
+
168
+ const renderItem = useCallback(
169
+ ({ item }: { item: 'HEADER' | T }) => {
170
+ if (item === 'HEADER') {
171
+ return (
172
+ <View
173
+ style={[
174
+ styles.stickyHeaderContainer,
175
+ {
176
+ height: navigationBarHeight + stickyComponentLayout.height,
177
+ },
178
+ ]}
179
+ >
180
+ <Animated.View
181
+ style={[
182
+ stickyHeaderAnimatedStyle,
183
+ styles.stickyHeader,
184
+ {
185
+ bottom:
186
+ headerLayout.height -
187
+ navigationBarHeight * 2 +
188
+ stickyComponentLayout.height,
189
+ },
190
+ ]}
191
+ >
192
+ {ListHeaderComponent}
193
+ </Animated.View>
194
+ {StickyComponent && (
195
+ <View
196
+ style={styles.stickyComponentContainer}
197
+ onLayout={(event: LayoutChangeEvent) => {
198
+ setStickyComponentLayout(event.nativeEvent.layout);
199
+ }}
200
+ >
201
+ <StickyComponent />
202
+ </View>
203
+ )}
204
+ </View>
205
+ );
206
+ }
207
+ return flatListProps.renderItem &&
208
+ typeof flatListProps.renderItem === 'function'
209
+ ? flatListProps.renderItem({ item } as ListRenderItemInfo<T>)
210
+ : null;
211
+ },
212
+ [
213
+ flatListProps,
214
+ navigationBarHeight,
215
+ stickyComponentLayout.height,
216
+ stickyHeaderAnimatedStyle,
217
+ headerLayout.height,
218
+ ListHeaderComponent,
219
+ StickyComponent,
220
+ setStickyComponentLayout,
221
+ ]
222
+ );
223
+
224
+ return (
225
+ <Animated.FlatList
226
+ {...flatListProps}
227
+ style={style}
228
+ stickyHeaderHiddenOnScroll={false}
229
+ stickyHeaderIndices={[1]}
230
+ ListHeaderComponent={
231
+ <Animated.View
232
+ style={[
233
+ styles.mainHeaderContainer,
234
+ {
235
+ height: headerLayout.height - navigationBarHeight * 2,
236
+ transform: [{ translateY: navigationBarHeight }],
237
+ },
238
+ ]}
239
+ >
240
+ {ListHeaderComponent}
241
+ </Animated.View>
242
+ }
243
+ onScroll={scrollHandler}
244
+ data={[
245
+ 'HEADER',
246
+ ...(Array.isArray(flatListProps.data) ? flatListProps.data : []),
247
+ ]}
248
+ renderItem={renderItem}
249
+ />
250
+ );
251
+ }
252
+
253
+ const styles = StyleSheet.create({
254
+ navigationBar: {
255
+ backgroundColor: 'transparent',
256
+ },
257
+ titleStyle: {
258
+ left: 0,
259
+ right: 0,
260
+ top: 0,
261
+ bottom: 0,
262
+ },
263
+ headerWrapper: {
264
+ overflow: 'visible',
265
+ },
266
+ headerContainer: {
267
+ left: 0,
268
+ right: 0,
269
+ overflow: 'visible',
270
+ position: 'absolute',
271
+ },
272
+ stickyHeaderContainer: {
273
+ overflow: 'scroll',
274
+ },
275
+ stickyHeader: {
276
+ position: 'absolute',
277
+ left: 0,
278
+ right: 0,
279
+ },
280
+ mainHeaderContainer: {
281
+ overflow: 'visible',
282
+ },
283
+ headerTitle: {
284
+ position: 'absolute',
285
+ },
286
+ stickyComponentContainer: {
287
+ position: 'absolute',
288
+ bottom: 0,
289
+ left: 0,
290
+ right: 0,
291
+ },
292
+ headerContentContainer: {
293
+ position: 'absolute',
294
+ top: 0,
295
+ left: 0,
296
+ right: 0,
297
+ bottom: 0,
298
+ },
299
+ });
@@ -0,0 +1,179 @@
1
+ import { useHeaderHeight } from '@react-navigation/elements';
2
+ import { useState } from 'react';
3
+ import {
4
+ useWindowDimensions,
5
+ type LayoutRectangle,
6
+ type ViewStyle,
7
+ } from 'react-native';
8
+ import {
9
+ interpolate,
10
+ useAnimatedScrollHandler,
11
+ useAnimatedStyle,
12
+ useSharedValue,
13
+ type AnimatedStyle,
14
+ type ScrollHandlerProcessed,
15
+ } from 'react-native-reanimated';
16
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
17
+
18
+ type AnimatedHeaderFlatListAnimatedStylesProps = {
19
+ headerTitleFontSize?: number;
20
+ navigationTitleFontSize?: number;
21
+ };
22
+
23
+ type AnimatedHeaderFlatListAnimatedStyles = {
24
+ scrollHandler: ScrollHandlerProcessed<Record<string, unknown>>;
25
+ navigationBarHeight: number;
26
+ headerLayout: LayoutRectangle;
27
+ setHeaderLayout: (layout: LayoutRectangle) => void;
28
+ headerTitleLayout: LayoutRectangle;
29
+ setHeaderTitleLayout: (layout: LayoutRectangle) => void;
30
+ stickyComponentLayout: LayoutRectangle;
31
+ setStickyComponentLayout: (layout: LayoutRectangle) => void;
32
+ navigationTitleAnimatedStyle: AnimatedStyle<ViewStyle>;
33
+ headerTitleAnimatedStyle: AnimatedStyle<ViewStyle>;
34
+ stickyHeaderAnimatedStyle: AnimatedStyle<ViewStyle>;
35
+ headerContentAnimatedStyle: AnimatedStyle<ViewStyle>;
36
+ headerBackgroundAnimatedStyle: AnimatedStyle<ViewStyle>;
37
+ };
38
+
39
+ export const useAnimatedHeaderFlatListAnimatedStyles = ({
40
+ headerTitleFontSize,
41
+ navigationTitleFontSize,
42
+ }: AnimatedHeaderFlatListAnimatedStylesProps): AnimatedHeaderFlatListAnimatedStyles => {
43
+ const { width: windowWidth } = useWindowDimensions();
44
+ const scrollY = useSharedValue(0);
45
+ const navigationBarHeight = useHeaderHeight();
46
+ const safeAreaInsets = useSafeAreaInsets();
47
+ const [headerLayout, setHeaderLayout] = useState<LayoutRectangle>({
48
+ x: 0,
49
+ y: 0,
50
+ width: 0,
51
+ height: 0,
52
+ });
53
+ const [headerTitleLayout, setHeaderTitleLayout] = useState<LayoutRectangle>({
54
+ x: 0,
55
+ y: 0,
56
+ width: 0,
57
+ height: 0,
58
+ });
59
+ const [stickyComponentLayout, setStickyComponentLayout] =
60
+ useState<LayoutRectangle>({
61
+ x: 0,
62
+ y: 0,
63
+ width: 0,
64
+ height: 0,
65
+ });
66
+ const distanceBetweenTitleAndNavigationBar =
67
+ (navigationBarHeight - safeAreaInsets.top + headerTitleLayout.height) / 2 +
68
+ headerTitleLayout.y -
69
+ navigationBarHeight;
70
+ const navigationTitleOpacity = useSharedValue(0);
71
+ const stickyHeaderOpacity = useSharedValue(0);
72
+ const navigationTitleAnimatedStyle = useAnimatedStyle(() => {
73
+ return {
74
+ opacity: navigationTitleOpacity.value,
75
+ };
76
+ });
77
+ const headerTitleAnimatedStyle = useAnimatedStyle(() => {
78
+ return {
79
+ opacity: 1 - navigationTitleOpacity.value,
80
+ transform: [
81
+ {
82
+ translateX: interpolate(
83
+ scrollY.value,
84
+ [0, distanceBetweenTitleAndNavigationBar],
85
+ [
86
+ 0,
87
+ windowWidth / 2 -
88
+ headerTitleLayout.x -
89
+ headerTitleLayout.width / 2,
90
+ ],
91
+ 'clamp'
92
+ ),
93
+ },
94
+ {
95
+ scale: interpolate(
96
+ scrollY.value,
97
+ [0, distanceBetweenTitleAndNavigationBar],
98
+ [
99
+ 1,
100
+ navigationTitleFontSize && headerTitleFontSize
101
+ ? navigationTitleFontSize / headerTitleFontSize
102
+ : 1,
103
+ ],
104
+ 'clamp'
105
+ ),
106
+ },
107
+ ],
108
+ };
109
+ });
110
+ const stickyHeaderAnimatedStyle = useAnimatedStyle(() => {
111
+ return {
112
+ opacity: stickyHeaderOpacity.value,
113
+ };
114
+ });
115
+ const headerContentAnimatedStyle = useAnimatedStyle(() => {
116
+ return {
117
+ opacity: interpolate(
118
+ scrollY.value,
119
+ [0, headerLayout.height - navigationBarHeight * 2],
120
+ [1, 0],
121
+ 'clamp'
122
+ ),
123
+ };
124
+ });
125
+ const headerBackgroundAnimatedStyle = useAnimatedStyle(() => {
126
+ if (scrollY.value >= 0) {
127
+ return {};
128
+ }
129
+ return {
130
+ transform: [
131
+ {
132
+ translateY: interpolate(
133
+ scrollY.value,
134
+ [scrollY.value, 0],
135
+ [scrollY.value / 2, 0],
136
+ 'clamp'
137
+ ),
138
+ },
139
+ {
140
+ scale: interpolate(
141
+ scrollY.value,
142
+ [scrollY.value, 0],
143
+ [
144
+ 1 - scrollY.value / (headerLayout.height - navigationBarHeight),
145
+ 1,
146
+ 1,
147
+ ],
148
+ 'clamp'
149
+ ),
150
+ },
151
+ ],
152
+ };
153
+ });
154
+ const scrollHandler = useAnimatedScrollHandler((event) => {
155
+ scrollY.value = event.contentOffset.y;
156
+ navigationTitleOpacity.value =
157
+ event.contentOffset.y >= distanceBetweenTitleAndNavigationBar ? 1 : 0;
158
+ stickyHeaderOpacity.value =
159
+ event.contentOffset.y >= headerLayout.height - navigationBarHeight * 2
160
+ ? 1
161
+ : 0;
162
+ });
163
+
164
+ return {
165
+ scrollHandler,
166
+ navigationBarHeight,
167
+ headerLayout,
168
+ setHeaderLayout,
169
+ headerTitleLayout,
170
+ setHeaderTitleLayout,
171
+ stickyComponentLayout,
172
+ setStickyComponentLayout,
173
+ navigationTitleAnimatedStyle,
174
+ headerTitleAnimatedStyle,
175
+ stickyHeaderAnimatedStyle,
176
+ headerContentAnimatedStyle,
177
+ headerBackgroundAnimatedStyle,
178
+ };
179
+ };
package/src/index.tsx ADDED
@@ -0,0 +1,3 @@
1
+ import { AnimatedHeaderFlatList } from './components/AnimatedHeaderFlatList';
2
+
3
+ export { AnimatedHeaderFlatList };