react-native-header-motion 0.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.
- package/LICENSE +20 -0
- package/README.md +479 -0
- package/lib/module/components/FlatList.js +64 -0
- package/lib/module/components/FlatList.js.map +1 -0
- package/lib/module/components/Header.js +19 -0
- package/lib/module/components/Header.js.map +1 -0
- package/lib/module/components/HeaderBase.js +59 -0
- package/lib/module/components/HeaderBase.js.map +1 -0
- package/lib/module/components/HeaderMotion.js +84 -0
- package/lib/module/components/HeaderMotion.js.map +1 -0
- package/lib/module/components/ScrollManager.js +39 -0
- package/lib/module/components/ScrollManager.js.map +1 -0
- package/lib/module/components/ScrollView.js +47 -0
- package/lib/module/components/ScrollView.js.map +1 -0
- package/lib/module/components/index.js +9 -0
- package/lib/module/components/index.js.map +1 -0
- package/lib/module/context.js +5 -0
- package/lib/module/context.js.map +1 -0
- package/lib/module/hooks/index.js +6 -0
- package/lib/module/hooks/index.js.map +1 -0
- package/lib/module/hooks/useActiveScrollId.js +47 -0
- package/lib/module/hooks/useActiveScrollId.js.map +1 -0
- package/lib/module/hooks/useMotionProgress.js +58 -0
- package/lib/module/hooks/useMotionProgress.js.map +1 -0
- package/lib/module/hooks/useScrollManager.js +150 -0
- package/lib/module/hooks/useScrollManager.js.map +1 -0
- package/lib/module/index.js +42 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utils/defaults.js +10 -0
- package/lib/module/utils/defaults.js.map +1 -0
- package/lib/module/utils/index.js +5 -0
- package/lib/module/utils/index.js.map +1 -0
- package/lib/module/utils/values.js +11 -0
- package/lib/module/utils/values.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/components/FlatList.d.ts +30 -0
- package/lib/typescript/src/components/FlatList.d.ts.map +1 -0
- package/lib/typescript/src/components/Header.d.ts +19 -0
- package/lib/typescript/src/components/Header.d.ts.map +1 -0
- package/lib/typescript/src/components/HeaderBase.d.ts +34 -0
- package/lib/typescript/src/components/HeaderBase.d.ts.map +1 -0
- package/lib/typescript/src/components/HeaderMotion.d.ts +52 -0
- package/lib/typescript/src/components/HeaderMotion.d.ts.map +1 -0
- package/lib/typescript/src/components/ScrollManager.d.ts +40 -0
- package/lib/typescript/src/components/ScrollManager.d.ts.map +1 -0
- package/lib/typescript/src/components/ScrollView.d.ts +24 -0
- package/lib/typescript/src/components/ScrollView.d.ts.map +1 -0
- package/lib/typescript/src/components/index.d.ts +7 -0
- package/lib/typescript/src/components/index.d.ts.map +1 -0
- package/lib/typescript/src/context.d.ts +14 -0
- package/lib/typescript/src/context.d.ts.map +1 -0
- package/lib/typescript/src/hooks/index.d.ts +4 -0
- package/lib/typescript/src/hooks/index.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useActiveScrollId.d.ts +32 -0
- package/lib/typescript/src/hooks/useActiveScrollId.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useMotionProgress.d.ts +38 -0
- package/lib/typescript/src/hooks/useMotionProgress.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useScrollManager.d.ts +37 -0
- package/lib/typescript/src/hooks/useScrollManager.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +51 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +43 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/utils/defaults.d.ts +6 -0
- package/lib/typescript/src/utils/defaults.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 -0
- package/lib/typescript/src/utils/values.d.ts +3 -0
- package/lib/typescript/src/utils/values.d.ts.map +1 -0
- package/package.json +164 -0
- package/src/components/FlatList.tsx +72 -0
- package/src/components/Header.tsx +30 -0
- package/src/components/HeaderBase.tsx +51 -0
- package/src/components/HeaderMotion.tsx +183 -0
- package/src/components/ScrollManager.tsx +58 -0
- package/src/components/ScrollView.tsx +58 -0
- package/src/components/index.ts +6 -0
- package/src/context.ts +20 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useActiveScrollId.ts +59 -0
- package/src/hooks/useMotionProgress.ts +56 -0
- package/src/hooks/useScrollManager.ts +186 -0
- package/src/index.ts +76 -0
- package/src/types.ts +62 -0
- package/src/utils/defaults.ts +16 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/values.ts +6 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Oskar Pawica
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
# React Native Header Motion
|
|
2
|
+
|
|
3
|
+
High-level APIs for **orchestrating header motion** driven by scroll — built on top of [**React Native Reanimated**](https://docs.swmansion.com/react-native-reanimated/).
|
|
4
|
+
|
|
5
|
+
This library is **100% a wrapper around Reanimated**. All the credit for the underlying animation engine, worklets, and primitives goes to **Reanimated** (and `react-native-worklets`). This package focuses on a specific use case: **header motion + scroll orchestration** (including multi-scroll/tab scenarios).
|
|
6
|
+
|
|
7
|
+
## What this is (and isn’t)
|
|
8
|
+
|
|
9
|
+
**✅ This is**
|
|
10
|
+
|
|
11
|
+
- A small set of components + hooks that expose a single `progress` shared value and a few measurement helpers.
|
|
12
|
+
- A scroll orchestration layer that can keep multiple scrollables in sync (e.g. tabs + pager).
|
|
13
|
+
|
|
14
|
+
**❌ This is NOT**
|
|
15
|
+
|
|
16
|
+
- An out-of-the-box “collapsible header” component with a baked-in look.
|
|
17
|
+
|
|
18
|
+
You build any header motion you want by animating based on `progress`.
|
|
19
|
+
|
|
20
|
+
## Requirements (peer dependencies)
|
|
21
|
+
|
|
22
|
+
You must have these installed in your app:
|
|
23
|
+
|
|
24
|
+
- `react-native-reanimated` **>= 4.0.0**
|
|
25
|
+
- `react-native-worklets` **>= 0.4.0**
|
|
26
|
+
|
|
27
|
+
This package declares them as peer dependencies, so your app owns those versions. Remember to install a version of Worklets compatible with your version of Reanimated.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm i react-native-header-motion
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
or
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
yarn add react-native-header-motion
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Reanimated setup
|
|
42
|
+
|
|
43
|
+
Follow the official Reanimated [installation instructions](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/getting-started/#installation) for your environment (Expo / bare RN).
|
|
44
|
+
|
|
45
|
+
## Mental model
|
|
46
|
+
|
|
47
|
+
There are three key concepts:
|
|
48
|
+
|
|
49
|
+
### 1) `progress` (SharedValue)
|
|
50
|
+
|
|
51
|
+
`progress` is a Reanimated `SharedValue<number>` that represents the normalized progress of your header animation.
|
|
52
|
+
|
|
53
|
+
- `0` → animation start (initial state)
|
|
54
|
+
- `1` → animation end (final state)
|
|
55
|
+
|
|
56
|
+
### 2) `progressThreshold`
|
|
57
|
+
|
|
58
|
+
`progressThreshold` is the distance needed for `progress` to move from `0 → 1`.
|
|
59
|
+
|
|
60
|
+
You can provide it as:
|
|
61
|
+
|
|
62
|
+
- a number, or
|
|
63
|
+
- a function `(measuredDynamic) => threshold`
|
|
64
|
+
|
|
65
|
+
If you provide a function, it uses the value measured by `measureDynamic`.
|
|
66
|
+
|
|
67
|
+
### 3) Measurement functions
|
|
68
|
+
|
|
69
|
+
The library gives you two measurement callbacks that you pass to your header layout:
|
|
70
|
+
|
|
71
|
+
- `measureTotalHeight` – attach to the _outer_ header container to measure the total header height. Scrollables use this to add `paddingTop` so content starts below the header.
|
|
72
|
+
- `measureDynamic` – attach to the part of the header that determines the threshold (often the animated/dynamic portion).
|
|
73
|
+
|
|
74
|
+
## Why `HeaderMotion.Header` exists
|
|
75
|
+
|
|
76
|
+
When you pass a `header` component to React Navigation / Expo Router, that header is rendered by the navigator in a different part of the React tree.
|
|
77
|
+
|
|
78
|
+
Because of that, the navigation header **cannot read the `HeaderMotion` context**, so calling `useMotionProgress()` inside that header would throw.
|
|
79
|
+
|
|
80
|
+
`HeaderMotion.Header` solves this by acting as a **bridge**: it runs inside the provider, reads context, and passes the values to your navigation header via a render function.
|
|
81
|
+
|
|
82
|
+
## Why `HeaderBase` / `AnimatedHeaderBase` uses absolute positioning
|
|
83
|
+
|
|
84
|
+
Navigation headers are special:
|
|
85
|
+
|
|
86
|
+
- Even with `headerTransparent: true`, the navigator can still reserve layout space for the header container.
|
|
87
|
+
- If you animate with translations without absolute positioning, you can end up with:
|
|
88
|
+
- content below becoming unclickable (an invisible parent header still sits on top), or
|
|
89
|
+
- content hidden under the header container.
|
|
90
|
+
|
|
91
|
+
`HeaderBase` and `AnimatedHeaderBase` are **absolutely positioned** to avoid those layout traps, which is especially important when you use transforms/translations.
|
|
92
|
+
|
|
93
|
+
## When to use components vs hooks
|
|
94
|
+
|
|
95
|
+
You can use either style; pick based on your integration needs:
|
|
96
|
+
|
|
97
|
+
- Prefer **components** when you want a “batteries included” wiring:
|
|
98
|
+
|
|
99
|
+
- `HeaderMotion.ScrollView` / `HeaderMotion.FlatList` for common scrollables
|
|
100
|
+
- `HeaderMotion.ScrollManager` for custom scrollables via render-props
|
|
101
|
+
|
|
102
|
+
- Prefer **hooks** when you want to build your own wrappers:
|
|
103
|
+
- `useScrollManager()` (same engine as `HeaderMotion.ScrollManager`, but hook-based)
|
|
104
|
+
- `useMotionProgress()` when your header is inside the provider tree
|
|
105
|
+
|
|
106
|
+
Also:
|
|
107
|
+
|
|
108
|
+
- Use `HeaderMotion.Header` when your header is rendered by navigation.
|
|
109
|
+
- Use `useMotionProgress` when your header is rendered inside the same tree as `HeaderMotion`.
|
|
110
|
+
|
|
111
|
+
## Examples
|
|
112
|
+
|
|
113
|
+
### Example app
|
|
114
|
+
|
|
115
|
+
Examples live in the example app: `example/`. They demonstrate a few cases, from simple animations, to scroll orchestration and persisted header animation state between different tabs (e.g. with `react-native-pager-view`).
|
|
116
|
+
|
|
117
|
+
Those examples use Expo Router as the navigation library, but it should be fairly simple to do the same with plain React Navigation.
|
|
118
|
+
|
|
119
|
+
### Expo Router
|
|
120
|
+
|
|
121
|
+
This is the core pattern used in the example app (`example/src/app/simple.tsx`).
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
import HeaderMotion, {
|
|
125
|
+
AnimatedHeaderBase,
|
|
126
|
+
type WithCollapsibleHeaderProps,
|
|
127
|
+
} from 'react-native-header-motion';
|
|
128
|
+
import { Stack } from 'expo-router';
|
|
129
|
+
import Animated, {
|
|
130
|
+
Extrapolation,
|
|
131
|
+
interpolate,
|
|
132
|
+
useAnimatedStyle,
|
|
133
|
+
} from 'react-native-reanimated';
|
|
134
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
135
|
+
import { View } from 'react-native';
|
|
136
|
+
|
|
137
|
+
export default function Screen() {
|
|
138
|
+
return (
|
|
139
|
+
<HeaderMotion>
|
|
140
|
+
<HeaderMotion.Header>
|
|
141
|
+
{(headerProps) => (
|
|
142
|
+
<Stack.Screen
|
|
143
|
+
options={{
|
|
144
|
+
header: () => <MyHeader {...headerProps} />,
|
|
145
|
+
}}
|
|
146
|
+
/>
|
|
147
|
+
)}
|
|
148
|
+
</HeaderMotion.Header>
|
|
149
|
+
|
|
150
|
+
<HeaderMotion.ScrollView>
|
|
151
|
+
{/* your scrollable content */}
|
|
152
|
+
</HeaderMotion.ScrollView>
|
|
153
|
+
</HeaderMotion>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function MyHeader({
|
|
158
|
+
progress,
|
|
159
|
+
measureTotalHeight,
|
|
160
|
+
measureDynamic,
|
|
161
|
+
progressThreshold,
|
|
162
|
+
}: WithCollapsibleHeaderProps) {
|
|
163
|
+
const insets = useSafeAreaInsets();
|
|
164
|
+
|
|
165
|
+
const containerStyle = useAnimatedStyle(() => {
|
|
166
|
+
const translateY = interpolate(
|
|
167
|
+
progress.value,
|
|
168
|
+
[0, 1],
|
|
169
|
+
[0, -progressThreshold],
|
|
170
|
+
Extrapolation.CLAMP
|
|
171
|
+
);
|
|
172
|
+
return { transform: [{ translateY }] };
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<AnimatedHeaderBase
|
|
177
|
+
onLayout={measureTotalHeight}
|
|
178
|
+
style={[{ paddingTop: insets.top }, containerStyle]}
|
|
179
|
+
>
|
|
180
|
+
<Animated.View onLayout={measureDynamic}>
|
|
181
|
+
{/* “dynamic” part of the header */}
|
|
182
|
+
</Animated.View>
|
|
183
|
+
|
|
184
|
+
<View>{/* "regular" part of the header */}</View>
|
|
185
|
+
</AnimatedHeaderBase>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### React Navigation
|
|
191
|
+
|
|
192
|
+
In React Navigation you typically configure headers via `navigation.setOptions()`.
|
|
193
|
+
|
|
194
|
+
Important: the header itself can’t call `useMotionProgress()`, so we still use `HeaderMotion.Header` as a bridge.
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
import React from 'react';
|
|
198
|
+
import HeaderMotion, {
|
|
199
|
+
AnimatedHeaderBase,
|
|
200
|
+
type WithCollapsibleHeaderProps,
|
|
201
|
+
} from 'react-native-header-motion';
|
|
202
|
+
import { useNavigation } from '@react-navigation/native';
|
|
203
|
+
import Animated, {
|
|
204
|
+
Extrapolation,
|
|
205
|
+
interpolate,
|
|
206
|
+
useAnimatedStyle,
|
|
207
|
+
} from 'react-native-reanimated';
|
|
208
|
+
import { View } from 'react-native';
|
|
209
|
+
|
|
210
|
+
export function MyScreen() {
|
|
211
|
+
return (
|
|
212
|
+
<HeaderMotion>
|
|
213
|
+
<HeaderMotion.Header>
|
|
214
|
+
{(headerProps) => (
|
|
215
|
+
<NavigationHeaderInstaller headerProps={headerProps} />
|
|
216
|
+
)}
|
|
217
|
+
</HeaderMotion.Header>
|
|
218
|
+
<HeaderMotion.ScrollView>{/* content */}</HeaderMotion.ScrollView>
|
|
219
|
+
</HeaderMotion>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function NavigationHeaderInstaller({
|
|
224
|
+
headerProps,
|
|
225
|
+
}: {
|
|
226
|
+
headerProps: WithCollapsibleHeaderProps;
|
|
227
|
+
}) {
|
|
228
|
+
const navigation = useNavigation();
|
|
229
|
+
|
|
230
|
+
React.useLayoutEffect(() => {
|
|
231
|
+
navigation.setOptions({
|
|
232
|
+
header: () => <MyHeader {...headerProps} />,
|
|
233
|
+
});
|
|
234
|
+
}, [navigation, headerProps]);
|
|
235
|
+
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function MyHeader({
|
|
240
|
+
progress,
|
|
241
|
+
measureTotalHeight,
|
|
242
|
+
measureDynamic,
|
|
243
|
+
progressThreshold,
|
|
244
|
+
}: WithCollapsibleHeaderProps) {
|
|
245
|
+
const insets = useSafeAreaInsets();
|
|
246
|
+
|
|
247
|
+
const containerStyle = useAnimatedStyle(() => {
|
|
248
|
+
const translateY = interpolate(
|
|
249
|
+
progress.value,
|
|
250
|
+
[0, 1],
|
|
251
|
+
[0, -progressThreshold],
|
|
252
|
+
Extrapolation.CLAMP
|
|
253
|
+
);
|
|
254
|
+
return { transform: [{ translateY }] };
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<AnimatedHeaderBase
|
|
259
|
+
onLayout={measureTotalHeight}
|
|
260
|
+
style={[{ paddingTop: insets.top }, containerStyle]}
|
|
261
|
+
>
|
|
262
|
+
<Animated.View onLayout={measureDynamic}>
|
|
263
|
+
{/* “dynamic” part of the header */}
|
|
264
|
+
</Animated.View>
|
|
265
|
+
|
|
266
|
+
<View>{/* "regular" part of the header */}</View>
|
|
267
|
+
</AnimatedHeaderBase>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Tabs / pager: synchronizing multiple scrollables
|
|
273
|
+
|
|
274
|
+
If you have multiple scrollables (e.g. pages in `react-native-pager-view`), you can keep a single header progress by:
|
|
275
|
+
|
|
276
|
+
1. Creating a shared “active scroll id” using `useActiveScrollId()`
|
|
277
|
+
2. Passing `activeScrollId.sv` to `<HeaderMotion activeScrollId={...} />`
|
|
278
|
+
3. Rendering each page scrollable with a unique `scrollId`
|
|
279
|
+
|
|
280
|
+
The example app shows this pattern in `example/src/app/collapsible-pager.tsx` using `HeaderMotion.ScrollManager`.
|
|
281
|
+
|
|
282
|
+
### Keeping the native header (back button/title) + custom animated header below
|
|
283
|
+
|
|
284
|
+
Sometimes you want to keep the native navigation header for back buttons + title, but still animate a custom header section below it.
|
|
285
|
+
|
|
286
|
+
In that case:
|
|
287
|
+
|
|
288
|
+
- set `headerTransparent: true`
|
|
289
|
+
- do **not** provide a custom `header` component
|
|
290
|
+
- render your animated header content _inside the screen_ under the native header
|
|
291
|
+
|
|
292
|
+
Sketch:
|
|
293
|
+
|
|
294
|
+
```tsx
|
|
295
|
+
import HeaderMotion, {
|
|
296
|
+
AnimatedHeaderBase,
|
|
297
|
+
useMotionProgress,
|
|
298
|
+
} from 'react-native-header-motion';
|
|
299
|
+
import { Stack } from 'expo-router';
|
|
300
|
+
import Animated, {
|
|
301
|
+
Extrapolation,
|
|
302
|
+
interpolate,
|
|
303
|
+
useAnimatedStyle,
|
|
304
|
+
} from 'react-native-reanimated';
|
|
305
|
+
import { View } from 'react-native';
|
|
306
|
+
|
|
307
|
+
export default function Screen() {
|
|
308
|
+
return (
|
|
309
|
+
<>
|
|
310
|
+
<Stack.Screen options={{ headerTransparent: true }} />
|
|
311
|
+
<HeaderMotion>
|
|
312
|
+
<InlineAnimatedHeader />
|
|
313
|
+
<HeaderMotion.ScrollView>
|
|
314
|
+
{/* rest of content */}
|
|
315
|
+
</HeaderMotion.ScrollView>
|
|
316
|
+
</HeaderMotion>
|
|
317
|
+
</>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function InlineAnimatedHeader() {
|
|
322
|
+
const { progress, measureTotalHeight, measureDynamic, progressThreshold } =
|
|
323
|
+
useMotionProgress();
|
|
324
|
+
|
|
325
|
+
const containerStyle = useAnimatedStyle(() => {
|
|
326
|
+
const translateY = interpolate(
|
|
327
|
+
progress.value,
|
|
328
|
+
[0, 1],
|
|
329
|
+
[0, -progressThreshold],
|
|
330
|
+
Extrapolation.CLAMP
|
|
331
|
+
);
|
|
332
|
+
return { transform: [{ translateY }] };
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
return (
|
|
336
|
+
<AnimatedHeaderBase onLayout={measureTotalHeight} style={containerStyle}>
|
|
337
|
+
<Animated.View onLayout={measureDynamic}>
|
|
338
|
+
{/* custom animated header content below the native header */}
|
|
339
|
+
</Animated.View>
|
|
340
|
+
<View>{/* sticky part */}</View>
|
|
341
|
+
</AnimatedHeaderBase>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## API
|
|
347
|
+
|
|
348
|
+
The package exports a default compound component plus hooks, types, and a couple base components.
|
|
349
|
+
|
|
350
|
+
### `HeaderMotion` (default export)
|
|
351
|
+
|
|
352
|
+
`HeaderMotion` is a compound component:
|
|
353
|
+
|
|
354
|
+
- `HeaderMotion` (provider)
|
|
355
|
+
- `HeaderMotion.Header` (bridge for navigation headers)
|
|
356
|
+
- `HeaderMotion.ScrollView` (pre-wired Animated.ScrollView)
|
|
357
|
+
- `HeaderMotion.FlatList` (pre-wired Animated.FlatList)
|
|
358
|
+
- `HeaderMotion.ScrollManager` (render-prop API for custom scrollables)
|
|
359
|
+
|
|
360
|
+
#### Props
|
|
361
|
+
|
|
362
|
+
- `progressThreshold?: number | (measuredDynamic: number) => number`
|
|
363
|
+
- Defines how many pixels correspond to `progress` going from `0` to `1`.
|
|
364
|
+
- If you pass a function, it uses the value measured from `measureDynamic`.
|
|
365
|
+
- `measureDynamic?: (e) => number`
|
|
366
|
+
- What value to read from the `onLayout` event (defaults to `height`).
|
|
367
|
+
- `measureDynamicMode?: 'mount' | 'update'`
|
|
368
|
+
- Whether `measureDynamic` updates only once or on every layout recalculation.
|
|
369
|
+
- `activeScrollId?: SharedValue<string>`
|
|
370
|
+
- Enables multi-scroll orchestration (tabs/pager).
|
|
371
|
+
- `progressExtrapolation?: ExtrapolationType`
|
|
372
|
+
- Controls how progress behaves outside the threshold range (useful for overscroll).
|
|
373
|
+
|
|
374
|
+
#### `HeaderMotion.Header`
|
|
375
|
+
|
|
376
|
+
Render-prop component that passes motion progress props to a header you render via navigation.
|
|
377
|
+
|
|
378
|
+
```tsx
|
|
379
|
+
<HeaderMotion.Header>
|
|
380
|
+
{(headerProps) => /* pass headerProps into navigation header */}
|
|
381
|
+
</HeaderMotion.Header>
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Use this instead of `useMotionProgress()` when your header is rendered by React Navigation / Expo Router.
|
|
385
|
+
|
|
386
|
+
#### `HeaderMotion.ScrollView`
|
|
387
|
+
|
|
388
|
+
Animated ScrollView wired with:
|
|
389
|
+
|
|
390
|
+
- `onScroll` handler
|
|
391
|
+
- `ref`
|
|
392
|
+
- automatic `paddingTop` based on measured header height
|
|
393
|
+
|
|
394
|
+
Supports `scrollId?: string` for multi-scroll scenarios.
|
|
395
|
+
|
|
396
|
+
#### `HeaderMotion.FlatList`
|
|
397
|
+
|
|
398
|
+
Animated FlatList wired similarly to the ScrollView.
|
|
399
|
+
|
|
400
|
+
Supports `scrollId?: string` for multi-scroll scenarios.
|
|
401
|
+
|
|
402
|
+
#### `HeaderMotion.ScrollManager`
|
|
403
|
+
|
|
404
|
+
Render-prop API for custom scrollables (pager pages, 3rd party lists, etc.).
|
|
405
|
+
|
|
406
|
+
```tsx
|
|
407
|
+
<HeaderMotion.ScrollManager scrollId="A">
|
|
408
|
+
{(
|
|
409
|
+
scrollableProps,
|
|
410
|
+
{ originalHeaderHeight, minHeightContentContainerStyle }
|
|
411
|
+
) => (
|
|
412
|
+
<Animated.ScrollView
|
|
413
|
+
{...scrollableProps}
|
|
414
|
+
contentContainerStyle={[
|
|
415
|
+
minHeightContentContainerStyle,
|
|
416
|
+
{ paddingTop: originalHeaderHeight },
|
|
417
|
+
]}
|
|
418
|
+
/>
|
|
419
|
+
)}
|
|
420
|
+
</HeaderMotion.ScrollManager>
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### Hooks
|
|
424
|
+
|
|
425
|
+
#### `useMotionProgress()`
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
|
|
429
|
+
- `progress` (`SharedValue<number>`)
|
|
430
|
+
- `progressThreshold` (`number`)
|
|
431
|
+
- `measureTotalHeight` (`onLayout` callback)
|
|
432
|
+
- `measureDynamic` (`onLayout` callback)
|
|
433
|
+
|
|
434
|
+
Only use inside the `HeaderMotion` provider tree.
|
|
435
|
+
|
|
436
|
+
#### `useScrollManager(scrollId?)`
|
|
437
|
+
|
|
438
|
+
Lower-level orchestration hook that powers the component APIs. Returns:
|
|
439
|
+
|
|
440
|
+
- `scrollableProps`: `{ onScroll, scrollEventThrottle, ref }`
|
|
441
|
+
- `headerMotionContext`:
|
|
442
|
+
- `originalHeaderHeight`
|
|
443
|
+
- `minHeightContentContainerStyle` (helps when content is shorter than the threshold)
|
|
444
|
+
|
|
445
|
+
#### `useActiveScrollId(initialId)`
|
|
446
|
+
|
|
447
|
+
Helper for multi-scroll scenarios (tabs/pager). Returns:
|
|
448
|
+
|
|
449
|
+
- `[active, setActive]`
|
|
450
|
+
- `active.state` (React state)
|
|
451
|
+
- `active.sv` (SharedValue)
|
|
452
|
+
|
|
453
|
+
### Base components
|
|
454
|
+
|
|
455
|
+
#### `HeaderBase`
|
|
456
|
+
|
|
457
|
+
Non-animated absolutely positioned header base.
|
|
458
|
+
|
|
459
|
+
#### `AnimatedHeaderBase`
|
|
460
|
+
|
|
461
|
+
Reanimated-powered, absolutely positioned header base.
|
|
462
|
+
|
|
463
|
+
### Types
|
|
464
|
+
|
|
465
|
+
- `WithCollapsibleHeaderProps` – convenience type for headers using motion progress props.
|
|
466
|
+
- `WithCollapsiblePagedHeaderProps` – like above, plus `activeTab` and `onTabChange`.
|
|
467
|
+
|
|
468
|
+
## Contributing
|
|
469
|
+
|
|
470
|
+
- Development workflow: see [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
471
|
+
- Code of conduct: see [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
|
|
472
|
+
|
|
473
|
+
## License
|
|
474
|
+
|
|
475
|
+
MIT
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { forwardRef } from 'react';
|
|
4
|
+
import { ScrollView } from 'react-native';
|
|
5
|
+
import Animated from 'react-native-reanimated';
|
|
6
|
+
import { HeaderMotionScrollManager } from "./ScrollManager.js";
|
|
7
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
8
|
+
/**
|
|
9
|
+
* Animated FlatList component that integrates with HeaderMotion.
|
|
10
|
+
* Automatically handles scroll tracking and header animation synchronization.
|
|
11
|
+
* Must be used within a HeaderMotion component.
|
|
12
|
+
*
|
|
13
|
+
* @template T - The type of items in the FlatList
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* <HeaderMotion>
|
|
18
|
+
* <HeaderMotion.FlatList
|
|
19
|
+
* data={items}
|
|
20
|
+
* renderItem={({ item }) => <Text>{item}</Text>}
|
|
21
|
+
* />
|
|
22
|
+
* </HeaderMotion>
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function HeaderMotionFlatList({
|
|
26
|
+
scrollId,
|
|
27
|
+
...props
|
|
28
|
+
}) {
|
|
29
|
+
return /*#__PURE__*/_jsx(HeaderMotionScrollManager, {
|
|
30
|
+
scrollId: scrollId,
|
|
31
|
+
children: ({
|
|
32
|
+
onScroll,
|
|
33
|
+
...scrollViewProps
|
|
34
|
+
}, {
|
|
35
|
+
originalHeaderHeight,
|
|
36
|
+
minHeightContentContainerStyle
|
|
37
|
+
}) => /*#__PURE__*/_jsx(Animated.FlatList, {
|
|
38
|
+
...scrollViewProps,
|
|
39
|
+
...props,
|
|
40
|
+
onScroll: onScroll,
|
|
41
|
+
renderScrollComponent: propsz => /*#__PURE__*/_jsx(AnimatedScrollContainer, {
|
|
42
|
+
...propsz
|
|
43
|
+
}),
|
|
44
|
+
contentContainerStyle: [minHeightContentContainerStyle, {
|
|
45
|
+
paddingTop: originalHeaderHeight
|
|
46
|
+
}, props.contentContainerStyle]
|
|
47
|
+
})
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const AnimatedScrollContainer = /*#__PURE__*/forwardRef(({
|
|
51
|
+
children,
|
|
52
|
+
contentContainerStyle,
|
|
53
|
+
...rest
|
|
54
|
+
}, ref) => {
|
|
55
|
+
return /*#__PURE__*/_jsx(ScrollView, {
|
|
56
|
+
...rest,
|
|
57
|
+
ref: ref,
|
|
58
|
+
children: /*#__PURE__*/_jsx(Animated.View, {
|
|
59
|
+
style: contentContainerStyle,
|
|
60
|
+
children: children
|
|
61
|
+
})
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
//# sourceMappingURL=FlatList.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["forwardRef","ScrollView","Animated","HeaderMotionScrollManager","jsx","_jsx","HeaderMotionFlatList","scrollId","props","children","onScroll","scrollViewProps","originalHeaderHeight","minHeightContentContainerStyle","FlatList","renderScrollComponent","propsz","AnimatedScrollContainer","contentContainerStyle","paddingTop","rest","ref","View","style"],"sourceRoot":"../../../src","sources":["components/FlatList.tsx"],"mappings":";;AAAA,SAASA,UAAU,QAAgD,OAAO;AAC1E,SAASC,UAAU,QAA8B,cAAc;AAC/D,OAAOC,QAAQ,MAAM,yBAAyB;AAC9C,SAASC,yBAAyB,QAAQ,oBAAiB;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAc5D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,oBAAoBA,CAAU;EAC5CC,QAAQ;EACR,GAAGC;AACyB,CAAC,EAAE;EAC/B,oBACEH,IAAA,CAACF,yBAAyB;IAACI,QAAQ,EAAEA,QAAS;IAAAE,QAAA,EAC3CA,CACC;MAAEC,QAAQ;MAAE,GAAGC;IAAgB,CAAC,EAChC;MAAEC,oBAAoB;MAAEC;IAA+B,CAAC,kBAExDR,IAAA,CAACH,QAAQ,CAACY,QAAQ;MAAA,GACZH,eAAe;MAAA,GACfH,KAAK;MACTE,QAAQ,EAAEA,QAAS;MACnBK,qBAAqB,EAAGC,MAAM,iBAC5BX,IAAA,CAACY,uBAAuB;QAAA,GAAKD;MAAM,CAAG,CACtC;MACFE,qBAAqB,EAAE,CACrBL,8BAA8B,EAC9B;QAAEM,UAAU,EAAEP;MAAqB,CAAC,EACpCJ,KAAK,CAACU,qBAAqB;IAC3B,CACH;EACF,CACwB,CAAC;AAEhC;AAEA,MAAMD,uBAAuB,gBAAGjB,UAAU,CAGxC,CAAC;EAAES,QAAQ;EAAES,qBAAqB;EAAE,GAAGE;AAAK,CAAC,EAAEC,GAAG,KAAK;EACvD,oBACEhB,IAAA,CAACJ,UAAU;IAAA,GAAKmB,IAAI;IAAEC,GAAG,EAAEA,GAAI;IAAAZ,QAAA,eAC7BJ,IAAA,CAACH,QAAQ,CAACoB,IAAI;MAACC,KAAK,EAAEL,qBAAsB;MAAAT,QAAA,EAAEA;IAAQ,CAAgB;EAAC,CAC7D,CAAC;AAEjB,CAAC,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { useMotionProgress } from "../hooks/useMotionProgress.js";
|
|
4
|
+
/**
|
|
5
|
+
* Header component for providing motion progress properties to animated headers.
|
|
6
|
+
* Must be used within a HeaderMotion component.
|
|
7
|
+
*
|
|
8
|
+
* Use to pass props to the header components in React Navigation / Expo Router, which cannot access HeaderMotion's context and `useMotionProgress` otherwise.`
|
|
9
|
+
*/
|
|
10
|
+
export function HeaderMotionHeader({
|
|
11
|
+
children
|
|
12
|
+
}) {
|
|
13
|
+
if (typeof children !== 'function') {
|
|
14
|
+
throw new Error('HeaderMotion.Header only accepts render function as the only child.');
|
|
15
|
+
}
|
|
16
|
+
const motionProgressProps = useMotionProgress();
|
|
17
|
+
return children(motionProgressProps);
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=Header.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["useMotionProgress","HeaderMotionHeader","children","Error","motionProgressProps"],"sourceRoot":"../../../src","sources":["components/Header.tsx"],"mappings":";;AAAA,SAASA,iBAAiB,QAAQ,+BAA4B;AAc9D;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,kBAAkBA,CAAC;EAAEC;AAAkC,CAAC,EAAE;EACxE,IAAI,OAAOA,QAAQ,KAAK,UAAU,EAAE;IAClC,MAAM,IAAIC,KAAK,CACb,qEACF,CAAC;EACH;EAEA,MAAMC,mBAAmB,GAAGJ,iBAAiB,CAAC,CAAC;EAC/C,OAAOE,QAAQ,CAACE,mBAAmB,CAAC;AACtC","ignoreList":[]}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { StyleSheet, View } from 'react-native';
|
|
4
|
+
import Animated from 'react-native-reanimated';
|
|
5
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
6
|
+
/**
|
|
7
|
+
* Base header component with absolute positioning.
|
|
8
|
+
* Provides a foundation for building headers that need to be positioned absolutely.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* <HeaderBase
|
|
13
|
+
* onLayout={measureTotalHeight}
|
|
14
|
+
* >
|
|
15
|
+
* ...
|
|
16
|
+
* </HeaderBase>
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function HeaderBase({
|
|
20
|
+
style,
|
|
21
|
+
...rest
|
|
22
|
+
}) {
|
|
23
|
+
return /*#__PURE__*/_jsx(View, {
|
|
24
|
+
style: [style, styles.container],
|
|
25
|
+
...rest
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Animated version of HeaderBase using Reanimated's Animated.View.
|
|
31
|
+
* Use this when you need to animate the header based on scroll progress.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```tsx
|
|
35
|
+
* <AnimatedHeaderBase
|
|
36
|
+
* onLayout={measureTotalHeight}
|
|
37
|
+
* style={[{ paddingTop: insets.top }, animatedStyle]}
|
|
38
|
+
* >
|
|
39
|
+
* ...
|
|
40
|
+
* </AnimatedHeaderBase>
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export function AnimatedHeaderBase({
|
|
44
|
+
style,
|
|
45
|
+
...rest
|
|
46
|
+
}) {
|
|
47
|
+
return /*#__PURE__*/_jsx(Animated.View, {
|
|
48
|
+
style: [style, styles.container],
|
|
49
|
+
...rest
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const styles = StyleSheet.create({
|
|
53
|
+
container: {
|
|
54
|
+
position: 'absolute',
|
|
55
|
+
left: 0,
|
|
56
|
+
right: 0
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
//# sourceMappingURL=HeaderBase.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["StyleSheet","View","Animated","jsx","_jsx","HeaderBase","style","rest","styles","container","AnimatedHeaderBase","create","position","left","right"],"sourceRoot":"../../../src","sources":["components/HeaderBase.tsx"],"mappings":";;AAAA,SAASA,UAAU,EAAEC,IAAI,QAAwB,cAAc;AAC/D,OAAOC,QAAQ,MAA8B,yBAAyB;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAKvE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,UAAUA,CAAC;EAAEC,KAAK;EAAE,GAAGC;AAAsB,CAAC,EAAE;EAC9D,oBAAOH,IAAA,CAACH,IAAI;IAACK,KAAK,EAAE,CAACA,KAAK,EAAEE,MAAM,CAACC,SAAS,CAAE;IAAA,GAAKF;EAAI,CAAG,CAAC;AAC7D;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASG,kBAAkBA,CAAC;EACjCJ,KAAK;EACL,GAAGC;AACoB,CAAC,EAAE;EAC1B,oBAAOH,IAAA,CAACF,QAAQ,CAACD,IAAI;IAACK,KAAK,EAAE,CAACA,KAAK,EAAEE,MAAM,CAACC,SAAS,CAAE;IAAA,GAAKF;EAAI,CAAG,CAAC;AACtE;AAEA,MAAMC,MAAM,GAAGR,UAAU,CAACW,MAAM,CAAC;EAC/BF,SAAS,EAAE;IACTG,QAAQ,EAAE,UAAU;IACpBC,IAAI,EAAE,CAAC;IACPC,KAAK,EAAE;EACT;AACF,CAAC,CAAC","ignoreList":[]}
|