;
577 | pageBuffer: number;
578 | };
579 |
580 | const PageWrapper = React.memo(
581 | ({
582 | index,
583 | pageAnim,
584 | pageWidth,
585 | pageHeight,
586 | vertical,
587 | PageComponent,
588 | renderPage,
589 | isActive,
590 | style,
591 | pageInterpolator,
592 | pageBuffer,
593 | initialIndex,
594 | }: PageWrapperProps) => {
595 | const pageSize = vertical ? pageHeight : pageWidth;
596 |
597 | const translation = useDerivedValue(() => {
598 | const translate = (index - pageAnim.value) * pageSize.value;
599 | return translate;
600 | }, []);
601 |
602 | const focusAnim = useDerivedValue(() => {
603 | if (!pageSize.value) {
604 | return index - initialIndex;
605 | }
606 | return translation.value / pageSize.value;
607 | }, [initialIndex]);
608 |
609 | const animStyle = useAnimatedStyle(() => {
610 | // Short circuit page interpolation to prevent buggy initial values due to possible race condition:
611 | // https://github.com/software-mansion/react-native-reanimated/issues/2571
612 | const isInitialPage = index === initialIndex;
613 | const hasInitialized = !!pageSize.value;
614 | const isInactivePageBeforeInit = !isInitialPage && !hasInitialized;
615 | const _pageWidth = isInactivePageBeforeInit ? preInitSize : pageWidth;
616 | const _pageHeight = isInactivePageBeforeInit ? preInitSize : pageHeight;
617 | return pageInterpolator({
618 | focusAnim,
619 | pageAnim,
620 | pageWidth: _pageWidth,
621 | pageHeight: _pageHeight,
622 | index,
623 | vertical,
624 | pageBuffer,
625 | });
626 | }, [
627 | pageWidth,
628 | pageHeight,
629 | pageAnim,
630 | index,
631 | initialIndex,
632 | translation,
633 | vertical,
634 | pageBuffer,
635 | ]);
636 |
637 | if (PageComponent && renderPage) {
638 | console.warn(
639 | "PageComponent and renderPage both defined, defaulting to PageComponent"
640 | );
641 | }
642 |
643 | if (!PageComponent && !renderPage) {
644 | throw new Error("Either PageComponent or renderPage must be defined.");
645 | }
646 |
647 | return (
648 |
657 | {PageComponent ? (
658 |
666 | ) : (
667 | renderPage?.({
668 | index,
669 | isActive,
670 | focusAnim,
671 | pageWidthAnim: pageWidth,
672 | pageHeightAnim: pageHeight,
673 | pageAnim,
674 | })
675 | )}
676 |
677 | );
678 | }
679 | );
680 |
681 | export default React.memo(withWrappedProvider(React.forwardRef(InfinitePager)));
682 |
683 | function withWrappedProvider(
684 | Inner: React.ComponentType
685 | ) {
686 | return React.forwardRef((props: P, ref: React.ForwardedRef) => {
687 | return (
688 |
689 |
690 |
691 | );
692 | });
693 | }
694 |
695 | const styles = StyleSheet.create({
696 | pageWrapper: {
697 | left: 0,
698 | right: 0,
699 | top: 0,
700 | bottom: 0,
701 | position: "absolute",
702 | },
703 | activePage: {
704 | position: "relative",
705 | },
706 | });
707 |
708 | const InfinitePagerContext = React.createContext({
709 | activePagers: makeMutable([] as string[]) as SharedValue,
710 | pagers: makeMutable([] as string[]) as SharedValue,
711 | nestingDepth: -1,
712 | });
713 |
714 | const SimultaneousGestureContext = React.createContext(
715 | [] as SimultaneousGesture[]
716 | );
717 |
718 | function SimultaneousGestureProvider({
719 | simultaneousGestures = EMPTY_SIMULTANEOUS_GESTURES,
720 | children,
721 | }: {
722 | simultaneousGestures?: SimultaneousGesture[];
723 | children: React.ReactNode;
724 | }) {
725 | return (
726 |
727 | {children}
728 |
729 | );
730 | }
731 |
732 | function InfinitePagerProvider({ children }: { children: React.ReactNode }) {
733 | const { nestingDepth, activePagers, pagers } =
734 | useContext(InfinitePagerContext);
735 | const rootPagers = useSharedValue([]);
736 | const rootActivePagers = useSharedValue([]);
737 |
738 | const value = useMemo(() => {
739 | const isRoot = nestingDepth === -1;
740 |
741 | return {
742 | nestingDepth: nestingDepth + 1,
743 | activePagers: isRoot ? rootActivePagers : activePagers,
744 | pagers: isRoot ? rootPagers : pagers,
745 | };
746 | }, [nestingDepth, activePagers, pagers, rootPagers, rootActivePagers]);
747 |
748 | return (
749 |
750 | {children}
751 |
752 | );
753 | }
754 |
--------------------------------------------------------------------------------
/src/pageInterpolators.ts:
--------------------------------------------------------------------------------
1 | import { Platform } from "react-native";
2 | import {
3 | Extrapolate,
4 | interpolate,
5 | useAnimatedStyle,
6 | } from "react-native-reanimated";
7 | import { PageInterpolatorParams } from ".";
8 |
9 | export function pageInterpolatorSlide({
10 | focusAnim,
11 | pageWidth,
12 | pageHeight,
13 | vertical,
14 | }: PageInterpolatorParams): ReturnType {
15 | "worklet";
16 |
17 | const translateX = vertical
18 | ? 0
19 | : interpolate(
20 | focusAnim.value,
21 | [-1, 0, 1],
22 | [-pageWidth.value, 0, pageWidth.value]
23 | );
24 | const translateY = vertical
25 | ? interpolate(
26 | focusAnim.value,
27 | [-1, 0, 1],
28 | [-pageHeight.value, 0, pageHeight.value]
29 | )
30 | : 0;
31 |
32 | return {
33 | transform: [{ translateX }, { translateY }],
34 | };
35 | }
36 |
37 | export function pageInterpolatorCube({
38 | focusAnim,
39 | pageWidth,
40 | pageHeight,
41 | vertical,
42 | }: PageInterpolatorParams) {
43 | "worklet";
44 |
45 | const size = vertical ? pageHeight.value : pageWidth.value;
46 |
47 | // FIXME: how to calculate this programatically?
48 | const ratio = Platform.OS === "android" ? 1.23 : 2;
49 | const perspective = size;
50 |
51 | const angle = Math.atan(perspective / (size / 2));
52 |
53 | const inputVal = interpolate(focusAnim.value, [-1, 1], [1, -1]);
54 | const inputRange = [-1, 1];
55 |
56 | const translate = interpolate(
57 | inputVal,
58 | inputRange,
59 | [size / ratio, -size / ratio],
60 | Extrapolate.CLAMP
61 | );
62 |
63 | const rotate = interpolate(
64 | inputVal,
65 | inputRange,
66 | [angle, -angle],
67 | Extrapolate.CLAMP
68 | );
69 |
70 | const translate1 = interpolate(
71 | inputVal,
72 | inputRange,
73 | [size / 2, -size / 2],
74 | Extrapolate.CLAMP
75 | );
76 |
77 | const extra = size / ratio / Math.cos(angle / 2) - size / ratio;
78 | const translate2 = interpolate(
79 | inputVal,
80 | inputRange,
81 | [-extra, extra],
82 | Extrapolate.CLAMP
83 | );
84 |
85 | return {
86 | transform: vertical
87 | ? [
88 | { perspective },
89 | { translateY: translate },
90 | { rotateX: `${-rotate}rad` },
91 | { translateY: translate1 },
92 | { translateY: translate2 },
93 | ]
94 | : [
95 | { perspective },
96 | { translateX: translate },
97 | { rotateY: `${rotate}rad` },
98 | { translateX: translate1 },
99 | { translateX: translate2 },
100 | ],
101 | opacity: interpolate(inputVal, [-2, -1, 0, 1, 2], [0, 0.9, 1, 0.9, 0]),
102 | };
103 | }
104 |
105 | export function pageInterpolatorStack({
106 | focusAnim,
107 | pageWidth,
108 | pageHeight,
109 | pageBuffer,
110 | vertical,
111 | }: PageInterpolatorParams) {
112 | "worklet";
113 |
114 | const translateX = interpolate(
115 | focusAnim.value,
116 | [-1, 0, 1],
117 | vertical ? [10, 0, -10] : [-pageWidth.value * 1.3, 0, -10]
118 | );
119 |
120 | const translateY = interpolate(
121 | focusAnim.value,
122 | [-0.5, 0, 1],
123 | vertical ? [-pageHeight.value * 1.3, 0, -10] : [10, 0, -10]
124 | );
125 |
126 | const opacity = interpolate(
127 | focusAnim.value,
128 | [-pageBuffer, -pageBuffer + 1, 0, pageBuffer - 1, pageBuffer],
129 | [0, 1, 1, 1, 1]
130 | );
131 |
132 | const scale = interpolate(
133 | focusAnim.value,
134 | [-pageBuffer, -pageBuffer + 1, 0, pageBuffer - 1, pageBuffer],
135 | [0.1, 0.9, 0.9, 0.9, 0.1]
136 | );
137 |
138 | return {
139 | transform: [{ translateX }, { translateY }, { scale }],
140 | opacity,
141 | };
142 | }
143 |
144 | export function pageInterpolatorTurnIn({
145 | focusAnim,
146 | pageWidth,
147 | pageHeight,
148 | vertical,
149 | }: PageInterpolatorParams) {
150 | "worklet";
151 |
152 | const translateX = interpolate(
153 | focusAnim.value,
154 | [-1, 0, 1],
155 | vertical ? [0, 0, 0] : [-pageWidth.value * 0.4, 0, pageWidth.value * 0.4]
156 | );
157 |
158 | const translateY = interpolate(
159 | focusAnim.value,
160 | [-1, 0, 1],
161 | vertical ? [-pageHeight.value * 0.5, 0, pageHeight.value * 0.5] : [0, 0, 0]
162 | );
163 |
164 | const scale = interpolate(focusAnim.value, [-1, 0, 1], [0.4, 0.5, 0.4]);
165 |
166 | const rotateY = interpolate(
167 | focusAnim.value,
168 | [-1, 1],
169 | vertical ? [0, 0] : [75, -75],
170 | Extrapolate.CLAMP
171 | );
172 | const rotateX = interpolate(
173 | focusAnim.value,
174 | [-1, 1],
175 | vertical ? [-75, 75] : [0, 0],
176 | Extrapolate.CLAMP
177 | );
178 |
179 | return {
180 | transform: [
181 | { perspective: 1000 },
182 | { translateX },
183 | { translateY },
184 | { rotateY: `${rotateY}deg` },
185 | { rotateX: `${rotateX}deg` },
186 | { scale },
187 | ],
188 | };
189 | }
190 |
191 | export const defaultPageInterpolator = pageInterpolatorSlide;
192 |
--------------------------------------------------------------------------------
/src/useStableCallback.ts:
--------------------------------------------------------------------------------
1 | // Utility hook that returns a function that never has stale dependencies, but
2 | // without changing identity, as a useCallback with dep array would.
3 | // Useful for functions that depend on external state, but
4 | // should not trigger effects when that external state changes.
5 |
6 | import { useCallback, useRef } from "react";
7 |
8 | export function useStableCallback<
9 | T extends (arg1?: any, arg2?: any, arg3?: any) => any
10 | >(cb: T) {
11 | const cbRef = useRef(cb);
12 | cbRef.current = cb;
13 | const stableCb = useCallback(
14 | (...args: Parameters) => cbRef.current(...args),
15 | []
16 | );
17 | return stableCb as T;
18 | }
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "jsx": "react",
5 | "lib": ["esnext"],
6 | "module": "esnext",
7 | "moduleResolution": "node",
8 | "noFallthroughCasesInSwitch": true,
9 | "noImplicitReturns": true,
10 | "noImplicitUseStrict": false,
11 | "noStrictGenericChecks": false,
12 | "resolveJsonModule": true,
13 | "skipLibCheck": true,
14 | "strict": true,
15 | "target": "esnext"
16 | },
17 | "exclude": ["node_modules"],
18 | "include": ["src"]
19 | }
20 |
--------------------------------------------------------------------------------