react-native-header-motion 0.4.0 → 1.0.0-beta.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.
- package/README.md +400 -335
- package/lib/module/components/Bridge.js +16 -0
- package/lib/module/components/Bridge.js.map +1 -0
- package/lib/module/components/FlatList.js +5 -62
- package/lib/module/components/FlatList.js.map +1 -1
- package/lib/module/components/Header.js +71 -13
- package/lib/module/components/Header.js.map +1 -1
- package/lib/module/components/HeaderDynamic.js +34 -0
- package/lib/module/components/HeaderDynamic.js.map +1 -0
- package/lib/module/components/HeaderMotion.js +59 -23
- package/lib/module/components/HeaderMotion.js.map +1 -1
- package/lib/module/components/HeaderPanBoundary.js +54 -0
- package/lib/module/components/HeaderPanBoundary.js.map +1 -0
- package/lib/module/components/NavigationBridge.js +20 -0
- package/lib/module/components/NavigationBridge.js.map +1 -0
- package/lib/module/components/ScrollManager.js +7 -5
- package/lib/module/components/ScrollManager.js.map +1 -1
- package/lib/module/components/ScrollView.js +6 -47
- package/lib/module/components/ScrollView.js.map +1 -1
- package/lib/module/components/createHeaderMotionScrollable.js +136 -0
- package/lib/module/components/createHeaderMotionScrollable.js.map +1 -0
- package/lib/module/components/index.js +3 -1
- package/lib/module/components/index.js.map +1 -1
- package/lib/module/context.js +8 -1
- package/lib/module/context.js.map +1 -1
- package/lib/module/hooks/index.js +1 -0
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/useActiveScrollId.js +7 -6
- package/lib/module/hooks/useActiveScrollId.js.map +1 -1
- package/lib/module/hooks/useHeaderMotionBridge.js +14 -0
- package/lib/module/hooks/useHeaderMotionBridge.js.map +1 -0
- package/lib/module/hooks/useMotionProgress.js +10 -36
- package/lib/module/hooks/useMotionProgress.js.map +1 -1
- package/lib/module/hooks/useMotionProgress.test.js +56 -0
- package/lib/module/hooks/useMotionProgress.test.js.map +1 -0
- package/lib/module/hooks/useScrollManager.js +219 -109
- package/lib/module/hooks/useScrollManager.js.map +1 -1
- package/lib/module/index.js +21 -18
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/defaults.js +2 -1
- package/lib/module/utils/defaults.js.map +1 -1
- package/lib/module/utils/header.js +24 -0
- package/lib/module/utils/header.js.map +1 -0
- package/lib/module/utils/headerOffsetStyle.js +31 -0
- package/lib/module/utils/headerOffsetStyle.js.map +1 -0
- package/lib/module/utils/index.js +3 -0
- package/lib/module/utils/index.js.map +1 -1
- package/lib/module/utils/refreshControl.js +93 -0
- package/lib/module/utils/refreshControl.js.map +1 -0
- package/lib/module/utils/values.js +36 -0
- package/lib/module/utils/values.js.map +1 -1
- package/lib/typescript/src/components/Bridge.d.ts +19 -0
- package/lib/typescript/src/components/Bridge.d.ts.map +1 -0
- package/lib/typescript/src/components/FlatList.d.ts +7 -15
- package/lib/typescript/src/components/FlatList.d.ts.map +1 -1
- package/lib/typescript/src/components/Header.d.ts +73 -12
- package/lib/typescript/src/components/Header.d.ts.map +1 -1
- package/lib/typescript/src/components/HeaderDynamic.d.ts +11 -0
- package/lib/typescript/src/components/HeaderDynamic.d.ts.map +1 -0
- package/lib/typescript/src/components/HeaderMotion.d.ts +37 -18
- package/lib/typescript/src/components/HeaderMotion.d.ts.map +1 -1
- package/lib/typescript/src/components/HeaderPanBoundary.d.ts +11 -0
- package/lib/typescript/src/components/HeaderPanBoundary.d.ts.map +1 -0
- package/lib/typescript/src/components/NavigationBridge.d.ts +19 -0
- package/lib/typescript/src/components/NavigationBridge.d.ts.map +1 -0
- package/lib/typescript/src/components/ScrollManager.d.ts +18 -25
- package/lib/typescript/src/components/ScrollManager.d.ts.map +1 -1
- package/lib/typescript/src/components/ScrollView.d.ts +7 -14
- package/lib/typescript/src/components/ScrollView.d.ts.map +1 -1
- package/lib/typescript/src/components/createHeaderMotionScrollable.d.ts +86 -0
- package/lib/typescript/src/components/createHeaderMotionScrollable.d.ts.map +1 -0
- package/lib/typescript/src/components/index.d.ts +3 -1
- package/lib/typescript/src/components/index.d.ts.map +1 -1
- package/lib/typescript/src/context.d.ts +3 -13
- package/lib/typescript/src/context.d.ts.map +1 -1
- package/lib/typescript/src/hooks/index.d.ts +1 -0
- package/lib/typescript/src/hooks/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useActiveScrollId.d.ts +7 -6
- package/lib/typescript/src/hooks/useActiveScrollId.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useHeaderMotionBridge.d.ts +10 -0
- package/lib/typescript/src/hooks/useHeaderMotionBridge.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useMotionProgress.d.ts +8 -25
- package/lib/typescript/src/hooks/useMotionProgress.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useMotionProgress.test.d.ts +2 -0
- package/lib/typescript/src/hooks/useMotionProgress.test.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useScrollManager.d.ts +63 -31
- package/lib/typescript/src/hooks/useScrollManager.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +56 -26
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +63 -15
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/utils/defaults.d.ts +3 -2
- package/lib/typescript/src/utils/defaults.d.ts.map +1 -1
- package/lib/typescript/src/utils/header.d.ts +10 -0
- package/lib/typescript/src/utils/header.d.ts.map +1 -0
- package/lib/typescript/src/utils/headerOffsetStyle.d.ts +19 -0
- package/lib/typescript/src/utils/headerOffsetStyle.d.ts.map +1 -0
- package/lib/typescript/src/utils/index.d.ts +3 -0
- package/lib/typescript/src/utils/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/refreshControl.d.ts +150 -0
- package/lib/typescript/src/utils/refreshControl.d.ts.map +1 -0
- package/lib/typescript/src/utils/values.d.ts +4 -1
- package/lib/typescript/src/utils/values.d.ts.map +1 -1
- package/package.json +13 -5
- package/src/components/Bridge.tsx +29 -0
- package/src/components/FlatList.tsx +18 -84
- package/src/components/Header.tsx +159 -23
- package/src/components/HeaderDynamic.tsx +45 -0
- package/src/components/HeaderMotion.tsx +114 -41
- package/src/components/HeaderPanBoundary.tsx +92 -0
- package/src/components/NavigationBridge.tsx +30 -0
- package/src/components/ScrollManager.tsx +38 -43
- package/src/components/ScrollView.tsx +16 -68
- package/src/components/createHeaderMotionScrollable.tsx +438 -0
- package/src/components/index.ts +3 -1
- package/src/context.ts +12 -18
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useActiveScrollId.ts +7 -6
- package/src/hooks/useHeaderMotionBridge.ts +15 -0
- package/src/hooks/useMotionProgress.test.ts +67 -0
- package/src/hooks/useMotionProgress.ts +12 -37
- package/src/hooks/useScrollManager.ts +310 -129
- package/src/index.ts +82 -36
- package/src/types.ts +85 -25
- package/src/utils/defaults.ts +7 -1
- package/src/utils/header.tsx +52 -0
- package/src/utils/headerOffsetStyle.ts +40 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/refreshControl.tsx +118 -0
- package/src/utils/values.ts +57 -1
- package/lib/module/components/HeaderBase.js +0 -59
- package/lib/module/components/HeaderBase.js.map +0 -1
- package/lib/module/hooks/refreshControl.js +0 -31
- package/lib/module/hooks/refreshControl.js.map +0 -1
- package/lib/typescript/src/components/HeaderBase.d.ts +0 -34
- package/lib/typescript/src/components/HeaderBase.d.ts.map +0 -1
- package/lib/typescript/src/hooks/refreshControl.d.ts +0 -13
- package/lib/typescript/src/hooks/refreshControl.d.ts.map +0 -1
- package/src/components/HeaderBase.tsx +0 -51
- package/src/hooks/refreshControl.ts +0 -55
|
@@ -1,84 +1,59 @@
|
|
|
1
|
-
import { useContext, useCallback, useEffect } from 'react';
|
|
2
1
|
import {
|
|
3
|
-
|
|
2
|
+
useContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useState,
|
|
6
|
+
type ContextType,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import {
|
|
9
|
+
cancelAnimation,
|
|
4
10
|
scrollTo,
|
|
5
11
|
useAnimatedReaction,
|
|
6
12
|
useAnimatedRef,
|
|
7
13
|
useAnimatedScrollHandler,
|
|
8
|
-
|
|
14
|
+
useSharedValue,
|
|
9
15
|
type AnimatedRef,
|
|
10
16
|
type ScrollHandler,
|
|
11
17
|
} from 'react-native-reanimated';
|
|
12
|
-
import {
|
|
18
|
+
import { scheduleOnRN, scheduleOnUI } from 'react-native-worklets';
|
|
13
19
|
import { HeaderMotionContext } from '../context';
|
|
14
|
-
import type { ScrollManagerConfig,
|
|
15
|
-
import {
|
|
20
|
+
import type { ScrollManagerConfig, ScrollHandlerContext } from '../types';
|
|
21
|
+
import type { LayoutChangeEvent } from 'react-native';
|
|
16
22
|
import {
|
|
17
23
|
resolveRefreshControl,
|
|
24
|
+
DEFAULT_SCROLL_ID,
|
|
25
|
+
ensureScrollValueRegistered,
|
|
26
|
+
warnIfMissingActiveScrollId,
|
|
18
27
|
type ResolveRefreshControlOptions,
|
|
19
|
-
} from '
|
|
28
|
+
} from '../utils';
|
|
29
|
+
import type { InstanceOrElement } from 'react-native-reanimated/lib/typescript/commonTypes';
|
|
20
30
|
import {
|
|
21
31
|
useConsumerScrollHandlers,
|
|
22
32
|
useScrollHandlerComposition,
|
|
23
33
|
type ConsumerScrollEventHandlers,
|
|
24
34
|
} from './useConsumerScrollHandlers';
|
|
25
35
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
*
|
|
35
|
-
* Must be used within a HeaderMotion component.
|
|
36
|
-
*
|
|
37
|
-
* @param scrollId - Optional unique identifier for the related scrollable.
|
|
38
|
-
* Use when you have multiple scrollables (e.g., in tabs).
|
|
39
|
-
* @param options - Optional configuration object.
|
|
40
|
-
* @param options.animatedRef - Optional animated ref to use instead of creating one internally.
|
|
41
|
-
* Useful when you need access to the scroll view ref from outside.
|
|
42
|
-
* @returns Configuration object containing:
|
|
43
|
-
* - `scrollableProps`: Props to apply to scrollable component (onScroll, scrollEventThrottle, ref)
|
|
44
|
-
* - `headerMotionContext`: Header context values (originalHeaderHeight, minHeightContentContainerStyle)
|
|
45
|
-
*
|
|
46
|
-
* @throws Error if used outside of a HeaderMotion component
|
|
47
|
-
*
|
|
48
|
-
* @example
|
|
49
|
-
* ```tsx
|
|
50
|
-
* function CustomScrollComponent() {
|
|
51
|
-
* const { scrollableProps, headerMotionContext } = useScrollManager('myScroll');
|
|
52
|
-
*
|
|
53
|
-
* return (
|
|
54
|
-
* <CustomScrollView {...scrollableProps}>
|
|
55
|
-
* <View style={{ paddingTop: headerMotionContext.originalHeaderHeight }}>
|
|
56
|
-
* Content
|
|
57
|
-
* </View>
|
|
58
|
-
* </CustomScrollView>
|
|
59
|
-
* );
|
|
60
|
-
* }
|
|
61
|
-
* ```
|
|
62
|
-
*/
|
|
63
|
-
export interface UseScrollManagerOptions
|
|
64
|
-
extends Omit<ResolveRefreshControlOptions, 'progressViewOffset'>,
|
|
65
|
-
ConsumerScrollEventHandlers {
|
|
66
|
-
/**
|
|
67
|
-
* Optional animated ref to use instead of creating one internally.
|
|
68
|
-
* Useful when you need access to the scroll view ref from outside.
|
|
69
|
-
*/
|
|
70
|
-
animatedRef?: AnimatedRef<any>;
|
|
71
|
-
/**
|
|
72
|
-
* Optional refresh progress offset override.
|
|
73
|
-
* When provided, it takes precedence over the automatic offset based on header height.
|
|
74
|
-
*/
|
|
75
|
-
progressViewOffset?: ResolveRefreshControlOptions['progressViewOffset'];
|
|
36
|
+
const SCROLL_TOLERANCE = 0.5;
|
|
37
|
+
|
|
38
|
+
type ScrollManagerContextValue = NonNullable<
|
|
39
|
+
ContextType<typeof HeaderMotionContext>
|
|
40
|
+
>;
|
|
41
|
+
|
|
42
|
+
interface MinHeightOptions {
|
|
43
|
+
enabled: boolean;
|
|
76
44
|
}
|
|
77
45
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
46
|
+
interface SynchronizationOptions<TRef extends InstanceOrElement> {
|
|
47
|
+
animatedRef: AnimatedRef<TRef>;
|
|
48
|
+
id: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface ScrollHandlersOptions {
|
|
52
|
+
consumerHandlers: ConsumerScrollEventHandlers;
|
|
53
|
+
id: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function useScrollManagerContext(): ScrollManagerContextValue {
|
|
82
57
|
const ctxValue = useContext(HeaderMotionContext);
|
|
83
58
|
if (!ctxValue) {
|
|
84
59
|
throw new Error(
|
|
@@ -86,31 +61,89 @@ export function useScrollManager(
|
|
|
86
61
|
);
|
|
87
62
|
}
|
|
88
63
|
|
|
64
|
+
return ctxValue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function useScrollManagerContentMinHeight({ enabled }: MinHeightOptions) {
|
|
68
|
+
const { progressThreshold } = useScrollManagerContext();
|
|
69
|
+
const preservedScrollContainerHeight = useSharedValue(0);
|
|
70
|
+
const [contentContainerMinHeight, setContentContainerMinHeight] = useState<
|
|
71
|
+
number | undefined
|
|
72
|
+
>(undefined);
|
|
73
|
+
|
|
74
|
+
const handleLayout = useCallback(
|
|
75
|
+
(e: LayoutChangeEvent) => {
|
|
76
|
+
if (!enabled) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const nextHeight = e.nativeEvent.layout.height;
|
|
81
|
+
scheduleOnUI((height: number) => {
|
|
82
|
+
'worklet';
|
|
83
|
+
preservedScrollContainerHeight.set(height);
|
|
84
|
+
const nextMinHeight = height + progressThreshold.get();
|
|
85
|
+
scheduleOnRN(setContentContainerMinHeight, nextMinHeight);
|
|
86
|
+
}, nextHeight);
|
|
87
|
+
},
|
|
88
|
+
[enabled, preservedScrollContainerHeight, progressThreshold]
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
useAnimatedReaction(
|
|
92
|
+
() => progressThreshold.get(),
|
|
93
|
+
(threshold, previousThreshold) => {
|
|
94
|
+
if (
|
|
95
|
+
!enabled ||
|
|
96
|
+
previousThreshold === null ||
|
|
97
|
+
previousThreshold === threshold
|
|
98
|
+
) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const currentHeight = preservedScrollContainerHeight.get();
|
|
103
|
+
if (currentHeight <= 0) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const nextMinHeight = currentHeight + threshold;
|
|
108
|
+
scheduleOnRN(setContentContainerMinHeight, nextMinHeight);
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
contentContainerMinHeight,
|
|
114
|
+
handleLayout: enabled ? handleLayout : undefined,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function useScrollManagerSynchronization<TRef extends InstanceOrElement>({
|
|
119
|
+
animatedRef,
|
|
120
|
+
id,
|
|
121
|
+
}: SynchronizationOptions<TRef>) {
|
|
89
122
|
const {
|
|
90
|
-
scrollValues,
|
|
91
|
-
progress,
|
|
92
123
|
activeScrollId,
|
|
124
|
+
progress,
|
|
93
125
|
progressThreshold,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
126
|
+
scrollToRef,
|
|
127
|
+
scrollValues,
|
|
128
|
+
} = useScrollManagerContext();
|
|
97
129
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const progressViewOffset =
|
|
104
|
-
options?.progressViewOffset ?? originalHeaderHeight;
|
|
130
|
+
useAnimatedReaction(
|
|
131
|
+
() => activeScrollId?.get(),
|
|
132
|
+
(activeId) => {
|
|
133
|
+
const currentValues = ensureScrollValueRegistered(scrollValues, id);
|
|
134
|
+
warnIfMissingActiveScrollId(currentValues, id, activeId);
|
|
105
135
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
136
|
+
if (!activeId || activeId === id) {
|
|
137
|
+
// TODO: Could we just be passing current scrollRef instead of the entire function?
|
|
138
|
+
scrollToRef.current = (y, scrollOptions = {}) => {
|
|
139
|
+
'worklet';
|
|
140
|
+
const { isValueDelta = true, animated = false } = scrollOptions;
|
|
141
|
+
const newY = isValueDelta ? scrollValues.get()[id]!.current - y : y;
|
|
142
|
+
scrollTo(animatedRef, 0, newY, animated);
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
);
|
|
114
147
|
|
|
115
148
|
useEffect(() => {
|
|
116
149
|
return () => {
|
|
@@ -122,13 +155,11 @@ export function useScrollManager(
|
|
|
122
155
|
});
|
|
123
156
|
}, id);
|
|
124
157
|
};
|
|
125
|
-
}, [
|
|
158
|
+
}, [id, scrollValues]);
|
|
126
159
|
|
|
127
160
|
useAnimatedReaction(
|
|
128
161
|
() => progress.value,
|
|
129
162
|
(newProgress, oldProgress) => {
|
|
130
|
-
// FUTURE: If really needed for, can use other scroll handlers to only do this either on scroll end or between scroll end and momentum end in onScroll (keep context in shared value)
|
|
131
|
-
// Only sync inactive scroll views when we have multiple tabs being tracked
|
|
132
163
|
const currentActiveScrollId = activeScrollId?.get();
|
|
133
164
|
if (
|
|
134
165
|
!currentActiveScrollId ||
|
|
@@ -138,25 +169,20 @@ export function useScrollManager(
|
|
|
138
169
|
return;
|
|
139
170
|
}
|
|
140
171
|
|
|
141
|
-
|
|
142
|
-
scrollValues.modify((value) => {
|
|
143
|
-
(value as ScrollValues)[id] = getInitialScrollValue();
|
|
144
|
-
return value;
|
|
145
|
-
});
|
|
146
|
-
}
|
|
172
|
+
ensureScrollValueRegistered(scrollValues, id);
|
|
147
173
|
|
|
148
174
|
let newCur = -1;
|
|
175
|
+
const threshold = progressThreshold.get();
|
|
149
176
|
|
|
150
177
|
scrollValues.modify((value) => {
|
|
151
|
-
|
|
178
|
+
const scrollValue = value[id];
|
|
152
179
|
if (!scrollValue) {
|
|
153
|
-
|
|
154
|
-
scrollValue = value[id]!;
|
|
180
|
+
return value;
|
|
155
181
|
}
|
|
156
182
|
|
|
157
183
|
const progressDiff = oldProgress - newProgress;
|
|
158
|
-
newCur = scrollValue.current - progressDiff *
|
|
159
|
-
const newMin = newCur - newProgress *
|
|
184
|
+
newCur = scrollValue.current - progressDiff * threshold;
|
|
185
|
+
const newMin = newCur - newProgress * threshold;
|
|
160
186
|
scrollValue.current = newCur;
|
|
161
187
|
scrollValue.min = newMin;
|
|
162
188
|
|
|
@@ -168,61 +194,219 @@ export function useScrollManager(
|
|
|
168
194
|
}
|
|
169
195
|
}
|
|
170
196
|
);
|
|
197
|
+
}
|
|
171
198
|
|
|
172
|
-
|
|
173
|
-
|
|
199
|
+
function useScrollManagerHandlers({
|
|
200
|
+
consumerHandlers,
|
|
201
|
+
id,
|
|
202
|
+
}: ScrollHandlersOptions) {
|
|
203
|
+
const {
|
|
204
|
+
activeScrollId,
|
|
205
|
+
headerPanMomentumOffset,
|
|
206
|
+
progressThreshold,
|
|
207
|
+
scrollValues,
|
|
208
|
+
} = useScrollManagerContext();
|
|
209
|
+
const { onScroll, onBeginDrag, onEndDrag, onMomentumBegin, onMomentumEnd } =
|
|
210
|
+
useConsumerScrollHandlers(consumerHandlers);
|
|
211
|
+
|
|
212
|
+
const handleScroll = useCallback<ScrollHandler<ScrollHandlerContext>>(
|
|
213
|
+
(e, ctx) => {
|
|
174
214
|
'worklet';
|
|
175
215
|
onScroll?.(e);
|
|
176
216
|
|
|
217
|
+
const newCurrent = e.contentOffset.y;
|
|
218
|
+
|
|
219
|
+
if (
|
|
220
|
+
ctx.lastOffset !== undefined &&
|
|
221
|
+
Math.abs(ctx.lastOffset - newCurrent) < SCROLL_TOLERANCE
|
|
222
|
+
) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
ctx.lastOffset = newCurrent;
|
|
226
|
+
|
|
227
|
+
const threshold = progressThreshold.get();
|
|
228
|
+
const values = scrollValues.get();
|
|
229
|
+
const scrollValue = values[id];
|
|
230
|
+
|
|
231
|
+
if (!scrollValue) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const activeScrollIdValue = activeScrollId?.get();
|
|
236
|
+
if (activeScrollIdValue && activeScrollIdValue !== id) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const oldCurrent = scrollValue.current;
|
|
241
|
+
const oldMin = scrollValue.min;
|
|
242
|
+
const isCollapsed = oldCurrent >= oldMin + threshold - 0.001;
|
|
243
|
+
|
|
244
|
+
if (isCollapsed && newCurrent >= threshold) {
|
|
245
|
+
scrollValue.current = newCurrent;
|
|
246
|
+
scrollValue.min = newCurrent - threshold;
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
177
250
|
scrollValues.modify((value) => {
|
|
178
251
|
if (!value[id]) {
|
|
179
252
|
return value;
|
|
180
253
|
}
|
|
181
254
|
|
|
182
|
-
const activeScrollIdValue = activeScrollId?.get();
|
|
183
|
-
if (activeScrollIdValue && activeScrollIdValue !== id) {
|
|
184
|
-
return value;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const oldCurrent = value[id].current;
|
|
188
|
-
const oldMin = value[id].min;
|
|
189
|
-
const isCollapsed = oldCurrent >= oldMin + progressThreshold - 0.001;
|
|
190
|
-
|
|
191
|
-
const newCurrent = e.contentOffset.y;
|
|
192
255
|
value[id].current = newCurrent;
|
|
193
256
|
|
|
194
257
|
if (isCollapsed) {
|
|
195
|
-
value[id].min = Math.max(0, newCurrent -
|
|
258
|
+
value[id].min = Math.max(0, newCurrent - threshold);
|
|
196
259
|
}
|
|
197
260
|
|
|
198
261
|
return value;
|
|
199
262
|
});
|
|
200
263
|
},
|
|
201
|
-
[
|
|
264
|
+
[activeScrollId, id, onScroll, progressThreshold, scrollValues]
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const handleBeginDrag = useCallback<ScrollHandler<ScrollHandlerContext>>(
|
|
268
|
+
(e) => {
|
|
269
|
+
'worklet';
|
|
270
|
+
onBeginDrag?.(e);
|
|
271
|
+
|
|
272
|
+
if (headerPanMomentumOffset.get() === null) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
cancelAnimation(headerPanMomentumOffset);
|
|
277
|
+
headerPanMomentumOffset.set(null);
|
|
278
|
+
},
|
|
279
|
+
[headerPanMomentumOffset, onBeginDrag]
|
|
202
280
|
);
|
|
203
281
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
282
|
+
return useAnimatedScrollHandler({
|
|
283
|
+
onBeginDrag: handleBeginDrag,
|
|
284
|
+
onScroll: handleScroll,
|
|
207
285
|
onEndDrag,
|
|
208
286
|
onMomentumBegin,
|
|
209
287
|
onMomentumEnd,
|
|
210
288
|
});
|
|
289
|
+
}
|
|
290
|
+
export interface UseScrollManagerOptions<TRef extends InstanceOrElement = any>
|
|
291
|
+
extends Omit<ResolveRefreshControlOptions, 'progressViewOffset'>,
|
|
292
|
+
ConsumerScrollEventHandlers {
|
|
293
|
+
/**
|
|
294
|
+
* Animated ref for the managed scrollable.
|
|
295
|
+
*
|
|
296
|
+
* Provide this when the caller also needs imperative access to the same
|
|
297
|
+
* scrollable instance. Otherwise the hook creates one internally.
|
|
298
|
+
*/
|
|
299
|
+
animatedRef?: AnimatedRef<TRef>;
|
|
300
|
+
/**
|
|
301
|
+
* Overrides the refresh indicator offset.
|
|
302
|
+
*
|
|
303
|
+
* By default, HeaderMotion derives this from the measured header height so
|
|
304
|
+
* pull-to-refresh starts below the header. Override it only when you need a
|
|
305
|
+
* custom refresh placement.
|
|
306
|
+
*/
|
|
307
|
+
progressViewOffset?: ResolveRefreshControlOptions['progressViewOffset'];
|
|
308
|
+
/**
|
|
309
|
+
* Ensures short content can still scroll far enough to fully collapse the
|
|
310
|
+
* header.
|
|
311
|
+
*
|
|
312
|
+
* **Experimental: this relies on extra layout measurement and may still be
|
|
313
|
+
* refined.**
|
|
314
|
+
*
|
|
315
|
+
* Enable this when your content is sometimes shorter than the viewport and
|
|
316
|
+
* you still want the header to reach the collapsed state.
|
|
317
|
+
*/
|
|
318
|
+
ensureScrollableContentMinHeight?: boolean;
|
|
319
|
+
}
|
|
211
320
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
321
|
+
/**
|
|
322
|
+
* Wires a custom scrollable into HeaderMotion.
|
|
323
|
+
*
|
|
324
|
+
* Most code should not use this hook directly.
|
|
325
|
+
*
|
|
326
|
+
* **Prefer `createHeaderMotionScrollable()` whenever possible.** It gives
|
|
327
|
+
* you the same integration in a reusable component wrapper with less manual
|
|
328
|
+
* wiring. Reach for `useScrollManager()` only in more complex cases where the
|
|
329
|
+
* factory API is not enough, for example when a third-party scrollable needs
|
|
330
|
+
* highly custom composition.
|
|
331
|
+
*
|
|
332
|
+
* It returns two things:
|
|
333
|
+
* - `scrollableProps`: the event handlers / ref / refresh-control props that
|
|
334
|
+
* should go on the scrollable itself
|
|
335
|
+
* - `headerMotionContext`: layout values you can use to offset the content
|
|
336
|
+
* below the measured header
|
|
337
|
+
*
|
|
338
|
+
* In multi-scroll setups, pass a unique `scrollId` for each scrollable.
|
|
339
|
+
* In single-scroll setups, you usually do not need one.
|
|
340
|
+
*
|
|
341
|
+
* If you need the same fallback behavior but prefer render-prop composition
|
|
342
|
+
* over a hook, use `HeaderMotion.ScrollManager`.
|
|
343
|
+
*
|
|
344
|
+
* @param scrollId Optional unique identifier for the managed scrollable.
|
|
345
|
+
* @param options Optional configuration for refs, refresh handling, user
|
|
346
|
+
* scroll callbacks, and short-content fallback behavior.
|
|
347
|
+
* @returns Object containing:
|
|
348
|
+
* - `scrollableProps`: props to spread onto the scrollable (`ref`, managed
|
|
349
|
+
* `onScroll`, optional `onLayout`, and resolved `refreshControl`)
|
|
350
|
+
* - `headerMotionContext`: layout values for offsetting the content container
|
|
351
|
+
* (`originalHeaderHeight` and optional `contentContainerMinHeight`)
|
|
352
|
+
*
|
|
353
|
+
* @example
|
|
354
|
+
* ```tsx
|
|
355
|
+
* function CustomScrollComponent() {
|
|
356
|
+
* const { scrollableProps, headerMotionContext } = useScrollManager('myScroll');
|
|
357
|
+
*
|
|
358
|
+
* return (
|
|
359
|
+
* <CustomScrollView {...scrollableProps}>
|
|
360
|
+
* <View
|
|
361
|
+
* style={{
|
|
362
|
+
* paddingTop: headerMotionContext.originalHeaderHeight,
|
|
363
|
+
* minHeight: headerMotionContext.contentContainerMinHeight,
|
|
364
|
+
* }}
|
|
365
|
+
* >
|
|
366
|
+
* Content
|
|
367
|
+
* </View>
|
|
368
|
+
* </CustomScrollView>
|
|
369
|
+
* );
|
|
370
|
+
* }
|
|
371
|
+
* ```
|
|
372
|
+
*/
|
|
373
|
+
export function useScrollManager<TRef extends InstanceOrElement = any>(
|
|
374
|
+
scrollId?: string,
|
|
375
|
+
options?: UseScrollManagerOptions<TRef>
|
|
376
|
+
): ScrollManagerConfig<TRef> {
|
|
377
|
+
const { originalHeaderHeight } = useScrollManagerContext();
|
|
378
|
+
const id = scrollId ?? DEFAULT_SCROLL_ID;
|
|
216
379
|
|
|
217
|
-
|
|
380
|
+
const ensureScrollableContentMinHeight =
|
|
381
|
+
options?.ensureScrollableContentMinHeight ?? false;
|
|
382
|
+
const refreshControl = options?.refreshControl;
|
|
383
|
+
const refreshing = options?.refreshing;
|
|
384
|
+
const onRefresh = options?.onRefresh;
|
|
385
|
+
const progressViewOffset =
|
|
386
|
+
options?.progressViewOffset ?? originalHeaderHeight;
|
|
218
387
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
388
|
+
const localRef = useAnimatedRef<TRef>();
|
|
389
|
+
const animatedRef = options?.animatedRef ?? localRef;
|
|
222
390
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
391
|
+
const { contentContainerMinHeight, handleLayout } =
|
|
392
|
+
useScrollManagerContentMinHeight({
|
|
393
|
+
enabled: ensureScrollableContentMinHeight,
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
useScrollManagerSynchronization({
|
|
397
|
+
id,
|
|
398
|
+
animatedRef,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
const animatedOnScroll = useScrollManagerHandlers({
|
|
402
|
+
id,
|
|
403
|
+
consumerHandlers: {
|
|
404
|
+
onScroll: options?.onScroll,
|
|
405
|
+
onScrollBeginDrag: options?.onScrollBeginDrag,
|
|
406
|
+
onScrollEndDrag: options?.onScrollEndDrag,
|
|
407
|
+
onMomentumScrollBegin: options?.onMomentumScrollBegin,
|
|
408
|
+
onMomentumScrollEnd: options?.onMomentumScrollEnd,
|
|
409
|
+
},
|
|
226
410
|
});
|
|
227
411
|
|
|
228
412
|
const resolvedRefreshControl = resolveRefreshControl({
|
|
@@ -233,17 +417,14 @@ export function useScrollManager(
|
|
|
233
417
|
});
|
|
234
418
|
|
|
235
419
|
const scrollableProps = {
|
|
236
|
-
onScroll: useScrollHandlerComposition(
|
|
237
|
-
|
|
238
|
-
options?.onScroll
|
|
239
|
-
),
|
|
240
|
-
scrollEventThrottle: 16,
|
|
420
|
+
onScroll: useScrollHandlerComposition(animatedOnScroll, options?.onScroll),
|
|
421
|
+
onLayout: handleLayout,
|
|
241
422
|
ref: animatedRef,
|
|
242
423
|
refreshControl: resolvedRefreshControl,
|
|
243
424
|
};
|
|
244
425
|
const headerMotionContext = {
|
|
245
426
|
originalHeaderHeight,
|
|
246
|
-
|
|
427
|
+
contentContainerMinHeight,
|
|
247
428
|
};
|
|
248
429
|
|
|
249
430
|
return { scrollableProps, headerMotionContext };
|