-
Notifications
You must be signed in to change notification settings - Fork 44
Expand file tree
/
Copy pathuseScrollContainerLogic.ts
More file actions
210 lines (195 loc) · 6.6 KB
/
useScrollContainerLogic.ts
File metadata and controls
210 lines (195 loc) · 6.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
import { useCallback, useMemo, useState } from 'react';
import { LayoutChangeEvent, NativeScrollEvent } from 'react-native';
import {
interpolate,
runOnUI,
scrollTo,
useDerivedValue,
useSharedValue,
withTiming,
useAnimatedScrollHandler,
AnimatedRef,
} from 'react-native-reanimated';
import { useDebouncedCallback } from 'use-debounce';
import type { SharedScrollContainerProps } from './types';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
/**
* The arguments for the useScrollContainerLogic hook.
*/
interface UseScrollContainerLogicArgs {
/**
* The ScrollView or FlatList ref that is rendered in the scroll container.
*
* @type {AnimatedRef<Animated.ScrollView | Animated.FlatList<any>>}
*/
scrollRef: AnimatedRef<any>;
/**
* This is a hack to ensure that the larger repositions itself correctly.
*
* @type {number}
* @default 4
*/
adjustmentOffset?: number;
/**
* Whether or not the large header should be shown. This is used to animate the large header in
* and out.
*
* @type {SharedScrollContainerProps['largeHeaderShown']}
*/
largeHeaderShown: SharedScrollContainerProps['largeHeaderShown'];
/**
* Whether or not the large header exists.
*/
largeHeaderExists: boolean;
/**
* Disables the auto fix scroll mechanism. This is useful if you want to disable the auto scroll
* when the large header is partially visible.
*
* @default false
*/
disableAutoFixScroll?: boolean;
/**
* 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.
*/
absoluteHeader?: boolean;
/**
* This property is used when `absoluteHeader` is true. This is the initial height of the
* absolute header. Since the header's height is computed on its layout event, this is used
* to set the initial height of the header so that it doesn't jump when it is initially rendered.
*/
initialAbsoluteHeaderHeight?: number;
/**
* A number between 0 and 1 representing at what point the header should fade in,
* based on the percentage of the LargeHeader's height. For example, if this is set to 0.5,
* the header will fade in when the scroll position is at 50% of the LargeHeader's height.
*
* @default 1
*/
headerFadeInThreshold?: number;
/**
* 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;
/**
* Whether or not the scroll container is a FlashList. This is used because FlashList actually
* implements the inverted property differently and requires a different approach to compute the
* scroll indicator insets and content container style.
*/
isFlashList?: boolean;
}
/**
* This hook computes the animation logic for the scroll container.
*
* @param {UseScrollContainerLogicArgs} args
* @returns {ReturnType<UseScrollContainerLogicArgs>}
*/
export const useScrollContainerLogic = ({
scrollRef,
largeHeaderShown,
largeHeaderExists,
disableAutoFixScroll = false,
adjustmentOffset = 4,
absoluteHeader = false,
initialAbsoluteHeaderHeight = 0,
headerFadeInThreshold = 1,
inverted,
onScrollWorklet,
isFlashList = false,
}: UseScrollContainerLogicArgs) => {
const insets = useSafeAreaInsets();
const [absoluteHeaderHeight, setAbsoluteHeaderHeight] = useState(initialAbsoluteHeaderHeight);
const scrollY = useSharedValue(0);
const largeHeaderHeight = useSharedValue(0);
const scrollHandler = useAnimatedScrollHandler(
(event) => {
if (onScrollWorklet) onScrollWorklet(event);
scrollY.value = event.contentOffset.y;
},
[onScrollWorklet]
);
const showNavBar = useDerivedValue<number>(() => {
if (!largeHeaderExists) return withTiming(scrollY.value <= 0 ? 0 : 1, { duration: 250 });
if (largeHeaderHeight.value < adjustmentOffset) return 0;
if (largeHeaderShown) {
largeHeaderShown.value = withTiming(
scrollY.value <= largeHeaderHeight.value * headerFadeInThreshold - adjustmentOffset ? 0 : 1,
{ duration: 250 }
);
}
return withTiming(
scrollY.value <= largeHeaderHeight.value * headerFadeInThreshold - adjustmentOffset ? 0 : 1,
{ duration: 250 }
);
}, [largeHeaderExists]);
const largeHeaderOpacity = useDerivedValue(() => {
return interpolate(showNavBar.value, [0, 1], [1, 0]);
});
const debouncedFixScroll = useDebouncedCallback(() => {
if (disableAutoFixScroll) return;
if (largeHeaderHeight.value !== 0 && scrollRef && scrollRef.current) {
if (scrollY.value >= largeHeaderHeight.value / 2 && scrollY.value < largeHeaderHeight.value) {
// Scroll to end of large header
runOnUI(() => {
'worklet';
scrollTo(scrollRef, 0, largeHeaderHeight.value, true);
})();
} else if (scrollY.value >= 0 && scrollY.value < largeHeaderHeight.value / 2) {
// Scroll to top
runOnUI(() => {
'worklet';
scrollTo(scrollRef, 0, 0, true);
})();
}
}
}, 50);
const onAbsoluteHeaderLayout = useCallback(
(e: LayoutChangeEvent) => {
if (absoluteHeader) {
setAbsoluteHeaderHeight(e.nativeEvent.layout.height);
}
},
[absoluteHeader]
);
const scrollViewAdjustments = useMemo(() => {
if (isFlashList) {
return {
scrollIndicatorInsets: {
top: absoluteHeader && inverted ? absoluteHeaderHeight : 0,
bottom: insets.bottom,
},
contentContainerStyle: {
paddingTop: absoluteHeader && inverted ? absoluteHeaderHeight : 0,
paddingBottom: insets.bottom,
},
};
}
return {
scrollIndicatorInsets: {
top: absoluteHeader && !inverted ? absoluteHeaderHeight : 0,
bottom: absoluteHeader && inverted ? absoluteHeaderHeight : 0,
},
contentContainerStyle: {
paddingTop: absoluteHeader && !inverted ? absoluteHeaderHeight : 0,
paddingBottom: absoluteHeader && inverted ? absoluteHeaderHeight : 0,
},
};
}, [inverted, absoluteHeaderHeight, absoluteHeader, isFlashList, insets]);
return {
scrollY,
showNavBar,
largeHeaderHeight,
largeHeaderOpacity,
scrollHandler,
debouncedFixScroll,
absoluteHeaderHeight,
onAbsoluteHeaderLayout,
scrollViewAdjustments,
};
};