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/package.json
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-header-motion",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Smooth, animated collapsible headers with scroll-based motion control in React Native",
|
|
5
|
+
"main": "./lib/module/index.js",
|
|
6
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"source": "./src/index.tsx",
|
|
10
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
11
|
+
"default": "./lib/module/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"lib",
|
|
18
|
+
"android",
|
|
19
|
+
"ios",
|
|
20
|
+
"cpp",
|
|
21
|
+
"*.podspec",
|
|
22
|
+
"react-native.config.js",
|
|
23
|
+
"!ios/build",
|
|
24
|
+
"!android/build",
|
|
25
|
+
"!android/gradle",
|
|
26
|
+
"!android/gradlew",
|
|
27
|
+
"!android/gradlew.bat",
|
|
28
|
+
"!android/local.properties",
|
|
29
|
+
"!**/__tests__",
|
|
30
|
+
"!**/__fixtures__",
|
|
31
|
+
"!**/__mocks__",
|
|
32
|
+
"!**/.*"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"example": "yarn workspace react-native-header-motion-example",
|
|
36
|
+
"clean": "del-cli lib",
|
|
37
|
+
"prepare": "bob build",
|
|
38
|
+
"typecheck": "tsc",
|
|
39
|
+
"test": "jest",
|
|
40
|
+
"release": "release-it --only-version",
|
|
41
|
+
"lint": "eslint \"**/*.{js,ts,tsx}\""
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"react-native",
|
|
45
|
+
"ios",
|
|
46
|
+
"android"
|
|
47
|
+
],
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/pawicao/react-native-header-motion.git"
|
|
51
|
+
},
|
|
52
|
+
"author": "Oskar Pawica <pawicao@gmail.com> (https://github.com/pawicao)",
|
|
53
|
+
"license": "MIT",
|
|
54
|
+
"bugs": {
|
|
55
|
+
"url": "https://github.com/pawicao/react-native-header-motion/issues"
|
|
56
|
+
},
|
|
57
|
+
"homepage": "https://github.com/pawicao/react-native-header-motion#readme",
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"registry": "https://registry.npmjs.org/"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@commitlint/config-conventional": "^19.8.1",
|
|
63
|
+
"@eslint/compat": "^1.3.2",
|
|
64
|
+
"@eslint/eslintrc": "^3.3.1",
|
|
65
|
+
"@eslint/js": "^9.35.0",
|
|
66
|
+
"@react-native/babel-preset": "0.83.0",
|
|
67
|
+
"@react-native/eslint-config": "0.83.0",
|
|
68
|
+
"@release-it/conventional-changelog": "^10.0.1",
|
|
69
|
+
"@types/jest": "^29.5.14",
|
|
70
|
+
"@types/react": "^19.1.12",
|
|
71
|
+
"commitlint": "^19.8.1",
|
|
72
|
+
"del-cli": "^6.0.0",
|
|
73
|
+
"eslint": "^9.35.0",
|
|
74
|
+
"eslint-config-prettier": "^10.1.8",
|
|
75
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
76
|
+
"jest": "^29.7.0",
|
|
77
|
+
"lefthook": "^2.0.3",
|
|
78
|
+
"prettier": "^2.8.8",
|
|
79
|
+
"react": "19.1.0",
|
|
80
|
+
"react-native": "0.81.5",
|
|
81
|
+
"react-native-builder-bob": "^0.40.17",
|
|
82
|
+
"react-native-reanimated": "4.1.1",
|
|
83
|
+
"react-native-worklets": "0.5.1",
|
|
84
|
+
"release-it": "^19.0.4",
|
|
85
|
+
"typescript": "^5.9.2"
|
|
86
|
+
},
|
|
87
|
+
"peerDependencies": {
|
|
88
|
+
"react": "*",
|
|
89
|
+
"react-native": "*",
|
|
90
|
+
"react-native-reanimated": ">=4.0.0",
|
|
91
|
+
"react-native-worklets": ">=0.4.0"
|
|
92
|
+
},
|
|
93
|
+
"workspaces": [
|
|
94
|
+
"example"
|
|
95
|
+
],
|
|
96
|
+
"packageManager": "yarn@4.11.0",
|
|
97
|
+
"react-native-builder-bob": {
|
|
98
|
+
"source": "src",
|
|
99
|
+
"output": "lib",
|
|
100
|
+
"targets": [
|
|
101
|
+
[
|
|
102
|
+
"module",
|
|
103
|
+
{
|
|
104
|
+
"esm": true
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
[
|
|
108
|
+
"typescript",
|
|
109
|
+
{
|
|
110
|
+
"project": "tsconfig.build.json"
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
]
|
|
114
|
+
},
|
|
115
|
+
"jest": {
|
|
116
|
+
"preset": "react-native",
|
|
117
|
+
"modulePathIgnorePatterns": [
|
|
118
|
+
"<rootDir>/example/node_modules",
|
|
119
|
+
"<rootDir>/lib/"
|
|
120
|
+
]
|
|
121
|
+
},
|
|
122
|
+
"commitlint": {
|
|
123
|
+
"extends": [
|
|
124
|
+
"@commitlint/config-conventional"
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
"release-it": {
|
|
128
|
+
"git": {
|
|
129
|
+
"commitMessage": "chore: release ${version}",
|
|
130
|
+
"tagName": "v${version}"
|
|
131
|
+
},
|
|
132
|
+
"npm": {
|
|
133
|
+
"publish": true
|
|
134
|
+
},
|
|
135
|
+
"github": {
|
|
136
|
+
"release": true
|
|
137
|
+
},
|
|
138
|
+
"plugins": {
|
|
139
|
+
"@release-it/conventional-changelog": {
|
|
140
|
+
"preset": {
|
|
141
|
+
"name": "angular"
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
"prettier": {
|
|
147
|
+
"quoteProps": "consistent",
|
|
148
|
+
"singleQuote": true,
|
|
149
|
+
"tabWidth": 2,
|
|
150
|
+
"trailingComma": "es5",
|
|
151
|
+
"useTabs": false
|
|
152
|
+
},
|
|
153
|
+
"create-react-native-library": {
|
|
154
|
+
"type": "library",
|
|
155
|
+
"languages": "js",
|
|
156
|
+
"tools": [
|
|
157
|
+
"jest",
|
|
158
|
+
"lefthook",
|
|
159
|
+
"release-it",
|
|
160
|
+
"eslint"
|
|
161
|
+
],
|
|
162
|
+
"version": "0.56.0"
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { forwardRef, type ComponentProps, type ComponentRef } from 'react';
|
|
2
|
+
import { ScrollView, type ScrollViewProps } from 'react-native';
|
|
3
|
+
import Animated from 'react-native-reanimated';
|
|
4
|
+
import { HeaderMotionScrollManager } from './ScrollManager';
|
|
5
|
+
|
|
6
|
+
type AnimatedFlatListProps<T = any> = ComponentProps<
|
|
7
|
+
typeof Animated.FlatList<T>
|
|
8
|
+
>;
|
|
9
|
+
|
|
10
|
+
export type HeaderMotionFlatListProps<T = any> = AnimatedFlatListProps<T> & {
|
|
11
|
+
/**
|
|
12
|
+
* Optional unique identifier for this scroll view.
|
|
13
|
+
* Use this when you have multiple scroll views (e.g. in tabs) to track them separately.
|
|
14
|
+
*/
|
|
15
|
+
scrollId?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Animated FlatList component that integrates with HeaderMotion.
|
|
20
|
+
* Automatically handles scroll tracking and header animation synchronization.
|
|
21
|
+
* Must be used within a HeaderMotion component.
|
|
22
|
+
*
|
|
23
|
+
* @template T - The type of items in the FlatList
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* <HeaderMotion>
|
|
28
|
+
* <HeaderMotion.FlatList
|
|
29
|
+
* data={items}
|
|
30
|
+
* renderItem={({ item }) => <Text>{item}</Text>}
|
|
31
|
+
* />
|
|
32
|
+
* </HeaderMotion>
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export function HeaderMotionFlatList<T = any>({
|
|
36
|
+
scrollId,
|
|
37
|
+
...props
|
|
38
|
+
}: HeaderMotionFlatListProps<T>) {
|
|
39
|
+
return (
|
|
40
|
+
<HeaderMotionScrollManager scrollId={scrollId}>
|
|
41
|
+
{(
|
|
42
|
+
{ onScroll, ...scrollViewProps },
|
|
43
|
+
{ originalHeaderHeight, minHeightContentContainerStyle }
|
|
44
|
+
) => (
|
|
45
|
+
<Animated.FlatList
|
|
46
|
+
{...scrollViewProps}
|
|
47
|
+
{...props}
|
|
48
|
+
onScroll={onScroll}
|
|
49
|
+
renderScrollComponent={(propsz) => (
|
|
50
|
+
<AnimatedScrollContainer {...propsz} />
|
|
51
|
+
)}
|
|
52
|
+
contentContainerStyle={[
|
|
53
|
+
minHeightContentContainerStyle,
|
|
54
|
+
{ paddingTop: originalHeaderHeight },
|
|
55
|
+
props.contentContainerStyle,
|
|
56
|
+
]}
|
|
57
|
+
/>
|
|
58
|
+
)}
|
|
59
|
+
</HeaderMotionScrollManager>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const AnimatedScrollContainer = forwardRef<
|
|
64
|
+
ComponentRef<typeof ScrollView>,
|
|
65
|
+
ScrollViewProps
|
|
66
|
+
>(({ children, contentContainerStyle, ...rest }, ref) => {
|
|
67
|
+
return (
|
|
68
|
+
<ScrollView {...rest} ref={ref}>
|
|
69
|
+
<Animated.View style={contentContainerStyle}>{children}</Animated.View>
|
|
70
|
+
</ScrollView>
|
|
71
|
+
);
|
|
72
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useMotionProgress } from '../hooks/useMotionProgress';
|
|
2
|
+
import type { MotionProgress } from '../types';
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
type HeaderRenderChildren = (props: MotionProgress) => ReactNode;
|
|
6
|
+
|
|
7
|
+
export interface HeaderMotionHeaderProps {
|
|
8
|
+
/**
|
|
9
|
+
* Render function that receives motion progress props.
|
|
10
|
+
* Use this to animate your header based on scroll progress and to provide measurement functions to the elements of the header.
|
|
11
|
+
*/
|
|
12
|
+
children: HeaderRenderChildren;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Header component for providing motion progress properties to animated headers.
|
|
17
|
+
* Must be used within a HeaderMotion component.
|
|
18
|
+
*
|
|
19
|
+
* Use to pass props to the header components in React Navigation / Expo Router, which cannot access HeaderMotion's context and `useMotionProgress` otherwise.`
|
|
20
|
+
*/
|
|
21
|
+
export function HeaderMotionHeader({ children }: HeaderMotionHeaderProps) {
|
|
22
|
+
if (typeof children !== 'function') {
|
|
23
|
+
throw new Error(
|
|
24
|
+
'HeaderMotion.Header only accepts render function as the only child.'
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const motionProgressProps = useMotionProgress();
|
|
29
|
+
return children(motionProgressProps);
|
|
30
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { StyleSheet, View, type ViewProps } from 'react-native';
|
|
2
|
+
import Animated, { type AnimatedProps } from 'react-native-reanimated';
|
|
3
|
+
|
|
4
|
+
export type HeaderBaseProps = ViewProps;
|
|
5
|
+
export type AnimatedHeaderBaseProps = AnimatedProps<ViewProps>;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Base header component with absolute positioning.
|
|
9
|
+
* Provides a foundation for building headers that need to be positioned absolutely.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* <HeaderBase
|
|
14
|
+
* onLayout={measureTotalHeight}
|
|
15
|
+
* >
|
|
16
|
+
* ...
|
|
17
|
+
* </HeaderBase>
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function HeaderBase({ style, ...rest }: HeaderBaseProps) {
|
|
21
|
+
return <View style={[style, styles.container]} {...rest} />;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Animated version of HeaderBase using Reanimated's Animated.View.
|
|
26
|
+
* Use this when you need to animate the header based on scroll progress.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```tsx
|
|
30
|
+
* <AnimatedHeaderBase
|
|
31
|
+
* onLayout={measureTotalHeight}
|
|
32
|
+
* style={[{ paddingTop: insets.top }, animatedStyle]}
|
|
33
|
+
* >
|
|
34
|
+
* ...
|
|
35
|
+
* </AnimatedHeaderBase>
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export function AnimatedHeaderBase({
|
|
39
|
+
style,
|
|
40
|
+
...rest
|
|
41
|
+
}: AnimatedHeaderBaseProps) {
|
|
42
|
+
return <Animated.View style={[style, styles.container]} {...rest} />;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const styles = StyleSheet.create({
|
|
46
|
+
container: {
|
|
47
|
+
position: 'absolute',
|
|
48
|
+
left: 0,
|
|
49
|
+
right: 0,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Extrapolation,
|
|
4
|
+
interpolate,
|
|
5
|
+
useAnimatedReaction,
|
|
6
|
+
useDerivedValue,
|
|
7
|
+
useSharedValue,
|
|
8
|
+
type ExtrapolationType,
|
|
9
|
+
type SharedValue,
|
|
10
|
+
} from 'react-native-reanimated';
|
|
11
|
+
import { HeaderMotionContext } from '../context';
|
|
12
|
+
import type { ReactNode } from 'react';
|
|
13
|
+
import type {
|
|
14
|
+
MeasureAnimatedHeader,
|
|
15
|
+
MeasureAnimatedHeaderAndSet,
|
|
16
|
+
ProgressThreshold,
|
|
17
|
+
ScrollValues,
|
|
18
|
+
} from '../types';
|
|
19
|
+
import {
|
|
20
|
+
DEFAULT_MEASURE_DYNAMIC,
|
|
21
|
+
DEFAULT_PROGRESS_THRESHOLD,
|
|
22
|
+
DEFAULT_SCROLL_ID,
|
|
23
|
+
getInitialScrollValue,
|
|
24
|
+
} from '../utils';
|
|
25
|
+
|
|
26
|
+
export interface HeaderMotionProps<T extends string> {
|
|
27
|
+
/**
|
|
28
|
+
* The threshold at which the header animation completes (reaches progress = 1).
|
|
29
|
+
* Can be a fixed number or a function that calculates based on the result of {@link measureDynamic}.
|
|
30
|
+
*
|
|
31
|
+
* Defaults to a function that returns the return value of `measureDynamic` unchanged.
|
|
32
|
+
*/
|
|
33
|
+
progressThreshold?: ProgressThreshold;
|
|
34
|
+
/**
|
|
35
|
+
* Function to measure a dimension of choice of the animated element of the header.
|
|
36
|
+
*
|
|
37
|
+
* Receives the layout change event from React Native.
|
|
38
|
+
*
|
|
39
|
+
* This function can be further accessed when rendering headers from `HeaderMotion.Header` or `useMotionProgress` - should be passed to the `onLayout` prop of such. If used, can be used for dynamic calculation of the {@link progressThreshold}.
|
|
40
|
+
*
|
|
41
|
+
* Defaults to measuring the height from the event.
|
|
42
|
+
*/
|
|
43
|
+
measureDynamic?: MeasureAnimatedHeader;
|
|
44
|
+
/**
|
|
45
|
+
* Mode for measuring dynamic header height.
|
|
46
|
+
* - 'mount': Only measure once on mount
|
|
47
|
+
* - 'update': Update measurement on every layout recalculation of the component that {@link measureDynamic} was provided to as the `onLayout` property
|
|
48
|
+
* @default 'mount'
|
|
49
|
+
*/
|
|
50
|
+
measureDynamicMode?: 'update' | 'mount';
|
|
51
|
+
/**
|
|
52
|
+
* Shared value for tracking the active scroll ID in multi-scroll scenarios (e.g. tabs).
|
|
53
|
+
* When provided, the header animation will sync across multiple scroll views.
|
|
54
|
+
*/
|
|
55
|
+
activeScrollId?: SharedValue<T>;
|
|
56
|
+
/**
|
|
57
|
+
* Extrapolation type for the progress animation.
|
|
58
|
+
* Controls how the progress value behaves outside the threshold range.
|
|
59
|
+
*
|
|
60
|
+
* You may want to modify it to achieve some animations for the overscroll scenarios.
|
|
61
|
+
* @default Extrapolation.CLAMP
|
|
62
|
+
*/
|
|
63
|
+
progressExtrapolation?: ExtrapolationType;
|
|
64
|
+
/** Child components that will have access to the header motion context */
|
|
65
|
+
children: ReactNode;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Context provider component for HeaderMotion.
|
|
70
|
+
* Manages header animation state and provides it to child components via context.
|
|
71
|
+
* @template T - The type of scroll ID string
|
|
72
|
+
*/
|
|
73
|
+
function HeaderMotionContextProvider<T extends string>({
|
|
74
|
+
progressThreshold = DEFAULT_PROGRESS_THRESHOLD,
|
|
75
|
+
measureDynamic = DEFAULT_MEASURE_DYNAMIC,
|
|
76
|
+
measureDynamicMode = 'mount',
|
|
77
|
+
activeScrollId,
|
|
78
|
+
progressExtrapolation = Extrapolation.CLAMP,
|
|
79
|
+
children,
|
|
80
|
+
}: HeaderMotionProps<T>) {
|
|
81
|
+
const [dynamicMeasurement, setDynamicMeasurement] = useState<
|
|
82
|
+
number | undefined
|
|
83
|
+
>(undefined);
|
|
84
|
+
const [originalHeaderHeight, setOriginalHeaderHeight] = useState(0);
|
|
85
|
+
|
|
86
|
+
const setOrUpdateDynamicMeasurement =
|
|
87
|
+
useCallback<MeasureAnimatedHeaderAndSet>(
|
|
88
|
+
(e) => {
|
|
89
|
+
const measured = measureDynamic(e);
|
|
90
|
+
setDynamicMeasurement((prevMeasurement) => {
|
|
91
|
+
if (prevMeasurement !== undefined && measureDynamicMode === 'mount') {
|
|
92
|
+
return prevMeasurement;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return measured;
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
[measureDynamicMode, measureDynamic, setDynamicMeasurement]
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const calculatedProgressThreshold = useMemo(() => {
|
|
102
|
+
if (typeof progressThreshold === 'number') {
|
|
103
|
+
return progressThreshold;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (dynamicMeasurement === undefined) {
|
|
107
|
+
return Infinity;
|
|
108
|
+
}
|
|
109
|
+
return progressThreshold(dynamicMeasurement);
|
|
110
|
+
}, [dynamicMeasurement, progressThreshold]);
|
|
111
|
+
|
|
112
|
+
const measureTotalHeight = useCallback<MeasureAnimatedHeaderAndSet>(
|
|
113
|
+
(e) => {
|
|
114
|
+
const measuredValue = e.nativeEvent.layout.height;
|
|
115
|
+
setOriginalHeaderHeight(measuredValue);
|
|
116
|
+
},
|
|
117
|
+
[setOriginalHeaderHeight]
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const scrollValues = useSharedValue<ScrollValues>({
|
|
121
|
+
[DEFAULT_SCROLL_ID]: getInitialScrollValue(),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
useAnimatedReaction(
|
|
125
|
+
() => activeScrollId?.get(),
|
|
126
|
+
(id) => {
|
|
127
|
+
if (!id || scrollValues.get()[id]) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
scrollValues.modify((value) => {
|
|
132
|
+
(value as ScrollValues)[id] = getInitialScrollValue();
|
|
133
|
+
return value;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const progress = useDerivedValue(() => {
|
|
139
|
+
const id = activeScrollId?.get() ?? DEFAULT_SCROLL_ID;
|
|
140
|
+
const scrollValue = scrollValues.get()[id];
|
|
141
|
+
|
|
142
|
+
if (!scrollValue) {
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const { min, current } = scrollValue;
|
|
147
|
+
return interpolate(
|
|
148
|
+
current,
|
|
149
|
+
[min, min + calculatedProgressThreshold],
|
|
150
|
+
[0, 1],
|
|
151
|
+
progressExtrapolation
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const ctxValue = useMemo(
|
|
156
|
+
() => ({
|
|
157
|
+
progress,
|
|
158
|
+
originalHeaderHeight,
|
|
159
|
+
measureDynamic: setOrUpdateDynamicMeasurement,
|
|
160
|
+
measureTotalHeight,
|
|
161
|
+
progressThreshold: calculatedProgressThreshold,
|
|
162
|
+
scrollValues,
|
|
163
|
+
activeScrollId: activeScrollId as SharedValue<string> | undefined,
|
|
164
|
+
}),
|
|
165
|
+
[
|
|
166
|
+
originalHeaderHeight,
|
|
167
|
+
progress,
|
|
168
|
+
measureTotalHeight,
|
|
169
|
+
setOrUpdateDynamicMeasurement,
|
|
170
|
+
scrollValues,
|
|
171
|
+
activeScrollId,
|
|
172
|
+
calculatedProgressThreshold,
|
|
173
|
+
]
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<HeaderMotionContext.Provider value={ctxValue}>
|
|
178
|
+
{children}
|
|
179
|
+
</HeaderMotionContext.Provider>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export { HeaderMotionContextProvider };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useScrollManager } from '../hooks';
|
|
2
|
+
import type { ScrollManagerConfig } from '../types';
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
type ScrollManagerRenderChildren = (
|
|
6
|
+
scrollableProps: ScrollManagerConfig['scrollableProps'],
|
|
7
|
+
options: ScrollManagerConfig['headerMotionContext']
|
|
8
|
+
) => ReactNode;
|
|
9
|
+
|
|
10
|
+
export interface HeaderMotionScrollManagerProps {
|
|
11
|
+
/**
|
|
12
|
+
* Optional unique identifier for this scroll view.
|
|
13
|
+
* Use this when you have multiple scroll views (e.g., in tabs) to track them separately.
|
|
14
|
+
*/
|
|
15
|
+
scrollId?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Render function that receives scroll props and header context.
|
|
18
|
+
* Use this to create custom scroll implementations that integrate with HeaderMotion.
|
|
19
|
+
*/
|
|
20
|
+
children: ScrollManagerRenderChildren;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* ScrollManager component that provides scroll tracking functionality for custom scroll implementations. Uses {@link useScrollManager} under the hood.
|
|
25
|
+
* Must be used within a HeaderMotion component.
|
|
26
|
+
*
|
|
27
|
+
* This is useful when you need to use a scroll component that isn't directly supported
|
|
28
|
+
* (like a custom scroll view or third-party list components).
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```tsx
|
|
32
|
+
* <HeaderMotion>
|
|
33
|
+
* <HeaderMotion.ScrollManager>
|
|
34
|
+
* {(scrollableProps, { originalHeaderHeight }) => (
|
|
35
|
+
* <CustomScrollView {...scrollableProps}>
|
|
36
|
+
* <View style={{ paddingTop: originalHeaderHeight }}>
|
|
37
|
+
* <Text>Content</Text>
|
|
38
|
+
* </View>
|
|
39
|
+
* </CustomScrollView>
|
|
40
|
+
* )}
|
|
41
|
+
* </HeaderMotion.ScrollManager>
|
|
42
|
+
* </HeaderMotion>
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function HeaderMotionScrollManager({
|
|
46
|
+
children,
|
|
47
|
+
scrollId,
|
|
48
|
+
}: HeaderMotionScrollManagerProps) {
|
|
49
|
+
if (typeof children !== 'function') {
|
|
50
|
+
throw new Error(
|
|
51
|
+
'HeaderMotion.ScrollManager only accepts render function as the only child.'
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const { scrollableProps, headerMotionContext } = useScrollManager(scrollId);
|
|
56
|
+
|
|
57
|
+
return children(scrollableProps, headerMotionContext);
|
|
58
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import Animated, {
|
|
2
|
+
type AnimatedScrollViewProps,
|
|
3
|
+
} from 'react-native-reanimated';
|
|
4
|
+
import { HeaderMotionScrollManager } from './ScrollManager';
|
|
5
|
+
|
|
6
|
+
export type HeaderMotionScrollViewProps = AnimatedScrollViewProps & {
|
|
7
|
+
/**
|
|
8
|
+
* Optional unique identifier for this scroll view.
|
|
9
|
+
* Use this when you have multiple scroll views (e.g. in tabs) to track them separately.
|
|
10
|
+
*/
|
|
11
|
+
scrollId?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Animated ScrollView component that integrates with HeaderMotion.
|
|
16
|
+
* Automatically handles scroll tracking and header animation synchronization.
|
|
17
|
+
* Must be used within a HeaderMotion component.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* <HeaderMotion>
|
|
22
|
+
* <HeaderMotion.ScrollView>
|
|
23
|
+
* <MyScrollableContent />
|
|
24
|
+
* </HeaderMotion.ScrollView>
|
|
25
|
+
* </HeaderMotion>
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function HeaderMotionScrollView({
|
|
29
|
+
scrollId,
|
|
30
|
+
children,
|
|
31
|
+
contentContainerStyle,
|
|
32
|
+
...props
|
|
33
|
+
}: HeaderMotionScrollViewProps) {
|
|
34
|
+
return (
|
|
35
|
+
<HeaderMotionScrollManager scrollId={scrollId}>
|
|
36
|
+
{(
|
|
37
|
+
{ onScroll, ...scrollViewProps },
|
|
38
|
+
{ originalHeaderHeight, minHeightContentContainerStyle }
|
|
39
|
+
) => (
|
|
40
|
+
<Animated.ScrollView
|
|
41
|
+
{...scrollViewProps}
|
|
42
|
+
{...props}
|
|
43
|
+
onScroll={onScroll}
|
|
44
|
+
>
|
|
45
|
+
<Animated.View
|
|
46
|
+
style={[
|
|
47
|
+
minHeightContentContainerStyle,
|
|
48
|
+
{ paddingTop: originalHeaderHeight },
|
|
49
|
+
contentContainerStyle,
|
|
50
|
+
]}
|
|
51
|
+
>
|
|
52
|
+
{children}
|
|
53
|
+
</Animated.View>
|
|
54
|
+
</Animated.ScrollView>
|
|
55
|
+
)}
|
|
56
|
+
</HeaderMotionScrollManager>
|
|
57
|
+
);
|
|
58
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createContext } from 'react';
|
|
2
|
+
import { type SharedValue } from 'react-native-reanimated';
|
|
3
|
+
import type {
|
|
4
|
+
MeasureAnimatedHeaderAndSet,
|
|
5
|
+
Progress,
|
|
6
|
+
ScrollValues,
|
|
7
|
+
} from './types';
|
|
8
|
+
|
|
9
|
+
interface HeaderMotionContextType {
|
|
10
|
+
progress: Progress;
|
|
11
|
+
measureTotalHeight: MeasureAnimatedHeaderAndSet;
|
|
12
|
+
measureDynamic: MeasureAnimatedHeaderAndSet;
|
|
13
|
+
scrollValues: SharedValue<ScrollValues>;
|
|
14
|
+
activeScrollId: SharedValue<string> | undefined;
|
|
15
|
+
progressThreshold: number;
|
|
16
|
+
originalHeaderHeight: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const HeaderMotionContext =
|
|
20
|
+
createContext<HeaderMotionContextType | null>(null);
|