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 CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "react-native-reanimated-carousel",
3
- "version": "3.3.1",
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",
@@ -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
+ });
@@ -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
+