react-native-reanimated-carousel 3.3.1 → 3.3.2
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 +3 -2
- package/src/Carousel.tsx +247 -0
- package/src/LazyView.tsx +14 -0
- package/src/ScrollViewGesture.tsx +328 -0
- package/src/constants/index.ts +16 -0
- package/src/hooks/computeNewIndexWhenDataChanges.ts +51 -0
- package/src/hooks/index.test.ts +80 -0
- package/src/hooks/useAutoPlay.ts +63 -0
- package/src/hooks/useCarouselController.tsx +317 -0
- package/src/hooks/useCheckMounted.ts +14 -0
- package/src/hooks/useCommonVariables.ts +73 -0
- package/src/hooks/useInitProps.ts +98 -0
- package/src/hooks/useLayoutConfig.ts +28 -0
- package/src/hooks/useOffsetX.ts +88 -0
- package/src/hooks/useOnProgressChange.ts +50 -0
- package/src/hooks/usePropsErrorBoundary.ts +31 -0
- package/src/hooks/useVisibleRanges.tsx +49 -0
- package/src/index.tsx +8 -0
- package/src/layouts/BaseLayout.tsx +124 -0
- package/src/layouts/ParallaxLayout.tsx +142 -0
- package/src/layouts/index.tsx +12 -0
- package/src/layouts/normal.ts +22 -0
- package/src/layouts/parallax.ts +89 -0
- package/src/layouts/stack.ts +362 -0
- package/src/store/index.ts +13 -0
- package/src/types.ts +237 -0
- package/src/utils/computedWithAutoFillData.ts +94 -0
- package/src/utils/dealWithAnimation.ts +19 -0
- package/src/utils/handlerOffsetDirection.ts +15 -0
- package/src/utils/log.ts +13 -0
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-reanimated-carousel",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.2",
|
|
4
4
|
"description": "Simple carousel component.fully implemented using Reanimated 2.Infinitely scrolling, very smooth.",
|
|
5
5
|
"main": "lib/commonjs/index",
|
|
6
6
|
"react-native": "src/index.tsx",
|
|
7
7
|
"types": "lib/typescript/index.d.ts",
|
|
8
8
|
"source": "src/index",
|
|
9
9
|
"files": [
|
|
10
|
-
"lib"
|
|
10
|
+
"lib",
|
|
11
|
+
"src"
|
|
11
12
|
],
|
|
12
13
|
"scripts": {
|
|
13
14
|
"gif": "node scripts/makegif.js ./scripts/gif-works-directory",
|
package/src/Carousel.tsx
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-use-before-define */
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { StyleSheet } from "react-native";
|
|
4
|
+
import { runOnJS, useDerivedValue } from "react-native-reanimated";
|
|
5
|
+
|
|
6
|
+
import { useAutoPlay } from "./hooks/useAutoPlay";
|
|
7
|
+
import { useCarouselController } from "./hooks/useCarouselController";
|
|
8
|
+
import { useCommonVariables } from "./hooks/useCommonVariables";
|
|
9
|
+
import { useInitProps } from "./hooks/useInitProps";
|
|
10
|
+
import { useLayoutConfig } from "./hooks/useLayoutConfig";
|
|
11
|
+
import { useOnProgressChange } from "./hooks/useOnProgressChange";
|
|
12
|
+
import { usePropsErrorBoundary } from "./hooks/usePropsErrorBoundary";
|
|
13
|
+
import { useVisibleRanges } from "./hooks/useVisibleRanges";
|
|
14
|
+
import { BaseLayout } from "./layouts/BaseLayout";
|
|
15
|
+
import { ScrollViewGesture } from "./ScrollViewGesture";
|
|
16
|
+
import { CTX } from "./store";
|
|
17
|
+
import type { ICarouselInstance, TCarouselProps } from "./types";
|
|
18
|
+
import { computedRealIndexWithAutoFillData } from "./utils/computedWithAutoFillData";
|
|
19
|
+
|
|
20
|
+
const Carousel = React.forwardRef<ICarouselInstance, TCarouselProps<any>>(
|
|
21
|
+
(_props, ref) => {
|
|
22
|
+
const props = useInitProps(_props);
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
testID,
|
|
26
|
+
loop,
|
|
27
|
+
autoFillData,
|
|
28
|
+
// Fill data with autoFillData
|
|
29
|
+
data,
|
|
30
|
+
// Length of fill data
|
|
31
|
+
dataLength,
|
|
32
|
+
// Raw data that has not been processed
|
|
33
|
+
rawData,
|
|
34
|
+
// Length of raw data
|
|
35
|
+
rawDataLength,
|
|
36
|
+
mode,
|
|
37
|
+
style,
|
|
38
|
+
width,
|
|
39
|
+
height,
|
|
40
|
+
vertical,
|
|
41
|
+
autoPlay,
|
|
42
|
+
windowSize,
|
|
43
|
+
autoPlayReverse,
|
|
44
|
+
autoPlayInterval,
|
|
45
|
+
scrollAnimationDuration,
|
|
46
|
+
withAnimation,
|
|
47
|
+
renderItem,
|
|
48
|
+
onScrollEnd,
|
|
49
|
+
onSnapToItem,
|
|
50
|
+
onScrollBegin,
|
|
51
|
+
onProgressChange,
|
|
52
|
+
customAnimation,
|
|
53
|
+
defaultIndex,
|
|
54
|
+
} = props;
|
|
55
|
+
|
|
56
|
+
const commonVariables = useCommonVariables(props);
|
|
57
|
+
const { size, handlerOffset } = commonVariables;
|
|
58
|
+
|
|
59
|
+
const offsetX = useDerivedValue(() => {
|
|
60
|
+
const totalSize = size * dataLength;
|
|
61
|
+
const x = handlerOffset.value % totalSize;
|
|
62
|
+
|
|
63
|
+
if (!loop)
|
|
64
|
+
return handlerOffset.value;
|
|
65
|
+
|
|
66
|
+
return isNaN(x) ? 0 : x;
|
|
67
|
+
}, [loop, size, dataLength]);
|
|
68
|
+
|
|
69
|
+
usePropsErrorBoundary({ ...props, dataLength });
|
|
70
|
+
useOnProgressChange({
|
|
71
|
+
autoFillData,
|
|
72
|
+
loop,
|
|
73
|
+
size,
|
|
74
|
+
offsetX,
|
|
75
|
+
rawDataLength,
|
|
76
|
+
onProgressChange,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const carouselController = useCarouselController({
|
|
80
|
+
loop,
|
|
81
|
+
size,
|
|
82
|
+
dataLength,
|
|
83
|
+
autoFillData,
|
|
84
|
+
handlerOffset,
|
|
85
|
+
withAnimation,
|
|
86
|
+
defaultIndex,
|
|
87
|
+
onScrollEnd: () => runOnJS(_onScrollEnd)(),
|
|
88
|
+
onScrollBegin: () => !!onScrollBegin && runOnJS(onScrollBegin)(),
|
|
89
|
+
duration: scrollAnimationDuration,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const { next, prev, scrollTo, getSharedIndex, getCurrentIndex }
|
|
93
|
+
= carouselController;
|
|
94
|
+
|
|
95
|
+
const { start: startAutoPlay, pause: pauseAutoPlay } = useAutoPlay({
|
|
96
|
+
autoPlay,
|
|
97
|
+
autoPlayInterval,
|
|
98
|
+
autoPlayReverse,
|
|
99
|
+
carouselController,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const _onScrollEnd = React.useCallback(() => {
|
|
103
|
+
const _sharedIndex = Math.round(getSharedIndex());
|
|
104
|
+
|
|
105
|
+
const realIndex = computedRealIndexWithAutoFillData({
|
|
106
|
+
index: _sharedIndex,
|
|
107
|
+
dataLength: rawDataLength,
|
|
108
|
+
loop,
|
|
109
|
+
autoFillData,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (onSnapToItem)
|
|
113
|
+
onSnapToItem(realIndex);
|
|
114
|
+
|
|
115
|
+
if (onScrollEnd)
|
|
116
|
+
onScrollEnd(realIndex);
|
|
117
|
+
}, [
|
|
118
|
+
loop,
|
|
119
|
+
autoFillData,
|
|
120
|
+
rawDataLength,
|
|
121
|
+
getSharedIndex,
|
|
122
|
+
onSnapToItem,
|
|
123
|
+
onScrollEnd,
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
const scrollViewGestureOnScrollBegin = React.useCallback(() => {
|
|
127
|
+
pauseAutoPlay();
|
|
128
|
+
onScrollBegin?.();
|
|
129
|
+
}, [onScrollBegin, pauseAutoPlay]);
|
|
130
|
+
|
|
131
|
+
const scrollViewGestureOnScrollEnd = React.useCallback(() => {
|
|
132
|
+
startAutoPlay();
|
|
133
|
+
_onScrollEnd();
|
|
134
|
+
}, [_onScrollEnd, startAutoPlay]);
|
|
135
|
+
|
|
136
|
+
const scrollViewGestureOnTouchBegin = React.useCallback(pauseAutoPlay, [
|
|
137
|
+
pauseAutoPlay,
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
const scrollViewGestureOnTouchEnd = React.useCallback(startAutoPlay, [
|
|
141
|
+
startAutoPlay,
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
React.useImperativeHandle(
|
|
145
|
+
ref,
|
|
146
|
+
() => ({
|
|
147
|
+
next,
|
|
148
|
+
prev,
|
|
149
|
+
getCurrentIndex,
|
|
150
|
+
scrollTo,
|
|
151
|
+
}),
|
|
152
|
+
[getCurrentIndex, next, prev, scrollTo],
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const visibleRanges = useVisibleRanges({
|
|
156
|
+
total: dataLength,
|
|
157
|
+
viewSize: size,
|
|
158
|
+
translation: handlerOffset,
|
|
159
|
+
windowSize,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const layoutConfig = useLayoutConfig({ ...props, size });
|
|
163
|
+
|
|
164
|
+
const renderLayout = React.useCallback(
|
|
165
|
+
(item: any, i: number) => {
|
|
166
|
+
const realIndex = computedRealIndexWithAutoFillData({
|
|
167
|
+
index: i,
|
|
168
|
+
dataLength: rawDataLength,
|
|
169
|
+
loop,
|
|
170
|
+
autoFillData,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<BaseLayout
|
|
175
|
+
key={i}
|
|
176
|
+
index={i}
|
|
177
|
+
handlerOffset={offsetX}
|
|
178
|
+
visibleRanges={visibleRanges}
|
|
179
|
+
animationStyle={customAnimation || layoutConfig}
|
|
180
|
+
>
|
|
181
|
+
{({ animationValue }) =>
|
|
182
|
+
renderItem({
|
|
183
|
+
item,
|
|
184
|
+
index: realIndex,
|
|
185
|
+
animationValue,
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
</BaseLayout>
|
|
189
|
+
);
|
|
190
|
+
},
|
|
191
|
+
[
|
|
192
|
+
loop,
|
|
193
|
+
rawData,
|
|
194
|
+
offsetX,
|
|
195
|
+
visibleRanges,
|
|
196
|
+
autoFillData,
|
|
197
|
+
renderItem,
|
|
198
|
+
layoutConfig,
|
|
199
|
+
customAnimation,
|
|
200
|
+
],
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<CTX.Provider value={{ props, common: commonVariables }}>
|
|
205
|
+
<ScrollViewGesture
|
|
206
|
+
key={mode}
|
|
207
|
+
size={size}
|
|
208
|
+
translation={handlerOffset}
|
|
209
|
+
style={[
|
|
210
|
+
styles.container,
|
|
211
|
+
{
|
|
212
|
+
width: width || "100%",
|
|
213
|
+
height: height || "100%",
|
|
214
|
+
},
|
|
215
|
+
style,
|
|
216
|
+
vertical
|
|
217
|
+
? styles.itemsVertical
|
|
218
|
+
: styles.itemsHorizontal,
|
|
219
|
+
]}
|
|
220
|
+
testID={testID}
|
|
221
|
+
onScrollBegin={scrollViewGestureOnScrollBegin}
|
|
222
|
+
onScrollEnd={scrollViewGestureOnScrollEnd}
|
|
223
|
+
onTouchBegin={scrollViewGestureOnTouchBegin}
|
|
224
|
+
onTouchEnd={scrollViewGestureOnTouchEnd}
|
|
225
|
+
>
|
|
226
|
+
{data.map(renderLayout)}
|
|
227
|
+
</ScrollViewGesture>
|
|
228
|
+
</CTX.Provider>
|
|
229
|
+
);
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
export default Carousel as <T extends any>(
|
|
234
|
+
props: React.PropsWithChildren<TCarouselProps<T>>
|
|
235
|
+
) => React.ReactElement;
|
|
236
|
+
|
|
237
|
+
const styles = StyleSheet.create({
|
|
238
|
+
container: {
|
|
239
|
+
overflow: "hidden",
|
|
240
|
+
},
|
|
241
|
+
itemsHorizontal: {
|
|
242
|
+
flexDirection: "row",
|
|
243
|
+
},
|
|
244
|
+
itemsVertical: {
|
|
245
|
+
flexDirection: "column",
|
|
246
|
+
},
|
|
247
|
+
});
|
package/src/LazyView.tsx
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
shouldUpdate: boolean
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const LazyView: React.FC<Props> = (props) => {
|
|
8
|
+
const { shouldUpdate, children } = props;
|
|
9
|
+
|
|
10
|
+
if (!shouldUpdate)
|
|
11
|
+
return <></>;
|
|
12
|
+
|
|
13
|
+
return <>{children}</>;
|
|
14
|
+
};
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { StyleProp, ViewStyle } from "react-native";
|
|
3
|
+
import type { PanGestureHandlerGestureEvent } from "react-native-gesture-handler";
|
|
4
|
+
import {
|
|
5
|
+
PanGestureHandler,
|
|
6
|
+
} from "react-native-gesture-handler";
|
|
7
|
+
import Animated, {
|
|
8
|
+
cancelAnimation,
|
|
9
|
+
measure,
|
|
10
|
+
runOnJS,
|
|
11
|
+
useAnimatedGestureHandler,
|
|
12
|
+
useAnimatedReaction,
|
|
13
|
+
useAnimatedRef,
|
|
14
|
+
useDerivedValue,
|
|
15
|
+
useSharedValue,
|
|
16
|
+
withDecay,
|
|
17
|
+
} from "react-native-reanimated";
|
|
18
|
+
|
|
19
|
+
import { Easing } from "./constants";
|
|
20
|
+
import { CTX } from "./store";
|
|
21
|
+
import type { WithTimingAnimation } from "./types";
|
|
22
|
+
import { dealWithAnimation } from "./utils/dealWithAnimation";
|
|
23
|
+
|
|
24
|
+
interface GestureContext extends Record<string, unknown> {
|
|
25
|
+
validStart: boolean
|
|
26
|
+
panOffset: number
|
|
27
|
+
max: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface Props {
|
|
31
|
+
size: number
|
|
32
|
+
infinite?: boolean
|
|
33
|
+
testID?: string
|
|
34
|
+
style?: StyleProp<ViewStyle>
|
|
35
|
+
onScrollBegin?: () => void
|
|
36
|
+
onScrollEnd?: () => void
|
|
37
|
+
onTouchBegin?: () => void
|
|
38
|
+
onTouchEnd?: () => void
|
|
39
|
+
translation: Animated.SharedValue<number>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const IScrollViewGesture: React.FC<Props> = (props) => {
|
|
43
|
+
const {
|
|
44
|
+
props: {
|
|
45
|
+
vertical,
|
|
46
|
+
pagingEnabled,
|
|
47
|
+
snapEnabled,
|
|
48
|
+
panGestureHandlerProps,
|
|
49
|
+
loop: infinite,
|
|
50
|
+
scrollAnimationDuration,
|
|
51
|
+
withAnimation,
|
|
52
|
+
enabled,
|
|
53
|
+
dataLength,
|
|
54
|
+
overscrollEnabled,
|
|
55
|
+
},
|
|
56
|
+
} = React.useContext(CTX);
|
|
57
|
+
|
|
58
|
+
const {
|
|
59
|
+
size,
|
|
60
|
+
translation,
|
|
61
|
+
testID,
|
|
62
|
+
style = {},
|
|
63
|
+
onScrollBegin,
|
|
64
|
+
onScrollEnd,
|
|
65
|
+
onTouchBegin,
|
|
66
|
+
onTouchEnd,
|
|
67
|
+
} = props;
|
|
68
|
+
|
|
69
|
+
const maxPage = dataLength;
|
|
70
|
+
const isHorizontal = useDerivedValue(() => !vertical, [vertical]);
|
|
71
|
+
const touching = useSharedValue(false);
|
|
72
|
+
const scrollEndTranslation = useSharedValue(0);
|
|
73
|
+
const scrollEndVelocity = useSharedValue(0);
|
|
74
|
+
const containerRef = useAnimatedRef<Animated.View>();
|
|
75
|
+
|
|
76
|
+
// Get the limit of the scroll.
|
|
77
|
+
const getLimit = React.useCallback(() => {
|
|
78
|
+
"worklet";
|
|
79
|
+
|
|
80
|
+
if (!infinite && !overscrollEnabled) {
|
|
81
|
+
const { width: containerWidth = 0 } = measure(containerRef);
|
|
82
|
+
|
|
83
|
+
// If the item's total width is less than the container's width, then there is no need to scroll.
|
|
84
|
+
if (dataLength * size < containerWidth)
|
|
85
|
+
return 0;
|
|
86
|
+
|
|
87
|
+
// Disable the "overscroll" effect
|
|
88
|
+
return dataLength * size - containerWidth;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return dataLength * size;
|
|
92
|
+
}, [infinite, size, dataLength, overscrollEnabled]);
|
|
93
|
+
|
|
94
|
+
const withSpring = React.useCallback(
|
|
95
|
+
(toValue: number, onFinished?: () => void) => {
|
|
96
|
+
"worklet";
|
|
97
|
+
const defaultWithAnimation: WithTimingAnimation = {
|
|
98
|
+
type: "timing",
|
|
99
|
+
config: {
|
|
100
|
+
duration: scrollAnimationDuration + 100,
|
|
101
|
+
easing: Easing.easeOutQuart,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return dealWithAnimation(withAnimation ?? defaultWithAnimation)(
|
|
106
|
+
toValue,
|
|
107
|
+
(isFinished: boolean) => {
|
|
108
|
+
"worklet";
|
|
109
|
+
if (isFinished)
|
|
110
|
+
onFinished && runOnJS(onFinished)();
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
},
|
|
114
|
+
[scrollAnimationDuration, withAnimation],
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const endWithSpring = React.useCallback(
|
|
118
|
+
(onFinished?: () => void) => {
|
|
119
|
+
"worklet";
|
|
120
|
+
const origin = translation.value;
|
|
121
|
+
const velocity = scrollEndVelocity.value;
|
|
122
|
+
// Default to scroll in the direction of the slide (with deceleration)
|
|
123
|
+
let finalTranslation: number = withDecay({ velocity, deceleration: 0.999 });
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The page size is the same as the item size.
|
|
127
|
+
* If direction is vertical, the page size is the height of the item.
|
|
128
|
+
* If direction is horizontal, the page size is the width of the item.
|
|
129
|
+
*
|
|
130
|
+
* `page size` equals to `size` variable.
|
|
131
|
+
* */
|
|
132
|
+
if (pagingEnabled) {
|
|
133
|
+
// distance with direction
|
|
134
|
+
const offset = -(scrollEndTranslation.value >= 0 ? 1 : -1); // 1 or -1
|
|
135
|
+
const computed = offset < 0 ? Math.ceil : Math.floor;
|
|
136
|
+
const page = computed(-translation.value / size);
|
|
137
|
+
|
|
138
|
+
if (infinite) {
|
|
139
|
+
const finalPage = page + offset;
|
|
140
|
+
finalTranslation = withSpring(withProcessTranslation(-finalPage * size), onFinished);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
const finalPage = Math.min(maxPage - 1, Math.max(0, page + offset));
|
|
144
|
+
finalTranslation = withSpring(withProcessTranslation(-finalPage * size), onFinished);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!pagingEnabled && snapEnabled) {
|
|
149
|
+
// scroll to the nearest item
|
|
150
|
+
const nextPage = Math.round((origin + velocity * 0.4) / size) * size;
|
|
151
|
+
finalTranslation = withSpring(withProcessTranslation(nextPage), onFinished);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
translation.value = finalTranslation;
|
|
155
|
+
|
|
156
|
+
function withProcessTranslation(translation: number) {
|
|
157
|
+
if (!infinite && !overscrollEnabled) {
|
|
158
|
+
const limit = getLimit();
|
|
159
|
+
const sign = Math.sign(translation);
|
|
160
|
+
return sign * Math.max(0, Math.min(limit, Math.abs(translation)));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return translation;
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
[
|
|
167
|
+
translation,
|
|
168
|
+
scrollEndVelocity.value,
|
|
169
|
+
pagingEnabled,
|
|
170
|
+
size,
|
|
171
|
+
scrollEndTranslation.value,
|
|
172
|
+
infinite,
|
|
173
|
+
withSpring,
|
|
174
|
+
snapEnabled,
|
|
175
|
+
maxPage,
|
|
176
|
+
],
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const onFinish = React.useCallback(
|
|
180
|
+
(isFinished: boolean) => {
|
|
181
|
+
"worklet";
|
|
182
|
+
if (isFinished) {
|
|
183
|
+
touching.value = false;
|
|
184
|
+
onScrollEnd && runOnJS(onScrollEnd)();
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
[onScrollEnd, touching],
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const activeDecay = React.useCallback(() => {
|
|
191
|
+
"worklet";
|
|
192
|
+
touching.value = true;
|
|
193
|
+
translation.value = withDecay(
|
|
194
|
+
{ velocity: scrollEndVelocity.value },
|
|
195
|
+
isFinished => onFinish(isFinished as boolean),
|
|
196
|
+
);
|
|
197
|
+
}, [onFinish, scrollEndVelocity.value, touching, translation]);
|
|
198
|
+
|
|
199
|
+
const resetBoundary = React.useCallback(() => {
|
|
200
|
+
"worklet";
|
|
201
|
+
if (touching.value)
|
|
202
|
+
return;
|
|
203
|
+
|
|
204
|
+
if (translation.value > 0) {
|
|
205
|
+
if (scrollEndTranslation.value < 0) {
|
|
206
|
+
activeDecay();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (!infinite) {
|
|
210
|
+
translation.value = withSpring(0);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (translation.value < -((maxPage - 1) * size)) {
|
|
216
|
+
if (scrollEndTranslation.value > 0) {
|
|
217
|
+
activeDecay();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (!infinite)
|
|
221
|
+
translation.value = withSpring(-((maxPage - 1) * size));
|
|
222
|
+
}
|
|
223
|
+
}, [
|
|
224
|
+
touching.value,
|
|
225
|
+
translation,
|
|
226
|
+
maxPage,
|
|
227
|
+
size,
|
|
228
|
+
scrollEndTranslation.value,
|
|
229
|
+
infinite,
|
|
230
|
+
activeDecay,
|
|
231
|
+
withSpring,
|
|
232
|
+
]);
|
|
233
|
+
|
|
234
|
+
useAnimatedReaction(
|
|
235
|
+
() => translation.value,
|
|
236
|
+
() => {
|
|
237
|
+
if (!pagingEnabled)
|
|
238
|
+
resetBoundary();
|
|
239
|
+
},
|
|
240
|
+
[pagingEnabled, resetBoundary],
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const panGestureEventHandler = useAnimatedGestureHandler<
|
|
244
|
+
PanGestureHandlerGestureEvent,
|
|
245
|
+
GestureContext
|
|
246
|
+
>(
|
|
247
|
+
{
|
|
248
|
+
onStart: (_, ctx) => {
|
|
249
|
+
touching.value = true;
|
|
250
|
+
ctx.validStart = true;
|
|
251
|
+
onScrollBegin && runOnJS(onScrollBegin)();
|
|
252
|
+
|
|
253
|
+
ctx.max = (maxPage - 1) * size;
|
|
254
|
+
if (!infinite && !overscrollEnabled)
|
|
255
|
+
ctx.max = getLimit();
|
|
256
|
+
|
|
257
|
+
ctx.panOffset = translation.value;
|
|
258
|
+
},
|
|
259
|
+
onActive: (e, ctx) => {
|
|
260
|
+
if (ctx.validStart) {
|
|
261
|
+
ctx.validStart = false;
|
|
262
|
+
cancelAnimation(translation);
|
|
263
|
+
}
|
|
264
|
+
touching.value = true;
|
|
265
|
+
const { translationX, translationY } = e;
|
|
266
|
+
const panTranslation = isHorizontal.value
|
|
267
|
+
? translationX
|
|
268
|
+
: translationY;
|
|
269
|
+
if (!infinite) {
|
|
270
|
+
if ((translation.value > 0 || translation.value < -ctx.max)) {
|
|
271
|
+
const boundary = translation.value > 0 ? 0 : -ctx.max;
|
|
272
|
+
const fixed = boundary - ctx.panOffset;
|
|
273
|
+
const dynamic = panTranslation - fixed;
|
|
274
|
+
translation.value = boundary + dynamic * 0.5;
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const translationValue = ctx.panOffset + panTranslation;
|
|
280
|
+
translation.value = translationValue;
|
|
281
|
+
},
|
|
282
|
+
onEnd: (e) => {
|
|
283
|
+
const { velocityX, velocityY, translationX, translationY } = e;
|
|
284
|
+
scrollEndVelocity.value = isHorizontal.value
|
|
285
|
+
? velocityX
|
|
286
|
+
: velocityY;
|
|
287
|
+
scrollEndTranslation.value = isHorizontal.value
|
|
288
|
+
? translationX
|
|
289
|
+
: translationY;
|
|
290
|
+
|
|
291
|
+
endWithSpring(onScrollEnd);
|
|
292
|
+
|
|
293
|
+
if (!infinite)
|
|
294
|
+
touching.value = false;
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
[
|
|
298
|
+
pagingEnabled,
|
|
299
|
+
isHorizontal.value,
|
|
300
|
+
infinite,
|
|
301
|
+
maxPage,
|
|
302
|
+
size,
|
|
303
|
+
snapEnabled,
|
|
304
|
+
onScrollBegin,
|
|
305
|
+
onScrollEnd,
|
|
306
|
+
],
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
return (
|
|
310
|
+
<PanGestureHandler
|
|
311
|
+
{...panGestureHandlerProps}
|
|
312
|
+
enabled={enabled}
|
|
313
|
+
onGestureEvent={panGestureEventHandler}
|
|
314
|
+
>
|
|
315
|
+
<Animated.View
|
|
316
|
+
ref={containerRef}
|
|
317
|
+
testID={testID}
|
|
318
|
+
style={style}
|
|
319
|
+
onTouchStart={onTouchBegin}
|
|
320
|
+
onTouchEnd={onTouchEnd}
|
|
321
|
+
>
|
|
322
|
+
{props.children}
|
|
323
|
+
</Animated.View>
|
|
324
|
+
</PanGestureHandler>
|
|
325
|
+
);
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
export const ScrollViewGesture = IScrollViewGesture;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type Animated from "react-native-reanimated";
|
|
2
|
+
import { Easing as _Easing } from "react-native-reanimated";
|
|
3
|
+
|
|
4
|
+
export enum DATA_LENGTH {
|
|
5
|
+
SINGLE_ITEM = 1,
|
|
6
|
+
DOUBLE_ITEM = 2,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const Easing = {
|
|
10
|
+
easeOutQuart: _Easing.bezier(
|
|
11
|
+
0.25,
|
|
12
|
+
1,
|
|
13
|
+
0.5,
|
|
14
|
+
1,
|
|
15
|
+
) as unknown as Animated.EasingFunction,
|
|
16
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function omitZero(a: number, b: number) {
|
|
2
|
+
"worklet";
|
|
3
|
+
if (a === 0)
|
|
4
|
+
return 0;
|
|
5
|
+
|
|
6
|
+
return b;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function computeNewIndexWhenDataChanges(params: {
|
|
10
|
+
direction: number
|
|
11
|
+
handlerOffset: number
|
|
12
|
+
size: number
|
|
13
|
+
previousLength: number
|
|
14
|
+
currentLength: number
|
|
15
|
+
}) {
|
|
16
|
+
"worklet";
|
|
17
|
+
const { direction, handlerOffset: _handlerOffset, size, previousLength, currentLength } = params;
|
|
18
|
+
|
|
19
|
+
let handlerOffset = _handlerOffset;
|
|
20
|
+
let positionIndex;
|
|
21
|
+
let round;
|
|
22
|
+
|
|
23
|
+
const isPositive = direction < 0;
|
|
24
|
+
|
|
25
|
+
if (isPositive) {
|
|
26
|
+
positionIndex = (Math.abs(handlerOffset)) / size;
|
|
27
|
+
round = parseInt(String(omitZero(previousLength, positionIndex / previousLength)));
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
positionIndex = (Math.abs(handlerOffset) - size) / size;
|
|
31
|
+
round = parseInt(String(omitZero(previousLength, positionIndex / previousLength))) + 1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const prevOffset = omitZero(previousLength, positionIndex % previousLength);
|
|
35
|
+
const prevIndex = isPositive ? prevOffset : previousLength - prevOffset - 1;
|
|
36
|
+
const changedLength = round * (currentLength - previousLength);
|
|
37
|
+
const changedOffset = changedLength * size;
|
|
38
|
+
if (prevIndex > currentLength - 1 && currentLength < previousLength) {
|
|
39
|
+
if (isPositive)
|
|
40
|
+
handlerOffset = (currentLength - 1) * size * direction;
|
|
41
|
+
|
|
42
|
+
else
|
|
43
|
+
handlerOffset = (currentLength - 1) * size * -1;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
handlerOffset += changedOffset * direction;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return handlerOffset;
|
|
50
|
+
}
|
|
51
|
+
|