react-native-screen-transitions 2.0.2 → 2.0.3
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/package.json +5 -3
- package/src/__tests__ /geometry.test.ts +127 -0
- package/src/components/bounds-activator.tsx +29 -0
- package/src/components/controllers/screen-lifecycle.tsx +72 -0
- package/src/components/create-transition-aware-component.tsx +99 -0
- package/src/components/root-transition-aware.tsx +56 -0
- package/src/configs/index.ts +2 -0
- package/src/configs/presets.ts +227 -0
- package/src/configs/specs.ts +9 -0
- package/src/hooks/animation/use-associated-style.tsx +28 -0
- package/src/hooks/animation/use-screen-animation.tsx +142 -0
- package/src/hooks/bounds/use-bound-measurer.tsx +71 -0
- package/src/hooks/gestures/use-build-gestures.tsx +369 -0
- package/src/hooks/gestures/use-scroll-progress.tsx +60 -0
- package/src/hooks/use-stable-callback.tsx +15 -0
- package/src/index.ts +32 -0
- package/src/integrations/native-stack/navigators/createNativeStackNavigator.tsx +112 -0
- package/src/integrations/native-stack/utils/debounce.tsx +14 -0
- package/src/integrations/native-stack/utils/getModalRoutesKeys.ts +21 -0
- package/src/integrations/native-stack/utils/useAnimatedHeaderHeight.tsx +18 -0
- package/src/integrations/native-stack/utils/useDismissedRouteError.tsx +30 -0
- package/src/integrations/native-stack/utils/useInvalidPreventRemoveError.tsx +31 -0
- package/src/integrations/native-stack/views/FontProcessor.native.tsx +12 -0
- package/src/integrations/native-stack/views/FontProcessor.tsx +5 -0
- package/src/integrations/native-stack/views/FooterComponent.tsx +10 -0
- package/src/integrations/native-stack/views/NativeStackView.native.tsx +657 -0
- package/src/integrations/native-stack/views/NativeStackView.tsx +214 -0
- package/src/integrations/native-stack/views/useHeaderConfigProps.tsx +295 -0
- package/src/providers/gestures.tsx +89 -0
- package/src/providers/keys.tsx +38 -0
- package/src/stores/animations.ts +45 -0
- package/src/stores/bounds.ts +71 -0
- package/src/stores/gestures.ts +55 -0
- package/src/stores/navigator-dismiss-state.ts +17 -0
- package/src/stores/utils/reset-stores-for-screen.ts +14 -0
- package/src/types/animation.ts +76 -0
- package/src/types/bounds.ts +82 -0
- package/src/types/core.ts +50 -0
- package/src/types/gesture.ts +33 -0
- package/src/types/navigator.ts +744 -0
- package/src/types/utils.ts +3 -0
- package/src/utils/animation/animate.ts +28 -0
- package/src/utils/animation/run-transition.ts +49 -0
- package/src/utils/bounds/_types/builder.ts +35 -0
- package/src/utils/bounds/_types/geometry.ts +17 -0
- package/src/utils/bounds/_types/get-bounds.ts +10 -0
- package/src/utils/bounds/build-bound-styles.ts +184 -0
- package/src/utils/bounds/constants.ts +28 -0
- package/src/utils/bounds/flatten-styles.ts +21 -0
- package/src/utils/bounds/geometry.ts +113 -0
- package/src/utils/bounds/get-bounds.ts +56 -0
- package/src/utils/bounds/index.ts +46 -0
- package/src/utils/bounds/style-composers.ts +172 -0
- package/src/utils/gesture/apply-gesture-activation-criteria.ts +109 -0
- package/src/utils/gesture/map-gesture-to-progress.ts +11 -0
- package/src/utils/gesture/normalize-gesture-translation.ts +20 -0
- package/src/utils/index.ts +1 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { ParamListBase, RouteProp } from "@react-navigation/native";
|
|
2
|
+
import { useWindowDimensions } from "react-native";
|
|
3
|
+
import { type SharedValue, useDerivedValue } from "react-native-reanimated";
|
|
4
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
5
|
+
import { useKeys } from "../../providers/keys";
|
|
6
|
+
import { Animations } from "../../stores/animations";
|
|
7
|
+
import { Bounds } from "../../stores/bounds";
|
|
8
|
+
import { type GestureMap, Gestures } from "../../stores/gestures";
|
|
9
|
+
import type {
|
|
10
|
+
ScreenInterpolationProps,
|
|
11
|
+
ScreenTransitionState,
|
|
12
|
+
} from "../../types/animation";
|
|
13
|
+
import type { BoundEntry } from "../../types/bounds";
|
|
14
|
+
import type { NativeStackDescriptor } from "../../types/navigator";
|
|
15
|
+
import { buildBoundsAccessor } from "../../utils/bounds";
|
|
16
|
+
|
|
17
|
+
type BuiltState = {
|
|
18
|
+
progress: SharedValue<number>;
|
|
19
|
+
closing: SharedValue<number>;
|
|
20
|
+
animating: SharedValue<number>;
|
|
21
|
+
gesture: GestureMap;
|
|
22
|
+
route: RouteProp<ParamListBase>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const FALLBACK = Object.freeze({
|
|
26
|
+
progress: 0,
|
|
27
|
+
closing: 0,
|
|
28
|
+
animating: 0,
|
|
29
|
+
gesture: {
|
|
30
|
+
x: 0,
|
|
31
|
+
y: 0,
|
|
32
|
+
normalizedX: 0,
|
|
33
|
+
normalizedY: 0,
|
|
34
|
+
isDismissing: 0,
|
|
35
|
+
isDragging: 0,
|
|
36
|
+
},
|
|
37
|
+
bounds: {} as Record<string, BoundEntry>,
|
|
38
|
+
route: {} as RouteProp<ParamListBase>,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const useBuildScreenTransitionState = (
|
|
42
|
+
descriptor: NativeStackDescriptor | undefined,
|
|
43
|
+
): BuiltState | undefined => {
|
|
44
|
+
const key = descriptor?.route.key;
|
|
45
|
+
if (!key) return undefined;
|
|
46
|
+
const progress = Animations.getAnimation(key, "progress");
|
|
47
|
+
const closing = Animations.getAnimation(key, "closing");
|
|
48
|
+
const animating = Animations.getAnimation(key, "animating");
|
|
49
|
+
const gesture = Gestures.getRouteGestures(key);
|
|
50
|
+
const route = descriptor?.route;
|
|
51
|
+
|
|
52
|
+
return { progress, closing, animating, gesture, route };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const unwrap = (
|
|
56
|
+
s: BuiltState | undefined,
|
|
57
|
+
key: string | undefined,
|
|
58
|
+
): ScreenTransitionState | undefined => {
|
|
59
|
+
"worklet";
|
|
60
|
+
return s && key
|
|
61
|
+
? {
|
|
62
|
+
progress: s.progress.value ?? 0,
|
|
63
|
+
closing: s.closing.value ?? 0,
|
|
64
|
+
animating: s.animating.value ?? 0,
|
|
65
|
+
gesture: {
|
|
66
|
+
x: s.gesture.x.value ?? 0,
|
|
67
|
+
y: s.gesture.y.value ?? 0,
|
|
68
|
+
normalizedX: s.gesture.normalizedX.value ?? 0,
|
|
69
|
+
normalizedY: s.gesture.normalizedY.value ?? 0,
|
|
70
|
+
isDismissing: s.gesture.isDismissing.value ?? 0,
|
|
71
|
+
isDragging: s.gesture.isDragging.value ?? 0,
|
|
72
|
+
},
|
|
73
|
+
bounds: Bounds.getBounds(key) ?? {},
|
|
74
|
+
route: s.route,
|
|
75
|
+
}
|
|
76
|
+
: undefined;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export function _useScreenAnimation() {
|
|
80
|
+
const {
|
|
81
|
+
current: currentDescriptor,
|
|
82
|
+
next: nextDescriptor,
|
|
83
|
+
previous: previousDescriptor,
|
|
84
|
+
} = useKeys();
|
|
85
|
+
|
|
86
|
+
const dimensions = useWindowDimensions();
|
|
87
|
+
|
|
88
|
+
const currentAnimation = useBuildScreenTransitionState(currentDescriptor);
|
|
89
|
+
|
|
90
|
+
const nextAnimation = useBuildScreenTransitionState(nextDescriptor);
|
|
91
|
+
const prevAnimation = useBuildScreenTransitionState(previousDescriptor);
|
|
92
|
+
|
|
93
|
+
const insets = useSafeAreaInsets();
|
|
94
|
+
|
|
95
|
+
const screenInterpolatorProps = useDerivedValue<ScreenInterpolationProps>(
|
|
96
|
+
() => {
|
|
97
|
+
"worklet";
|
|
98
|
+
|
|
99
|
+
const previous = unwrap(prevAnimation, previousDescriptor?.route.key);
|
|
100
|
+
const next = unwrap(nextAnimation, nextDescriptor?.route.key);
|
|
101
|
+
const current =
|
|
102
|
+
unwrap(currentAnimation, currentDescriptor?.route.key) ?? FALLBACK;
|
|
103
|
+
|
|
104
|
+
const progress = current.progress + (next?.progress ?? 0);
|
|
105
|
+
|
|
106
|
+
const focused = !next;
|
|
107
|
+
const activeBoundId = Bounds.getActiveBoundId() || "";
|
|
108
|
+
|
|
109
|
+
const bounds = buildBoundsAccessor({
|
|
110
|
+
activeBoundId,
|
|
111
|
+
current,
|
|
112
|
+
previous,
|
|
113
|
+
next,
|
|
114
|
+
progress,
|
|
115
|
+
dimensions,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
layouts: { screen: dimensions },
|
|
120
|
+
insets,
|
|
121
|
+
previous,
|
|
122
|
+
current,
|
|
123
|
+
next,
|
|
124
|
+
focused,
|
|
125
|
+
activeBoundId,
|
|
126
|
+
progress,
|
|
127
|
+
bounds,
|
|
128
|
+
} satisfies ScreenInterpolationProps;
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const screenStyleInterpolator =
|
|
133
|
+
nextDescriptor?.options.screenStyleInterpolator ||
|
|
134
|
+
currentDescriptor?.options.screenStyleInterpolator;
|
|
135
|
+
|
|
136
|
+
return { screenInterpolatorProps, screenStyleInterpolator };
|
|
137
|
+
}
|
|
138
|
+
export function useScreenAnimation() {
|
|
139
|
+
const { screenInterpolatorProps } = _useScreenAnimation();
|
|
140
|
+
|
|
141
|
+
return useDerivedValue(() => screenInterpolatorProps.value);
|
|
142
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import type { View } from "react-native";
|
|
3
|
+
import {
|
|
4
|
+
type AnimatedRef,
|
|
5
|
+
measure,
|
|
6
|
+
type StyleProps,
|
|
7
|
+
useSharedValue,
|
|
8
|
+
} from "react-native-reanimated";
|
|
9
|
+
import { useKeys } from "../../providers/keys";
|
|
10
|
+
import { Bounds } from "../../stores/bounds";
|
|
11
|
+
import { flattenStyle } from "../../utils/bounds/flatten-styles";
|
|
12
|
+
import { useScreenAnimation } from "../animation/use-screen-animation";
|
|
13
|
+
|
|
14
|
+
interface BoundMeasurerHookProps {
|
|
15
|
+
sharedBoundTag: string;
|
|
16
|
+
animatedRef: AnimatedRef<View>;
|
|
17
|
+
current: { route: { key: string } };
|
|
18
|
+
style: StyleProps;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const useBoundMeasurer = ({
|
|
22
|
+
sharedBoundTag,
|
|
23
|
+
animatedRef,
|
|
24
|
+
current,
|
|
25
|
+
style,
|
|
26
|
+
}: BoundMeasurerHookProps) => {
|
|
27
|
+
const { previous } = useKeys();
|
|
28
|
+
const interpolatorProps = useScreenAnimation();
|
|
29
|
+
const hasAlreadyMeasured = useSharedValue(false);
|
|
30
|
+
|
|
31
|
+
const measureAndSet = useCallback(() => {
|
|
32
|
+
"worklet";
|
|
33
|
+
if (!sharedBoundTag) return;
|
|
34
|
+
const measured = measure(animatedRef);
|
|
35
|
+
if (measured) {
|
|
36
|
+
Bounds.setBounds(
|
|
37
|
+
current.route.key,
|
|
38
|
+
sharedBoundTag,
|
|
39
|
+
measured,
|
|
40
|
+
flattenStyle(style),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}, [sharedBoundTag, animatedRef, current.route.key, style]);
|
|
44
|
+
|
|
45
|
+
const measureOnLayout = useCallback(() => {
|
|
46
|
+
"worklet";
|
|
47
|
+
if (!sharedBoundTag || hasAlreadyMeasured.value) return;
|
|
48
|
+
|
|
49
|
+
const previousRouteKey = previous?.route.key;
|
|
50
|
+
if (!previousRouteKey) return;
|
|
51
|
+
|
|
52
|
+
const previousBounds = Bounds.getBounds(previousRouteKey);
|
|
53
|
+
const hasPreviousBoundForTag = previousBounds[sharedBoundTag];
|
|
54
|
+
|
|
55
|
+
if (interpolatorProps.value.current.animating && hasPreviousBoundForTag) {
|
|
56
|
+
measureAndSet();
|
|
57
|
+
hasAlreadyMeasured.value = true;
|
|
58
|
+
}
|
|
59
|
+
}, [
|
|
60
|
+
measureAndSet,
|
|
61
|
+
interpolatorProps,
|
|
62
|
+
sharedBoundTag,
|
|
63
|
+
previous?.route.key,
|
|
64
|
+
hasAlreadyMeasured,
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
measureAndSet,
|
|
69
|
+
measureOnLayout,
|
|
70
|
+
};
|
|
71
|
+
};
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { useCallback, useMemo } from "react";
|
|
2
|
+
import { useWindowDimensions } from "react-native";
|
|
3
|
+
import {
|
|
4
|
+
Gesture,
|
|
5
|
+
type GestureStateChangeEvent,
|
|
6
|
+
type GestureTouchEvent,
|
|
7
|
+
type GestureUpdateEvent,
|
|
8
|
+
type PanGestureHandlerEventPayload,
|
|
9
|
+
} from "react-native-gesture-handler";
|
|
10
|
+
import type { GestureStateManagerType } from "react-native-gesture-handler/lib/typescript/handlers/gestures/gestureStateManager";
|
|
11
|
+
import {
|
|
12
|
+
interpolate,
|
|
13
|
+
runOnJS,
|
|
14
|
+
type SharedValue,
|
|
15
|
+
useSharedValue,
|
|
16
|
+
} from "react-native-reanimated";
|
|
17
|
+
import type { ScrollProgress } from "../../providers/gestures";
|
|
18
|
+
import { useKeys } from "../../providers/keys";
|
|
19
|
+
import { Animations } from "../../stores/animations";
|
|
20
|
+
import { Gestures } from "../../stores/gestures";
|
|
21
|
+
import { NavigatorDismissState } from "../../stores/navigator-dismiss-state";
|
|
22
|
+
import { animate } from "../../utils/animation/animate";
|
|
23
|
+
import { runTransition } from "../../utils/animation/run-transition";
|
|
24
|
+
import { applyGestureActivationCriteria } from "../../utils/gesture/apply-gesture-activation-criteria";
|
|
25
|
+
import { mapGestureToProgress } from "../../utils/gesture/map-gesture-to-progress";
|
|
26
|
+
|
|
27
|
+
const GESTURE_VELOCITY_IMPACT = 0.3;
|
|
28
|
+
const DEFAULT_GESTURE_RESPONSE_DISTANCE = 50;
|
|
29
|
+
const DEFAULT_GESTURE_DIRECTION = "horizontal";
|
|
30
|
+
const DEFAULT_GESTURE_ENABLED = false;
|
|
31
|
+
const DEFAULT_GESTURE_DRIVES_PROGRESS = true;
|
|
32
|
+
|
|
33
|
+
interface BuildGesturesHookProps {
|
|
34
|
+
scrollProgress: SharedValue<ScrollProgress>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const useBuildGestures = ({
|
|
38
|
+
scrollProgress,
|
|
39
|
+
}: BuildGesturesHookProps) => {
|
|
40
|
+
const dimensions = useWindowDimensions();
|
|
41
|
+
const { current } = useKeys();
|
|
42
|
+
|
|
43
|
+
const initialTouch = useSharedValue({
|
|
44
|
+
x: 0,
|
|
45
|
+
y: 0,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const gestures = Gestures.getRouteGestures(current.route.key);
|
|
49
|
+
|
|
50
|
+
const animations = Animations.getAll(current.route.key);
|
|
51
|
+
|
|
52
|
+
const {
|
|
53
|
+
gestureDirection = DEFAULT_GESTURE_DIRECTION,
|
|
54
|
+
gestureEnabled = DEFAULT_GESTURE_ENABLED,
|
|
55
|
+
transitionSpec,
|
|
56
|
+
gestureVelocityImpact = GESTURE_VELOCITY_IMPACT,
|
|
57
|
+
gestureResponseDistance = DEFAULT_GESTURE_RESPONSE_DISTANCE,
|
|
58
|
+
gestureDrivesProgress = DEFAULT_GESTURE_DRIVES_PROGRESS,
|
|
59
|
+
} = current.options;
|
|
60
|
+
|
|
61
|
+
const directions = Array.isArray(gestureDirection)
|
|
62
|
+
? gestureDirection
|
|
63
|
+
: [gestureDirection];
|
|
64
|
+
|
|
65
|
+
const allowed = useMemo(
|
|
66
|
+
() => ({
|
|
67
|
+
bidirectional: directions.includes("bidirectional"),
|
|
68
|
+
vertical: directions.includes("vertical"),
|
|
69
|
+
verticalInverted: directions.includes("vertical-inverted"),
|
|
70
|
+
horizontal: directions.includes("horizontal"),
|
|
71
|
+
horizontalInverted: directions.includes("horizontal-inverted"),
|
|
72
|
+
}),
|
|
73
|
+
[directions],
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const nativeGesture = useMemo(() => Gesture.Native(), []);
|
|
77
|
+
|
|
78
|
+
const onTouchesDown = useCallback(
|
|
79
|
+
(e: GestureTouchEvent) => {
|
|
80
|
+
"worklet";
|
|
81
|
+
const firstTouch = e.changedTouches[0];
|
|
82
|
+
initialTouch.value = { x: firstTouch.x, y: firstTouch.y };
|
|
83
|
+
},
|
|
84
|
+
[initialTouch],
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const onTouchesMove = useCallback(
|
|
88
|
+
(e: GestureTouchEvent, manager: GestureStateManagerType) => {
|
|
89
|
+
"worklet";
|
|
90
|
+
|
|
91
|
+
const touch = e.changedTouches[0];
|
|
92
|
+
const deltaX = touch.x - initialTouch.value.x;
|
|
93
|
+
const deltaY = touch.y - initialTouch.value.y;
|
|
94
|
+
|
|
95
|
+
const isVerticalSwipe = Math.abs(deltaY) > Math.abs(deltaX);
|
|
96
|
+
const isHorizontalSwipe = Math.abs(deltaX) > Math.abs(deltaY);
|
|
97
|
+
|
|
98
|
+
const isSwipingDown = isVerticalSwipe && deltaY > 0;
|
|
99
|
+
const isSwipingUp = isVerticalSwipe && deltaY < 0;
|
|
100
|
+
const isSwipingRight = isHorizontalSwipe && deltaX > 0;
|
|
101
|
+
const isSwipingLeft = isHorizontalSwipe && deltaX < 0;
|
|
102
|
+
|
|
103
|
+
const minMovement = 5;
|
|
104
|
+
const hasEnoughMovement =
|
|
105
|
+
Math.abs(deltaX) > minMovement || Math.abs(deltaY) > minMovement;
|
|
106
|
+
|
|
107
|
+
if (!hasEnoughMovement) return;
|
|
108
|
+
|
|
109
|
+
if (gestures.isDragging?.value) {
|
|
110
|
+
manager.activate();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let shouldActivate = false;
|
|
115
|
+
|
|
116
|
+
if (allowed.vertical && isSwipingDown) {
|
|
117
|
+
shouldActivate = scrollProgress.value.y <= 0;
|
|
118
|
+
}
|
|
119
|
+
if (allowed.verticalInverted && isSwipingUp) {
|
|
120
|
+
const maxScrollableY =
|
|
121
|
+
scrollProgress.value.contentHeight -
|
|
122
|
+
scrollProgress.value.layoutHeight;
|
|
123
|
+
|
|
124
|
+
shouldActivate = scrollProgress.value.y >= maxScrollableY;
|
|
125
|
+
}
|
|
126
|
+
if (allowed.horizontal && isSwipingRight) {
|
|
127
|
+
shouldActivate = scrollProgress.value.x <= 0;
|
|
128
|
+
}
|
|
129
|
+
if (allowed.horizontalInverted && isSwipingLeft) {
|
|
130
|
+
const maxProgress =
|
|
131
|
+
scrollProgress.value.contentWidth - scrollProgress.value.layoutWidth;
|
|
132
|
+
shouldActivate = scrollProgress.value.x >= maxProgress;
|
|
133
|
+
}
|
|
134
|
+
if (allowed.bidirectional) {
|
|
135
|
+
if (isSwipingDown) {
|
|
136
|
+
shouldActivate = scrollProgress.value.y >= 0;
|
|
137
|
+
} else if (isSwipingUp) {
|
|
138
|
+
shouldActivate = scrollProgress.value.y <= 0;
|
|
139
|
+
} else if (isSwipingRight || isSwipingLeft) {
|
|
140
|
+
shouldActivate = true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (
|
|
145
|
+
(shouldActivate || gestures.isDragging?.value) &&
|
|
146
|
+
!gestures.isDismissing?.value
|
|
147
|
+
) {
|
|
148
|
+
manager.activate();
|
|
149
|
+
} else {
|
|
150
|
+
manager.fail();
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
[initialTouch, scrollProgress, gestures, allowed],
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const onStart = useCallback(() => {
|
|
157
|
+
"worklet";
|
|
158
|
+
gestures.isDragging.value = 1;
|
|
159
|
+
gestures.isDismissing.value = 0;
|
|
160
|
+
}, [gestures]);
|
|
161
|
+
|
|
162
|
+
const onUpdate = useCallback(
|
|
163
|
+
(event: GestureUpdateEvent<PanGestureHandlerEventPayload>) => {
|
|
164
|
+
"worklet";
|
|
165
|
+
|
|
166
|
+
let gestureProgress = 0;
|
|
167
|
+
|
|
168
|
+
gestures.x.value = event.translationX;
|
|
169
|
+
gestures.y.value = event.translationY;
|
|
170
|
+
|
|
171
|
+
gestures.normalizedX.value = interpolate(
|
|
172
|
+
event.translationX,
|
|
173
|
+
[-dimensions.width, dimensions.width],
|
|
174
|
+
[-1, 1],
|
|
175
|
+
"clamp",
|
|
176
|
+
);
|
|
177
|
+
gestures.normalizedY.value = interpolate(
|
|
178
|
+
event.translationY,
|
|
179
|
+
[-dimensions.height, dimensions.height],
|
|
180
|
+
[-1, 1],
|
|
181
|
+
"clamp",
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
if (allowed.bidirectional) {
|
|
185
|
+
const distance = Math.sqrt(
|
|
186
|
+
event.translationX ** 2 + event.translationY ** 2,
|
|
187
|
+
);
|
|
188
|
+
gestureProgress = mapGestureToProgress(distance, dimensions.width);
|
|
189
|
+
} else {
|
|
190
|
+
let maxProgress = 0;
|
|
191
|
+
|
|
192
|
+
const allowedDown = allowed.vertical;
|
|
193
|
+
const allowedUp = allowed.verticalInverted;
|
|
194
|
+
const allowedRight = allowed.horizontal;
|
|
195
|
+
const allowedLeft = allowed.horizontalInverted;
|
|
196
|
+
|
|
197
|
+
if (allowedRight && event.translationX > 0) {
|
|
198
|
+
const currentProgress = mapGestureToProgress(
|
|
199
|
+
event.translationX,
|
|
200
|
+
dimensions.width,
|
|
201
|
+
);
|
|
202
|
+
maxProgress = Math.max(maxProgress, currentProgress);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (allowedLeft && event.translationX < 0) {
|
|
206
|
+
const currentProgress = mapGestureToProgress(
|
|
207
|
+
-event.translationX,
|
|
208
|
+
dimensions.width,
|
|
209
|
+
);
|
|
210
|
+
maxProgress = Math.max(maxProgress, currentProgress);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (allowedDown && event.translationY > 0) {
|
|
214
|
+
const currentProgress = mapGestureToProgress(
|
|
215
|
+
event.translationY,
|
|
216
|
+
dimensions.height,
|
|
217
|
+
);
|
|
218
|
+
maxProgress = Math.max(maxProgress, currentProgress);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (allowedUp && event.translationY < 0) {
|
|
222
|
+
const currentProgress = mapGestureToProgress(
|
|
223
|
+
-event.translationY,
|
|
224
|
+
dimensions.height,
|
|
225
|
+
);
|
|
226
|
+
maxProgress = Math.max(maxProgress, currentProgress);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
gestureProgress = maxProgress;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (gestureDrivesProgress) {
|
|
233
|
+
animations.progress.value = 1 - gestureProgress;
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
[dimensions, gestures, animations, gestureDrivesProgress, allowed],
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
const setNavigatorDismissal = useCallback(() => {
|
|
240
|
+
const key = current.navigation.getState().key;
|
|
241
|
+
|
|
242
|
+
NavigatorDismissState.set(key, true);
|
|
243
|
+
}, [current]);
|
|
244
|
+
|
|
245
|
+
const handleDismiss = useCallback(() => {
|
|
246
|
+
const key = current.navigation.getState().key;
|
|
247
|
+
current.navigation.goBack();
|
|
248
|
+
NavigatorDismissState.remove(key);
|
|
249
|
+
}, [current]);
|
|
250
|
+
|
|
251
|
+
const onEnd = useCallback(
|
|
252
|
+
(event: GestureStateChangeEvent<PanGestureHandlerEventPayload>) => {
|
|
253
|
+
"worklet";
|
|
254
|
+
|
|
255
|
+
const { translationX, translationY, velocityX, velocityY } = event;
|
|
256
|
+
|
|
257
|
+
// reminder: we should make this into an option
|
|
258
|
+
const dismissThreshold = 0.5;
|
|
259
|
+
|
|
260
|
+
const finalX = translationX + velocityX * gestureVelocityImpact;
|
|
261
|
+
const finalY = translationY + velocityY * gestureVelocityImpact;
|
|
262
|
+
|
|
263
|
+
const diagonal = Math.sqrt(
|
|
264
|
+
dimensions.width * dimensions.width +
|
|
265
|
+
dimensions.height * dimensions.height,
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
let shouldDismiss = false;
|
|
269
|
+
|
|
270
|
+
const horizontalDistance = Math.abs(finalX);
|
|
271
|
+
const verticalDistance = Math.abs(finalY);
|
|
272
|
+
const crossAxisThreshold = diagonal * dismissThreshold * 0.7;
|
|
273
|
+
|
|
274
|
+
if (allowed.bidirectional) {
|
|
275
|
+
// For bidirectional, use the original distance-based logic
|
|
276
|
+
const finalDistance = Math.sqrt(finalX * finalX + finalY * finalY);
|
|
277
|
+
shouldDismiss = finalDistance > diagonal * dismissThreshold;
|
|
278
|
+
} else {
|
|
279
|
+
// Check primary direction dismissal
|
|
280
|
+
if (allowed.vertical && finalY > 0) {
|
|
281
|
+
shouldDismiss = verticalDistance > diagonal * dismissThreshold;
|
|
282
|
+
} else if (allowed.verticalInverted && finalY < 0) {
|
|
283
|
+
shouldDismiss = verticalDistance > diagonal * dismissThreshold;
|
|
284
|
+
} else if (allowed.horizontal && finalX > 0) {
|
|
285
|
+
shouldDismiss = horizontalDistance > diagonal * dismissThreshold;
|
|
286
|
+
} else if (allowed.horizontalInverted && finalX < 0) {
|
|
287
|
+
shouldDismiss = horizontalDistance > diagonal * dismissThreshold;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Allow dismissal on perpendicular axis if movement is significant
|
|
291
|
+
if (!shouldDismiss) {
|
|
292
|
+
if (
|
|
293
|
+
(allowed.vertical || allowed.verticalInverted) &&
|
|
294
|
+
horizontalDistance > crossAxisThreshold
|
|
295
|
+
) {
|
|
296
|
+
shouldDismiss = true;
|
|
297
|
+
} else if (
|
|
298
|
+
(allowed.horizontal || allowed.horizontalInverted) &&
|
|
299
|
+
verticalDistance > crossAxisThreshold
|
|
300
|
+
) {
|
|
301
|
+
shouldDismiss = true;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
gestures.isDismissing.value = Number(shouldDismiss);
|
|
307
|
+
|
|
308
|
+
if (gestures.isDismissing.value) {
|
|
309
|
+
runOnJS(setNavigatorDismissal)();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
runTransition({
|
|
313
|
+
target: gestures.isDismissing.value ? "close" : "open",
|
|
314
|
+
spec: transitionSpec,
|
|
315
|
+
onFinish: gestures.isDismissing.value ? handleDismiss : undefined,
|
|
316
|
+
animations,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const spec = gestures.isDismissing.value
|
|
320
|
+
? transitionSpec?.close
|
|
321
|
+
: transitionSpec?.open;
|
|
322
|
+
|
|
323
|
+
gestures.x.value = animate(0, spec);
|
|
324
|
+
gestures.y.value = animate(0, spec);
|
|
325
|
+
gestures.normalizedX.value = animate(0, spec);
|
|
326
|
+
gestures.normalizedY.value = animate(0, spec);
|
|
327
|
+
gestures.isDragging.value = 0;
|
|
328
|
+
},
|
|
329
|
+
[
|
|
330
|
+
dimensions,
|
|
331
|
+
animations,
|
|
332
|
+
transitionSpec,
|
|
333
|
+
gestureVelocityImpact,
|
|
334
|
+
setNavigatorDismissal,
|
|
335
|
+
handleDismiss,
|
|
336
|
+
gestures,
|
|
337
|
+
allowed,
|
|
338
|
+
],
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
const panGesture = useMemo(
|
|
342
|
+
() =>
|
|
343
|
+
Gesture.Pan()
|
|
344
|
+
.enabled(gestureEnabled)
|
|
345
|
+
.manualActivation(true)
|
|
346
|
+
.onTouchesDown(onTouchesDown)
|
|
347
|
+
.onTouchesMove(onTouchesMove)
|
|
348
|
+
.onStart(onStart)
|
|
349
|
+
.onUpdate(onUpdate)
|
|
350
|
+
.onEnd(onEnd)
|
|
351
|
+
.blocksExternalGesture(nativeGesture),
|
|
352
|
+
[
|
|
353
|
+
gestureEnabled,
|
|
354
|
+
nativeGesture,
|
|
355
|
+
onTouchesDown,
|
|
356
|
+
onTouchesMove,
|
|
357
|
+
onStart,
|
|
358
|
+
onUpdate,
|
|
359
|
+
onEnd,
|
|
360
|
+
],
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
applyGestureActivationCriteria({
|
|
364
|
+
gestureDirection,
|
|
365
|
+
gestureResponseDistance,
|
|
366
|
+
panGesture,
|
|
367
|
+
});
|
|
368
|
+
return { panGesture, nativeGesture };
|
|
369
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import type { LayoutChangeEvent } from "react-native";
|
|
3
|
+
import { useAnimatedScrollHandler } from "react-native-reanimated";
|
|
4
|
+
import type { ReanimatedScrollEvent } from "react-native-reanimated/lib/typescript/hook/commonTypes";
|
|
5
|
+
import { useGestureContext } from "../../providers/gestures";
|
|
6
|
+
|
|
7
|
+
interface ScrollProgressHookProps {
|
|
8
|
+
onScroll?: (event: ReanimatedScrollEvent) => void;
|
|
9
|
+
onContentSizeChange?: (width: number, height: number) => void;
|
|
10
|
+
onLayout?: (event: LayoutChangeEvent) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const useScrollProgress = (props: ScrollProgressHookProps) => {
|
|
14
|
+
const { scrollProgress } = useGestureContext();
|
|
15
|
+
const scrollHandler = useAnimatedScrollHandler({
|
|
16
|
+
onScroll: (event) => {
|
|
17
|
+
props.onScroll?.(event);
|
|
18
|
+
scrollProgress.modify((v) => {
|
|
19
|
+
"worklet";
|
|
20
|
+
v.x = event.contentOffset.x;
|
|
21
|
+
v.y = event.contentOffset.y;
|
|
22
|
+
return v;
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const onContentSizeChange = useCallback(
|
|
28
|
+
(width: number, height: number) => {
|
|
29
|
+
props.onContentSizeChange?.(width, height);
|
|
30
|
+
|
|
31
|
+
scrollProgress.modify((v) => {
|
|
32
|
+
"worklet";
|
|
33
|
+
v.contentWidth = width;
|
|
34
|
+
v.contentHeight = height;
|
|
35
|
+
return v;
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
[scrollProgress, props.onContentSizeChange],
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const onLayout = useCallback(
|
|
42
|
+
(event: LayoutChangeEvent) => {
|
|
43
|
+
props.onLayout?.(event);
|
|
44
|
+
const { width, height } = event.nativeEvent.layout;
|
|
45
|
+
scrollProgress.modify((v) => {
|
|
46
|
+
"worklet";
|
|
47
|
+
v.layoutHeight = height;
|
|
48
|
+
v.layoutWidth = width;
|
|
49
|
+
return v;
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
[scrollProgress, props.onLayout],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
scrollHandler,
|
|
57
|
+
onContentSizeChange,
|
|
58
|
+
onLayout,
|
|
59
|
+
};
|
|
60
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
export default function useStableCallback<C extends (...args: any[]) => any>(
|
|
4
|
+
callback: C,
|
|
5
|
+
) {
|
|
6
|
+
const callbackRef = useRef(callback);
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
callbackRef.current = callback;
|
|
10
|
+
}, [callback]);
|
|
11
|
+
|
|
12
|
+
return useCallback((...args: Parameters<C>) => {
|
|
13
|
+
callbackRef.current(...args);
|
|
14
|
+
}, []);
|
|
15
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { FlatList, Pressable, ScrollView, View } from "react-native";
|
|
2
|
+
import { createTransitionAwareComponent } from "./components/create-transition-aware-component";
|
|
3
|
+
import { presets, specs } from "./configs";
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
View: createTransitionAwareComponent(View),
|
|
7
|
+
Pressable: createTransitionAwareComponent(Pressable),
|
|
8
|
+
ScrollView: createTransitionAwareComponent(ScrollView, {
|
|
9
|
+
isScrollable: true,
|
|
10
|
+
}),
|
|
11
|
+
FlatList: createTransitionAwareComponent(FlatList, {
|
|
12
|
+
isScrollable: true,
|
|
13
|
+
}),
|
|
14
|
+
presets,
|
|
15
|
+
specs,
|
|
16
|
+
createTransitionAwareComponent,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export { useScreenAnimation } from "./hooks/animation/use-screen-animation";
|
|
20
|
+
export { createNativeStackNavigator } from "./integrations/native-stack/navigators/createNativeStackNavigator";
|
|
21
|
+
|
|
22
|
+
export type {
|
|
23
|
+
NativeStackHeaderLeftProps,
|
|
24
|
+
NativeStackHeaderProps,
|
|
25
|
+
NativeStackHeaderRightProps,
|
|
26
|
+
NativeStackNavigationEventMap,
|
|
27
|
+
NativeStackNavigationOptions,
|
|
28
|
+
NativeStackNavigationProp,
|
|
29
|
+
NativeStackNavigatorProps,
|
|
30
|
+
NativeStackOptionsArgs,
|
|
31
|
+
NativeStackScreenProps,
|
|
32
|
+
} from "./types/navigator";
|