@momo-kits/carousel 0.165.2-beta.3 → 0.167.1-beta.1
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/animation.ts +62 -0
- package/index.tsx +455 -390
- package/package.json +5 -6
- package/types.ts +48 -123
- package/core/LICENSE +0 -21
- package/core/README.md +0 -16
- package/core/components/Carousel.tsx +0 -28
- package/core/components/CarouselLayout.tsx +0 -231
- package/core/components/ItemLayout.tsx +0 -215
- package/core/components/ItemRenderer.tsx +0 -126
- package/core/components/LazyView.tsx +0 -14
- package/core/components/ScrollViewGesture.tsx +0 -443
- package/core/constants/index.ts +0 -11
- package/core/hooks/useAutoPlay.ts +0 -63
- package/core/hooks/useCarouselController.tsx +0 -295
- package/core/hooks/useCheckMounted.ts +0 -14
- package/core/hooks/useCommonVariables.ts +0 -261
- package/core/hooks/useInitProps.ts +0 -177
- package/core/hooks/useLayoutConfig.ts +0 -35
- package/core/hooks/useOffsetX.ts +0 -81
- package/core/hooks/useOnProgressChange.ts +0 -33
- package/core/hooks/usePanGestureProxy.ts +0 -144
- package/core/hooks/usePropsErrorBoundary.ts +0 -41
- package/core/hooks/useUpdateGestureConfig.ts +0 -17
- package/core/hooks/useVisibleRanges.tsx +0 -141
- package/core/layouts/index.tsx +0 -10
- package/core/layouts/normal.ts +0 -32
- package/core/layouts/parallax.ts +0 -50
- package/core/layouts/stack.ts +0 -266
- package/core/public-types.ts +0 -209
- package/core/store/index.tsx +0 -63
- package/core/types.ts +0 -32
- package/core/utils/carousel-direction.ts +0 -39
- package/core/utils/carousel-math.ts +0 -156
- package/core/utils/compute-gesture-translation.ts +0 -29
- package/core/utils/compute-offset-if-size-changed.ts +0 -14
- package/core/utils/computed-with-auto-fill-data.ts +0 -92
- package/core/utils/deal-with-animation.ts +0 -19
- package/core/utils/log.ts +0 -12
- package/core/utils/sanitize-animation-style.ts +0 -20
package/index.tsx
CHANGED
|
@@ -9,483 +9,548 @@ import React, {
|
|
|
9
9
|
} from 'react';
|
|
10
10
|
import {
|
|
11
11
|
Dimensions,
|
|
12
|
-
|
|
12
|
+
FlatList,
|
|
13
|
+
GestureResponderEvent,
|
|
14
|
+
LayoutChangeEvent,
|
|
15
|
+
NativeScrollEvent,
|
|
16
|
+
NativeSyntheticEvent,
|
|
17
|
+
Platform,
|
|
13
18
|
StyleSheet,
|
|
14
19
|
View,
|
|
15
|
-
type LayoutChangeEvent,
|
|
16
|
-
type NativeScrollEvent,
|
|
17
|
-
type NativeSyntheticEvent,
|
|
18
20
|
} from 'react-native';
|
|
19
|
-
import {
|
|
21
|
+
import Animated, {
|
|
20
22
|
Extrapolation,
|
|
21
23
|
interpolate,
|
|
22
24
|
runOnJS,
|
|
25
|
+
useAnimatedScrollHandler,
|
|
26
|
+
useAnimatedStyle,
|
|
27
|
+
useSharedValue,
|
|
23
28
|
type SharedValue,
|
|
24
29
|
} from 'react-native-reanimated';
|
|
30
|
+
import { CarouselProps, CarouselRef } from './types';
|
|
31
|
+
|
|
32
|
+
const { width: viewportWidth } = Dimensions.get('window');
|
|
33
|
+
|
|
34
|
+
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
|
|
35
|
+
|
|
36
|
+
type CarouselItemProps = {
|
|
37
|
+
index: number;
|
|
38
|
+
itemWidth: number;
|
|
39
|
+
scrollX: SharedValue<number>;
|
|
40
|
+
inactiveSlideOpacity: number;
|
|
41
|
+
inactiveSlideScale: number;
|
|
42
|
+
slideStyle: any;
|
|
43
|
+
children: React.ReactNode;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const CarouselItem: React.FC<CarouselItemProps> = ({
|
|
47
|
+
index,
|
|
48
|
+
itemWidth,
|
|
49
|
+
scrollX,
|
|
50
|
+
inactiveSlideOpacity,
|
|
51
|
+
inactiveSlideScale,
|
|
52
|
+
slideStyle,
|
|
53
|
+
children,
|
|
54
|
+
}) => {
|
|
55
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
56
|
+
const inputRange = [
|
|
57
|
+
(index - 1) * itemWidth,
|
|
58
|
+
index * itemWidth,
|
|
59
|
+
(index + 1) * itemWidth,
|
|
60
|
+
];
|
|
61
|
+
const opacity =
|
|
62
|
+
inactiveSlideOpacity < 1
|
|
63
|
+
? interpolate(
|
|
64
|
+
scrollX.value,
|
|
65
|
+
inputRange,
|
|
66
|
+
[inactiveSlideOpacity, 1, inactiveSlideOpacity],
|
|
67
|
+
Extrapolation.CLAMP,
|
|
68
|
+
)
|
|
69
|
+
: 1;
|
|
70
|
+
const scale =
|
|
71
|
+
inactiveSlideScale < 1
|
|
72
|
+
? interpolate(
|
|
73
|
+
scrollX.value,
|
|
74
|
+
inputRange,
|
|
75
|
+
[inactiveSlideScale, 1, inactiveSlideScale],
|
|
76
|
+
Extrapolation.CLAMP,
|
|
77
|
+
)
|
|
78
|
+
: 1;
|
|
79
|
+
return {
|
|
80
|
+
opacity,
|
|
81
|
+
transform: [{ scale }],
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<Animated.View style={[{ width: itemWidth }, slideStyle, animatedStyle]}>
|
|
87
|
+
{children}
|
|
88
|
+
</Animated.View>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
25
91
|
|
|
26
|
-
|
|
27
|
-
import type {
|
|
28
|
-
CarouselItemAnimation,
|
|
29
|
-
CarouselPanGesture,
|
|
30
|
-
CarouselProps as CoreCarouselProps,
|
|
31
|
-
CarouselRef as CoreCarouselRef,
|
|
32
|
-
} from './core/public-types';
|
|
33
|
-
import type { CarouselProps, CarouselRef } from './types';
|
|
34
|
-
|
|
35
|
-
const { width: WINDOW_WIDTH } = Dimensions.get('window');
|
|
36
|
-
|
|
37
|
-
/** Gap the FlatList implementation left between items when `visibleItem > 1`. */
|
|
38
|
-
const MULTI_ITEM_SPACING = 10;
|
|
39
|
-
|
|
40
|
-
/** How long a `fireCallback: false` command suppresses `onSnapToItem`. */
|
|
41
|
-
const SUPPRESS_CALLBACK_WINDOW = 1000;
|
|
42
|
-
|
|
43
|
-
const IGNORED_LEGACY_PROPS = [
|
|
44
|
-
'loopClonesPerSide',
|
|
45
|
-
'snapToInterval',
|
|
46
|
-
'disableIntervalMomentum',
|
|
47
|
-
'getItemLayout',
|
|
48
|
-
'activeSlideOffset',
|
|
49
|
-
'onMomentumScrollEnd',
|
|
50
|
-
] as const;
|
|
51
|
-
|
|
52
|
-
function positiveModulo(value: number, divisor: number) {
|
|
53
|
-
if (divisor <= 0) return 0;
|
|
54
|
-
const remainder = value % divisor;
|
|
55
|
-
return remainder < 0 ? remainder + divisor : remainder;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function scrollEvent(x: number) {
|
|
59
|
-
return {
|
|
60
|
-
nativeEvent: { contentOffset: { x, y: 0 } },
|
|
61
|
-
} as NativeSyntheticEvent<NativeScrollEvent>;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const CarouselImpl = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
|
|
92
|
+
const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
|
|
65
93
|
const {
|
|
66
|
-
// engine props
|
|
67
94
|
data,
|
|
68
95
|
renderItem,
|
|
69
96
|
keyExtractor,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
autoplay = false,
|
|
73
|
-
autoplayInterval,
|
|
74
|
-
autoplayDirection,
|
|
75
|
-
orientation = 'horizontal',
|
|
76
|
-
itemSize,
|
|
77
|
-
scrollEnabled,
|
|
78
|
-
snapMode,
|
|
79
|
-
overscrollEnabled,
|
|
80
|
-
renderWindowSize,
|
|
81
|
-
animation,
|
|
82
|
-
progress,
|
|
83
|
-
onProgressChange,
|
|
84
|
-
scrollOffsetValue,
|
|
85
|
-
style,
|
|
86
|
-
contentContainerStyle,
|
|
87
|
-
testID,
|
|
88
|
-
onLayout,
|
|
89
|
-
onConfigurePanGesture,
|
|
90
|
-
onScrollStart,
|
|
91
|
-
onSnapToItem,
|
|
92
|
-
layout,
|
|
93
|
-
itemAnimation,
|
|
94
|
-
|
|
95
|
-
// legacy props
|
|
96
|
-
firstItem,
|
|
97
|
-
itemWidth,
|
|
97
|
+
firstItem = 0,
|
|
98
|
+
itemWidth: itemWidthProp,
|
|
98
99
|
visibleItem = 1,
|
|
99
100
|
full = false,
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
activeSlideOffset = 20,
|
|
102
|
+
scrollEnabled = true,
|
|
103
|
+
enableSnap = true,
|
|
104
|
+
snapToInterval: snapToIntervalProp,
|
|
105
|
+
disableIntervalMomentum = Platform.OS === 'android',
|
|
106
|
+
loop = false,
|
|
107
|
+
loopClonesPerSide = 3,
|
|
108
|
+
autoplay = false,
|
|
109
|
+
autoplayDelay = 1000,
|
|
110
|
+
autoplayInterval = 3000,
|
|
102
111
|
inactiveSlideOpacity = 1,
|
|
103
112
|
inactiveSlideScale = 1,
|
|
104
113
|
apparitionDelay = 0,
|
|
114
|
+
style,
|
|
105
115
|
slideStyle,
|
|
116
|
+
contentContainerStyle,
|
|
117
|
+
onSnapToItem,
|
|
106
118
|
onScrollIndexChanged,
|
|
107
|
-
onScroll,
|
|
108
|
-
|
|
109
|
-
|
|
119
|
+
onScroll: onScrollProp,
|
|
120
|
+
onMomentumScrollEnd: onMomentumScrollEndProp,
|
|
121
|
+
onTouchStart: onTouchStartProp,
|
|
122
|
+
onTouchEnd: onTouchEndProp,
|
|
123
|
+
onLayout: onLayoutProp,
|
|
124
|
+
getItemLayout: getItemLayoutProp,
|
|
110
125
|
} = props;
|
|
111
126
|
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
const [
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
flattenedStyle.flexGrow !== undefined;
|
|
128
|
-
|
|
129
|
-
const warnedRef = useRef(false);
|
|
130
|
-
if (__DEV__ && !warnedRef.current) {
|
|
131
|
-
warnedRef.current = true;
|
|
132
|
-
for (const name of IGNORED_LEGACY_PROPS) {
|
|
133
|
-
if (props[name] !== undefined) {
|
|
134
|
-
console.warn(
|
|
135
|
-
`[@momo-kits/carousel] \`${name}\` is no longer supported and is ignored.`
|
|
136
|
-
);
|
|
137
|
-
}
|
|
127
|
+
const flatListRef = useRef<FlatList>(null);
|
|
128
|
+
const autoplayTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
129
|
+
const scrollX = useSharedValue(0);
|
|
130
|
+
const containerWidthRef = useRef(viewportWidth);
|
|
131
|
+
const isAutoplayPausedRef = useRef(false);
|
|
132
|
+
const currentIndexRef = useRef(firstItem);
|
|
133
|
+
|
|
134
|
+
const [currentIndex, setCurrentIndex] = useState(firstItem);
|
|
135
|
+
const [containerWidth, setContainerWidth] = useState(viewportWidth);
|
|
136
|
+
const [isVisible, setIsVisible] = useState(apparitionDelay === 0);
|
|
137
|
+
const [isLayoutReady, setIsLayoutReady] = useState(false);
|
|
138
|
+
|
|
139
|
+
const itemWidth = useMemo(() => {
|
|
140
|
+
if (full) {
|
|
141
|
+
return containerWidth;
|
|
138
142
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
console.warn(
|
|
142
|
-
`[@momo-kits/carousel] \`${name}\` is called without an event object; the gesture handler has no GestureResponderEvent to pass.`
|
|
143
|
-
);
|
|
144
|
-
}
|
|
143
|
+
if (itemWidthProp) {
|
|
144
|
+
return itemWidthProp;
|
|
145
145
|
}
|
|
146
|
-
|
|
146
|
+
const spacing = visibleItem > 1 ? 10 : 0;
|
|
147
|
+
return (containerWidth - spacing * (visibleItem - 1)) / visibleItem;
|
|
148
|
+
}, [containerWidth, full, itemWidthProp, visibleItem]);
|
|
147
149
|
|
|
148
|
-
const
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
if (!full && visibleItem > 1) {
|
|
152
|
-
const size =
|
|
153
|
-
(containerSize - MULTI_ITEM_SPACING * (visibleItem - 1)) / visibleItem;
|
|
154
|
-
return size > 0 ? size : undefined;
|
|
150
|
+
const snapToInterval = useMemo(() => {
|
|
151
|
+
if (snapToIntervalProp !== undefined) {
|
|
152
|
+
return snapToIntervalProp;
|
|
155
153
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}, [containerSize, full, itemSize, itemWidth, visibleItem]);
|
|
159
|
-
|
|
160
|
-
/** Size the animations interpolate against. */
|
|
161
|
-
const pageSize = resolvedItemSize ?? containerSize;
|
|
162
|
-
|
|
163
|
-
const resolvedDefaultIndex = useMemo(() => {
|
|
164
|
-
const requested = defaultIndex ?? firstItem ?? 0;
|
|
165
|
-
return Number.isInteger(requested) &&
|
|
166
|
-
requested >= 0 &&
|
|
167
|
-
requested < data.length
|
|
168
|
-
? requested
|
|
169
|
-
: 0;
|
|
170
|
-
}, [data.length, defaultIndex, firstItem]);
|
|
171
|
-
|
|
172
|
-
const resolvedSnapMode = snapMode ?? (enableSnap === false ? 'none' : 'page');
|
|
173
|
-
|
|
174
|
-
const hasLegacyItemAnimation =
|
|
175
|
-
inactiveSlideOpacity < 1 || inactiveSlideScale < 1;
|
|
176
|
-
|
|
177
|
-
const legacyItemAnimation = useMemo<CarouselItemAnimation | undefined>(() => {
|
|
178
|
-
if (!hasLegacyItemAnimation) return undefined;
|
|
179
|
-
|
|
180
|
-
// An `itemAnimation` replaces the built-in layout, so it has to carry the
|
|
181
|
-
// slide translation as well as the legacy opacity/scale.
|
|
182
|
-
const size = pageSize;
|
|
183
|
-
const directionSign = I18nManager.isRTL ? -1 : 1;
|
|
184
|
-
const minOpacity = inactiveSlideOpacity;
|
|
185
|
-
const minScale = inactiveSlideScale;
|
|
186
|
-
|
|
187
|
-
return (relativeProgress: number) => {
|
|
188
|
-
'worklet';
|
|
189
|
-
const translate = interpolate(
|
|
190
|
-
relativeProgress,
|
|
191
|
-
[-1, 0, 1],
|
|
192
|
-
[-size, 0, size]
|
|
193
|
-
);
|
|
194
|
-
const opacity =
|
|
195
|
-
minOpacity < 1
|
|
196
|
-
? interpolate(
|
|
197
|
-
relativeProgress,
|
|
198
|
-
[-1, 0, 1],
|
|
199
|
-
[minOpacity, 1, minOpacity],
|
|
200
|
-
Extrapolation.CLAMP
|
|
201
|
-
)
|
|
202
|
-
: 1;
|
|
203
|
-
const scale =
|
|
204
|
-
minScale < 1
|
|
205
|
-
? interpolate(
|
|
206
|
-
relativeProgress,
|
|
207
|
-
[-1, 0, 1],
|
|
208
|
-
[minScale, 1, minScale],
|
|
209
|
-
Extrapolation.CLAMP
|
|
210
|
-
)
|
|
211
|
-
: 1;
|
|
154
|
+
return itemWidth;
|
|
155
|
+
}, [itemWidth, snapToIntervalProp]);
|
|
212
156
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
157
|
+
const dataWithClones = useMemo(() => {
|
|
158
|
+
if (!loop || data.length === 0) {
|
|
159
|
+
return data;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const clones = [];
|
|
163
|
+
for (let i = 0; i < loopClonesPerSide; i++) {
|
|
164
|
+
const startIndex = data.length - loopClonesPerSide + i;
|
|
165
|
+
const endIndex = i;
|
|
166
|
+
clones.push(data[startIndex >= 0 ? startIndex : data.length + startIndex]);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const result = [...clones, ...data];
|
|
170
|
+
|
|
171
|
+
for (let i = 0; i < loopClonesPerSide; i++) {
|
|
172
|
+
result.push(data[i % data.length]);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return result;
|
|
176
|
+
}, [data, loop, loopClonesPerSide]);
|
|
177
|
+
|
|
178
|
+
const getRealIndex = useCallback(
|
|
179
|
+
(index: number) => {
|
|
180
|
+
if (!loop) return index;
|
|
181
|
+
|
|
182
|
+
const dataLength = data.length;
|
|
183
|
+
if (dataLength === 0) return 0;
|
|
184
|
+
|
|
185
|
+
let realIndex = index - loopClonesPerSide;
|
|
186
|
+
|
|
187
|
+
if (realIndex < 0) {
|
|
188
|
+
realIndex = dataLength + realIndex;
|
|
189
|
+
} else if (realIndex >= dataLength) {
|
|
190
|
+
realIndex = realIndex % dataLength;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return realIndex;
|
|
240
194
|
},
|
|
241
|
-
[
|
|
195
|
+
[loop, loopClonesPerSide, data.length]
|
|
242
196
|
);
|
|
243
197
|
|
|
244
|
-
const
|
|
245
|
-
(
|
|
246
|
-
if (
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
198
|
+
const getLoopIndex = useCallback(
|
|
199
|
+
(realIndex: number) => {
|
|
200
|
+
if (!loop) return realIndex;
|
|
201
|
+
return realIndex + loopClonesPerSide;
|
|
202
|
+
},
|
|
203
|
+
[loop, loopClonesPerSide]
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const snapToItem = useCallback(
|
|
207
|
+
(index: number, animated = true, fireCallback = true) => {
|
|
208
|
+
if (!flatListRef.current || index < 0 || index >= data.length) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const loopIndex = getLoopIndex(index);
|
|
213
|
+
const offset = loopIndex * itemWidth;
|
|
214
|
+
|
|
215
|
+
flatListRef.current.scrollToOffset({
|
|
216
|
+
offset,
|
|
217
|
+
animated,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
currentIndexRef.current = index;
|
|
221
|
+
setCurrentIndex(index);
|
|
222
|
+
|
|
223
|
+
if (fireCallback && onSnapToItem) {
|
|
224
|
+
onSnapToItem(index);
|
|
251
225
|
}
|
|
252
226
|
},
|
|
253
|
-
[
|
|
227
|
+
[data.length, getLoopIndex, itemWidth, onSnapToItem]
|
|
254
228
|
);
|
|
255
229
|
|
|
256
|
-
const
|
|
257
|
-
(
|
|
258
|
-
|
|
230
|
+
const snapToNext = useCallback(
|
|
231
|
+
(animated = true, fireCallback = true) => {
|
|
232
|
+
const nextIndex = (currentIndexRef.current + 1) % data.length;
|
|
233
|
+
snapToItem(nextIndex, animated, fireCallback);
|
|
234
|
+
},
|
|
235
|
+
[data.length, snapToItem]
|
|
259
236
|
);
|
|
260
237
|
|
|
261
|
-
const
|
|
262
|
-
(
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
[
|
|
238
|
+
const snapToPrev = useCallback(
|
|
239
|
+
(animated = true, fireCallback = true) => {
|
|
240
|
+
const prevIndex =
|
|
241
|
+
currentIndexRef.current === 0 ? data.length - 1 : currentIndexRef.current - 1;
|
|
242
|
+
snapToItem(prevIndex, animated, fireCallback);
|
|
243
|
+
},
|
|
244
|
+
[data.length, snapToItem]
|
|
268
245
|
);
|
|
269
246
|
|
|
270
|
-
const
|
|
247
|
+
const stopAutoplay = useCallback(() => {
|
|
248
|
+
if (autoplayTimerRef.current) {
|
|
249
|
+
clearInterval(autoplayTimerRef.current);
|
|
250
|
+
autoplayTimerRef.current = null;
|
|
251
|
+
}
|
|
252
|
+
}, []);
|
|
271
253
|
|
|
272
|
-
const
|
|
254
|
+
const pauseAutoPlay = useCallback(() => {
|
|
255
|
+
isAutoplayPausedRef.current = true;
|
|
256
|
+
stopAutoplay();
|
|
257
|
+
}, [stopAutoplay]);
|
|
273
258
|
|
|
274
|
-
const
|
|
275
|
-
(
|
|
276
|
-
|
|
277
|
-
onScroll?.(scrollEvent(value * pageSize));
|
|
259
|
+
const startAutoplay = useCallback(() => {
|
|
260
|
+
stopAutoplay();
|
|
261
|
+
isAutoplayPausedRef.current = false;
|
|
278
262
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
263
|
+
if (!autoplay || data.length <= 1) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const startTimer = () => {
|
|
268
|
+
autoplayTimerRef.current = setInterval(() => {
|
|
269
|
+
if (!isAutoplayPausedRef.current) {
|
|
270
|
+
snapToNext(true, true);
|
|
284
271
|
}
|
|
272
|
+
}, autoplayInterval);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
if (autoplayDelay > 0) {
|
|
276
|
+
setTimeout(startTimer, autoplayDelay);
|
|
277
|
+
} else {
|
|
278
|
+
startTimer();
|
|
279
|
+
}
|
|
280
|
+
}, [autoplay, autoplayDelay, autoplayInterval, data.length, snapToNext, stopAutoplay]);
|
|
281
|
+
|
|
282
|
+
const handleLoopReposition = useCallback(
|
|
283
|
+
(index: number) => {
|
|
284
|
+
if (!loop || !flatListRef.current) return;
|
|
285
|
+
|
|
286
|
+
const dataLength = data.length;
|
|
287
|
+
if (dataLength === 0) return;
|
|
288
|
+
|
|
289
|
+
if (index < loopClonesPerSide) {
|
|
290
|
+
const realIndex = dataLength - (loopClonesPerSide - index);
|
|
291
|
+
const loopIndex = getLoopIndex(realIndex);
|
|
292
|
+
setTimeout(() => {
|
|
293
|
+
flatListRef.current?.scrollToOffset({
|
|
294
|
+
offset: loopIndex * itemWidth,
|
|
295
|
+
animated: false,
|
|
296
|
+
});
|
|
297
|
+
}, 50);
|
|
298
|
+
} else if (index >= dataLength + loopClonesPerSide) {
|
|
299
|
+
const realIndex = index - (dataLength + loopClonesPerSide);
|
|
300
|
+
const loopIndex = getLoopIndex(realIndex);
|
|
301
|
+
setTimeout(() => {
|
|
302
|
+
flatListRef.current?.scrollToOffset({
|
|
303
|
+
offset: loopIndex * itemWidth,
|
|
304
|
+
animated: false,
|
|
305
|
+
});
|
|
306
|
+
}, 50);
|
|
285
307
|
}
|
|
286
308
|
},
|
|
287
|
-
[data.length,
|
|
309
|
+
[loop, data.length, loopClonesPerSide, itemWidth, getLoopIndex]
|
|
288
310
|
);
|
|
289
311
|
|
|
290
|
-
const
|
|
291
|
-
(
|
|
292
|
-
|
|
312
|
+
const handleScroll = useCallback(
|
|
313
|
+
(offsetX: number) => {
|
|
314
|
+
const index = Math.round(offsetX / itemWidth);
|
|
315
|
+
const realIndex = getRealIndex(index);
|
|
293
316
|
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
|
|
317
|
+
if (realIndex !== currentIndexRef.current) {
|
|
318
|
+
currentIndexRef.current = realIndex;
|
|
319
|
+
setCurrentIndex(realIndex);
|
|
320
|
+
|
|
321
|
+
if (onScrollIndexChanged) {
|
|
322
|
+
onScrollIndexChanged(realIndex);
|
|
323
|
+
}
|
|
297
324
|
}
|
|
325
|
+
},
|
|
326
|
+
[itemWidth, getRealIndex, onScrollIndexChanged]
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const handleMomentumScrollEnd = useCallback(
|
|
330
|
+
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
331
|
+
const offsetX = event.nativeEvent.contentOffset.x;
|
|
332
|
+
const index = Math.round(offsetX / itemWidth);
|
|
333
|
+
const realIndex = getRealIndex(index);
|
|
298
334
|
|
|
299
|
-
|
|
335
|
+
currentIndexRef.current = realIndex;
|
|
336
|
+
setCurrentIndex(realIndex);
|
|
337
|
+
|
|
338
|
+
if (onSnapToItem) {
|
|
339
|
+
onSnapToItem(realIndex);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
handleLoopReposition(index);
|
|
343
|
+
|
|
344
|
+
if (onMomentumScrollEndProp) {
|
|
345
|
+
onMomentumScrollEndProp(event);
|
|
346
|
+
}
|
|
300
347
|
},
|
|
301
|
-
[onSnapToItem]
|
|
348
|
+
[itemWidth, getRealIndex, onSnapToItem, handleLoopReposition, onMomentumScrollEndProp]
|
|
302
349
|
);
|
|
303
350
|
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
351
|
+
const handleTouchStart = useCallback(
|
|
352
|
+
(event: any) => {
|
|
353
|
+
if (autoplay) {
|
|
354
|
+
pauseAutoPlay();
|
|
355
|
+
}
|
|
356
|
+
if (onTouchStartProp) {
|
|
357
|
+
onTouchStartProp(event);
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
[autoplay, pauseAutoPlay, onTouchStartProp]
|
|
308
361
|
);
|
|
309
362
|
|
|
310
|
-
const
|
|
311
|
-
(
|
|
312
|
-
if (
|
|
313
|
-
|
|
314
|
-
'worklet';
|
|
315
|
-
runOnJS(onTouchStart)();
|
|
316
|
-
});
|
|
363
|
+
const handleTouchEnd = useCallback(
|
|
364
|
+
(event: GestureResponderEvent) => {
|
|
365
|
+
if (autoplay && !isAutoplayPausedRef.current) {
|
|
366
|
+
startAutoplay();
|
|
317
367
|
}
|
|
318
|
-
if (
|
|
319
|
-
|
|
320
|
-
'worklet';
|
|
321
|
-
runOnJS(onTouchEnd)();
|
|
322
|
-
});
|
|
368
|
+
if (onTouchEndProp) {
|
|
369
|
+
onTouchEndProp(event);
|
|
323
370
|
}
|
|
324
|
-
onConfigurePanGesture?.(gesture);
|
|
325
371
|
},
|
|
326
|
-
[
|
|
372
|
+
[autoplay, startAutoplay, onTouchEndProp]
|
|
327
373
|
);
|
|
328
374
|
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
375
|
+
const handleLayout = useCallback(
|
|
376
|
+
(event: LayoutChangeEvent) => {
|
|
377
|
+
const { width } = event.nativeEvent.layout;
|
|
378
|
+
containerWidthRef.current = width;
|
|
379
|
+
setContainerWidth(width);
|
|
380
|
+
setIsLayoutReady(true);
|
|
335
381
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
382
|
+
if (onLayoutProp) {
|
|
383
|
+
onLayoutProp(event);
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
[onLayoutProp]
|
|
387
|
+
);
|
|
339
388
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
389
|
+
const getItemLayout = useCallback(
|
|
390
|
+
(data: any, index: number) => {
|
|
391
|
+
if (getItemLayoutProp) {
|
|
392
|
+
return getItemLayoutProp(data, index);
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
length: itemWidth,
|
|
396
|
+
offset: itemWidth * index,
|
|
397
|
+
index,
|
|
398
|
+
};
|
|
399
|
+
},
|
|
400
|
+
[itemWidth, getItemLayoutProp]
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
const renderAnimatedItem = useCallback(
|
|
404
|
+
({ item, index }: { item: any; index: number }) => {
|
|
405
|
+
const realIndex = getRealIndex(index);
|
|
406
|
+
const hasOpacityAnimation = inactiveSlideOpacity < 1;
|
|
407
|
+
const hasScaleAnimation = inactiveSlideScale < 1;
|
|
408
|
+
|
|
409
|
+
if (!hasOpacityAnimation && !hasScaleAnimation) {
|
|
410
|
+
return (
|
|
411
|
+
<View style={[{ width: itemWidth }, slideStyle]}>
|
|
412
|
+
{renderItem({ item, index: realIndex })}
|
|
413
|
+
</View>
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return (
|
|
418
|
+
<CarouselItem
|
|
419
|
+
index={index}
|
|
420
|
+
itemWidth={itemWidth}
|
|
421
|
+
scrollX={scrollX}
|
|
422
|
+
inactiveSlideOpacity={inactiveSlideOpacity}
|
|
423
|
+
inactiveSlideScale={inactiveSlideScale}
|
|
424
|
+
slideStyle={slideStyle}
|
|
425
|
+
>
|
|
426
|
+
{renderItem({ item, index: realIndex })}
|
|
427
|
+
</CarouselItem>
|
|
344
428
|
);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
429
|
+
},
|
|
430
|
+
[
|
|
431
|
+
getRealIndex,
|
|
432
|
+
inactiveSlideOpacity,
|
|
433
|
+
inactiveSlideScale,
|
|
434
|
+
itemWidth,
|
|
435
|
+
slideStyle,
|
|
436
|
+
renderItem,
|
|
437
|
+
scrollX,
|
|
438
|
+
]
|
|
439
|
+
);
|
|
349
440
|
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
441
|
+
const handleKeyExtractor = useCallback(
|
|
442
|
+
(item: any, index: number) => {
|
|
443
|
+
if (keyExtractor) {
|
|
444
|
+
return keyExtractor(item, index);
|
|
445
|
+
}
|
|
446
|
+
return `carousel-item-${index}`;
|
|
447
|
+
},
|
|
448
|
+
[keyExtractor]
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
const emitScrollProp = useCallback(
|
|
452
|
+
(offsetX: number) => {
|
|
453
|
+
if (onScrollProp) {
|
|
454
|
+
onScrollProp({
|
|
455
|
+
nativeEvent: { contentOffset: { x: offsetX, y: 0 } },
|
|
456
|
+
} as NativeSyntheticEvent<NativeScrollEvent>);
|
|
457
|
+
}
|
|
458
|
+
},
|
|
459
|
+
[onScrollProp],
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
const onScrollEvent = useAnimatedScrollHandler({
|
|
463
|
+
onScroll: (event) => {
|
|
464
|
+
scrollX.value = event.contentOffset.x;
|
|
465
|
+
runOnJS(handleScroll)(event.contentOffset.x);
|
|
466
|
+
runOnJS(emitScrollProp)(event.contentOffset.x);
|
|
467
|
+
},
|
|
468
|
+
});
|
|
354
469
|
|
|
355
470
|
useEffect(() => {
|
|
356
|
-
if (
|
|
357
|
-
|
|
471
|
+
if (isLayoutReady && firstItem > 0 && firstItem < data.length) {
|
|
472
|
+
setTimeout(() => {
|
|
473
|
+
snapToItem(firstItem, false, false);
|
|
474
|
+
}, 50);
|
|
475
|
+
}
|
|
476
|
+
}, [isLayoutReady, firstItem, data.length, snapToItem]);
|
|
358
477
|
|
|
359
|
-
|
|
360
|
-
|
|
478
|
+
useEffect(() => {
|
|
479
|
+
if (autoplay && data.length > 1) {
|
|
480
|
+
startAutoplay();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return () => {
|
|
484
|
+
stopAutoplay();
|
|
485
|
+
};
|
|
486
|
+
}, [autoplay, data.length, startAutoplay, stopAutoplay]);
|
|
361
487
|
|
|
362
488
|
useEffect(() => {
|
|
363
|
-
if (apparitionDelay
|
|
489
|
+
if (apparitionDelay > 0) {
|
|
490
|
+
const timer = setTimeout(() => {
|
|
491
|
+
setIsVisible(true);
|
|
492
|
+
}, apparitionDelay);
|
|
364
493
|
|
|
365
|
-
|
|
366
|
-
|
|
494
|
+
return () => clearTimeout(timer);
|
|
495
|
+
}
|
|
367
496
|
}, [apparitionDelay]);
|
|
368
497
|
|
|
369
|
-
const suppressNextCallback = useCallback(() => {
|
|
370
|
-
suppressUntilRef.current = Date.now() + SUPPRESS_CALLBACK_WINDOW;
|
|
371
|
-
}, []);
|
|
372
|
-
|
|
373
498
|
useImperativeHandle(
|
|
374
499
|
ref,
|
|
375
500
|
() => ({
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
getCurrentIndex: () => coreRef.current?.getCurrentIndex() ?? 0,
|
|
380
|
-
|
|
381
|
-
snapToItem: (index, animated = true, fireCallback = true) => {
|
|
382
|
-
if (!fireCallback) suppressNextCallback();
|
|
383
|
-
coreRef.current?.scrollTo({ index, animated });
|
|
384
|
-
},
|
|
385
|
-
snapToNext: (animated = true, fireCallback = true) => {
|
|
386
|
-
if (!fireCallback) suppressNextCallback();
|
|
387
|
-
coreRef.current?.next({ animated });
|
|
388
|
-
},
|
|
389
|
-
snapToPrev: (animated = true, fireCallback = true) => {
|
|
390
|
-
if (!fireCallback) suppressNextCallback();
|
|
391
|
-
coreRef.current?.prev({ animated });
|
|
392
|
-
},
|
|
501
|
+
snapToItem,
|
|
502
|
+
snapToNext,
|
|
503
|
+
snapToPrev,
|
|
393
504
|
startAutoplay,
|
|
394
|
-
pauseAutoplay,
|
|
395
|
-
stopAutoplay
|
|
505
|
+
pauseAutoplay: pauseAutoPlay,
|
|
506
|
+
stopAutoplay,
|
|
396
507
|
}),
|
|
397
|
-
[
|
|
508
|
+
[snapToItem, snapToNext, snapToPrev, startAutoplay, pauseAutoPlay, stopAutoplay] // eslint-disable-line react-hooks/exhaustive-deps
|
|
398
509
|
);
|
|
399
510
|
|
|
400
|
-
// A FlatList grows into the space it is given and otherwise takes the height
|
|
401
|
-
// of its content. Items are absolutely positioned here, so both halves of
|
|
402
|
-
// that have to be restored when the caller sets no size of its own.
|
|
403
|
-
const resolvedStyle = hasExplicitSize
|
|
404
|
-
? style
|
|
405
|
-
: [
|
|
406
|
-
styles.autoSize,
|
|
407
|
-
measuredItemSize === null
|
|
408
|
-
? null
|
|
409
|
-
: { [isVertical ? 'minWidth' : 'minHeight']: measuredItemSize },
|
|
410
|
-
style,
|
|
411
|
-
];
|
|
412
|
-
|
|
413
511
|
if (!isVisible) {
|
|
414
|
-
return <View style={
|
|
512
|
+
return <View style={[styles.container, style]} />;
|
|
415
513
|
}
|
|
416
514
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
? { itemAnimation: resolvedItemAnimation }
|
|
446
|
-
: null),
|
|
447
|
-
...(needsProgress ? { onProgressChange: handleProgressChange } : null),
|
|
448
|
-
...(needsGestureConfig
|
|
449
|
-
? { onConfigurePanGesture: configurePanGesture }
|
|
450
|
-
: null),
|
|
451
|
-
} as CoreCarouselProps<any>;
|
|
452
|
-
|
|
453
|
-
return <CoreCarousel ref={coreRef} {...coreProps} />;
|
|
515
|
+
return (
|
|
516
|
+
<View style={[styles.container, style]} onLayout={handleLayout}>
|
|
517
|
+
<AnimatedFlatList
|
|
518
|
+
ref={flatListRef}
|
|
519
|
+
data={dataWithClones}
|
|
520
|
+
renderItem={renderAnimatedItem}
|
|
521
|
+
keyExtractor={handleKeyExtractor}
|
|
522
|
+
horizontal
|
|
523
|
+
showsHorizontalScrollIndicator={false}
|
|
524
|
+
scrollEnabled={scrollEnabled}
|
|
525
|
+
scrollEventThrottle={16}
|
|
526
|
+
onScroll={onScrollEvent}
|
|
527
|
+
onMomentumScrollEnd={handleMomentumScrollEnd}
|
|
528
|
+
onTouchStart={handleTouchStart}
|
|
529
|
+
onTouchEnd={handleTouchEnd}
|
|
530
|
+
snapToInterval={enableSnap ? (snapToInterval as number) : undefined}
|
|
531
|
+
snapToAlignment={enableSnap ? 'start' : undefined}
|
|
532
|
+
decelerationRate={enableSnap ? 'fast' : 'normal'}
|
|
533
|
+
disableIntervalMomentum={disableIntervalMomentum}
|
|
534
|
+
getItemLayout={getItemLayout}
|
|
535
|
+
contentContainerStyle={contentContainerStyle}
|
|
536
|
+
removeClippedSubviews={false}
|
|
537
|
+
initialNumToRender={Math.min(10, dataWithClones.length)}
|
|
538
|
+
maxToRenderPerBatch={5}
|
|
539
|
+
windowSize={5}
|
|
540
|
+
/>
|
|
541
|
+
</View>
|
|
542
|
+
);
|
|
454
543
|
});
|
|
455
544
|
|
|
456
545
|
const styles = StyleSheet.create({
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
flexShrink: 1,
|
|
460
|
-
},
|
|
461
|
-
slideAuto: {
|
|
462
|
-
width: '100%',
|
|
463
|
-
},
|
|
464
|
-
slideFill: {
|
|
465
|
-
width: '100%',
|
|
466
|
-
height: '100%',
|
|
546
|
+
container: {
|
|
547
|
+
overflow: 'hidden',
|
|
467
548
|
},
|
|
468
549
|
});
|
|
469
550
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
type CarouselComponent = <Item>(
|
|
473
|
-
props: CarouselProps<Item> & { ref?: React.Ref<CarouselRef> }
|
|
474
|
-
) => React.ReactElement;
|
|
551
|
+
Carousel.displayName = 'Carousel';
|
|
475
552
|
|
|
476
|
-
export const Carousel = CarouselImpl as CarouselComponent;
|
|
477
553
|
export default Carousel;
|
|
554
|
+
export { Carousel };
|
|
555
|
+
export type { CarouselProps, CarouselRef };
|
|
478
556
|
|
|
479
|
-
export type {
|
|
480
|
-
CarouselAnimation,
|
|
481
|
-
CarouselItemAnimation,
|
|
482
|
-
CarouselLayout,
|
|
483
|
-
CarouselPanGesture,
|
|
484
|
-
CarouselProgressChangeHandler,
|
|
485
|
-
CarouselProps,
|
|
486
|
-
CarouselRef,
|
|
487
|
-
CarouselRenderItem,
|
|
488
|
-
CarouselRenderItemInfo,
|
|
489
|
-
CarouselScrollToOptions,
|
|
490
|
-
CarouselStepOptions,
|
|
491
|
-
} from './types';
|