@momo-kits/carousel 0.165.2-beta.1 → 0.165.2-beta.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/core/LICENSE +21 -0
- package/core/README.md +16 -0
- package/core/components/Carousel.tsx +28 -0
- package/core/components/CarouselLayout.tsx +231 -0
- package/core/components/ItemLayout.tsx +215 -0
- package/core/components/ItemRenderer.tsx +126 -0
- package/core/components/LazyView.tsx +14 -0
- package/core/components/ScrollViewGesture.tsx +443 -0
- package/core/constants/index.ts +11 -0
- package/core/hooks/useAutoPlay.ts +63 -0
- package/core/hooks/useCarouselController.tsx +295 -0
- package/core/hooks/useCheckMounted.ts +14 -0
- package/core/hooks/useCommonVariables.ts +261 -0
- package/core/hooks/useInitProps.ts +177 -0
- package/core/hooks/useLayoutConfig.ts +35 -0
- package/core/hooks/useOffsetX.ts +81 -0
- package/core/hooks/useOnProgressChange.ts +33 -0
- package/core/hooks/usePanGestureProxy.ts +144 -0
- package/core/hooks/usePropsErrorBoundary.ts +41 -0
- package/core/hooks/useUpdateGestureConfig.ts +17 -0
- package/core/hooks/useVisibleRanges.tsx +141 -0
- package/core/layouts/index.tsx +10 -0
- package/core/layouts/normal.ts +32 -0
- package/core/layouts/parallax.ts +50 -0
- package/core/layouts/stack.ts +266 -0
- package/core/public-types.ts +209 -0
- package/core/store/index.tsx +63 -0
- package/core/types.ts +32 -0
- package/core/utils/carousel-direction.ts +39 -0
- package/core/utils/carousel-math.ts +156 -0
- package/core/utils/compute-gesture-translation.ts +29 -0
- package/core/utils/compute-offset-if-size-changed.ts +14 -0
- package/core/utils/computed-with-auto-fill-data.ts +92 -0
- package/core/utils/deal-with-animation.ts +19 -0
- package/core/utils/log.ts +12 -0
- package/core/utils/sanitize-animation-style.ts +20 -0
- package/index.tsx +390 -455
- package/package.json +6 -5
- package/types.ts +123 -48
- package/animation.ts +0 -62
package/index.tsx
CHANGED
|
@@ -9,548 +9,483 @@ import React, {
|
|
|
9
9
|
} from 'react';
|
|
10
10
|
import {
|
|
11
11
|
Dimensions,
|
|
12
|
-
|
|
13
|
-
GestureResponderEvent,
|
|
14
|
-
LayoutChangeEvent,
|
|
15
|
-
NativeScrollEvent,
|
|
16
|
-
NativeSyntheticEvent,
|
|
17
|
-
Platform,
|
|
12
|
+
I18nManager,
|
|
18
13
|
StyleSheet,
|
|
19
14
|
View,
|
|
15
|
+
type LayoutChangeEvent,
|
|
16
|
+
type NativeScrollEvent,
|
|
17
|
+
type NativeSyntheticEvent,
|
|
20
18
|
} from 'react-native';
|
|
21
|
-
import
|
|
19
|
+
import {
|
|
22
20
|
Extrapolation,
|
|
23
21
|
interpolate,
|
|
24
22
|
runOnJS,
|
|
25
|
-
useAnimatedScrollHandler,
|
|
26
|
-
useAnimatedStyle,
|
|
27
|
-
useSharedValue,
|
|
28
23
|
type SharedValue,
|
|
29
24
|
} 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
|
-
};
|
|
91
25
|
|
|
92
|
-
|
|
26
|
+
import { Carousel as CoreCarousel } from './core/components/Carousel';
|
|
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) => {
|
|
93
65
|
const {
|
|
66
|
+
// engine props
|
|
94
67
|
data,
|
|
95
68
|
renderItem,
|
|
96
69
|
keyExtractor,
|
|
97
|
-
|
|
98
|
-
|
|
70
|
+
defaultIndex,
|
|
71
|
+
loop,
|
|
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,
|
|
99
98
|
visibleItem = 1,
|
|
100
99
|
full = false,
|
|
101
|
-
|
|
102
|
-
|
|
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,
|
|
100
|
+
enableSnap,
|
|
101
|
+
autoplayDelay = 0,
|
|
111
102
|
inactiveSlideOpacity = 1,
|
|
112
103
|
inactiveSlideScale = 1,
|
|
113
104
|
apparitionDelay = 0,
|
|
114
|
-
style,
|
|
115
105
|
slideStyle,
|
|
116
|
-
contentContainerStyle,
|
|
117
|
-
onSnapToItem,
|
|
118
106
|
onScrollIndexChanged,
|
|
119
|
-
onScroll
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
onTouchEnd: onTouchEndProp,
|
|
123
|
-
onLayout: onLayoutProp,
|
|
124
|
-
getItemLayout: getItemLayoutProp,
|
|
107
|
+
onScroll,
|
|
108
|
+
onTouchStart,
|
|
109
|
+
onTouchEnd,
|
|
125
110
|
} = props;
|
|
126
111
|
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
const [
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
return itemWidth;
|
|
155
|
-
}, [itemWidth, snapToIntervalProp]);
|
|
156
|
-
|
|
157
|
-
const dataWithClones = useMemo(() => {
|
|
158
|
-
if (!loop || data.length === 0) {
|
|
159
|
-
return data;
|
|
112
|
+
const coreRef = useRef<CoreCarouselRef>(null);
|
|
113
|
+
const suppressUntilRef = useRef(0);
|
|
114
|
+
const autoplayDelayTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
115
|
+
|
|
116
|
+
const [containerSize, setContainerSize] = useState(WINDOW_WIDTH);
|
|
117
|
+
const [measuredItemSize, setMeasuredItemSize] = useState<number | null>(null);
|
|
118
|
+
const [isVisible, setIsVisible] = useState(apparitionDelay <= 0);
|
|
119
|
+
const [isAutoplayRunning, setIsAutoplayRunning] = useState(false);
|
|
120
|
+
|
|
121
|
+
const isVertical = orientation === 'vertical';
|
|
122
|
+
const flattenedStyle = StyleSheet.flatten(style) ?? {};
|
|
123
|
+
const sizeField = isVertical ? 'width' : 'height';
|
|
124
|
+
const hasExplicitSize =
|
|
125
|
+
flattenedStyle[sizeField] !== undefined ||
|
|
126
|
+
flattenedStyle.flex !== undefined ||
|
|
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
|
+
}
|
|
160
138
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
139
|
+
for (const name of ['onTouchStart', 'onTouchEnd'] as const) {
|
|
140
|
+
if (props[name] && props[name]!.length > 0) {
|
|
141
|
+
console.warn(
|
|
142
|
+
`[@momo-kits/carousel] \`${name}\` is called without an event object; the gesture handler has no GestureResponderEvent to pass.`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
167
145
|
}
|
|
146
|
+
}
|
|
168
147
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
148
|
+
const resolvedItemSize = useMemo(() => {
|
|
149
|
+
if (itemSize && itemSize > 0) return itemSize;
|
|
150
|
+
if (itemWidth && itemWidth > 0) return itemWidth;
|
|
151
|
+
if (!full && visibleItem > 1) {
|
|
152
|
+
const size =
|
|
153
|
+
(containerSize - MULTI_ITEM_SPACING * (visibleItem - 1)) / visibleItem;
|
|
154
|
+
return size > 0 ? size : undefined;
|
|
173
155
|
}
|
|
156
|
+
// Undefined lets the engine measure the container itself.
|
|
157
|
+
return undefined;
|
|
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;
|
|
174
212
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (
|
|
201
|
-
|
|
213
|
+
return {
|
|
214
|
+
opacity,
|
|
215
|
+
transform: [
|
|
216
|
+
isVertical
|
|
217
|
+
? { translateY: translate }
|
|
218
|
+
: { translateX: translate * directionSign },
|
|
219
|
+
{ scale },
|
|
220
|
+
],
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
}, [
|
|
224
|
+
hasLegacyItemAnimation,
|
|
225
|
+
inactiveSlideOpacity,
|
|
226
|
+
inactiveSlideScale,
|
|
227
|
+
isVertical,
|
|
228
|
+
pageSize,
|
|
229
|
+
]);
|
|
230
|
+
|
|
231
|
+
const resolvedItemAnimation =
|
|
232
|
+
itemAnimation ?? (layout ? undefined : legacyItemAnimation);
|
|
233
|
+
|
|
234
|
+
const handleContainerLayout = useCallback(
|
|
235
|
+
(event: LayoutChangeEvent) => {
|
|
236
|
+
const { width, height } = event.nativeEvent.layout;
|
|
237
|
+
const size = isVertical ? height : width;
|
|
238
|
+
if (size > 0) setContainerSize(size);
|
|
239
|
+
onLayout?.(event);
|
|
202
240
|
},
|
|
203
|
-
[
|
|
241
|
+
[isVertical, onLayout]
|
|
204
242
|
);
|
|
205
243
|
|
|
206
|
-
const
|
|
207
|
-
(
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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);
|
|
244
|
+
const handleItemLayout = useCallback(
|
|
245
|
+
(event: LayoutChangeEvent) => {
|
|
246
|
+
if (hasExplicitSize) return;
|
|
247
|
+
const { width, height } = event.nativeEvent.layout;
|
|
248
|
+
const size = isVertical ? width : height;
|
|
249
|
+
if (size > 0) {
|
|
250
|
+
setMeasuredItemSize(prev => (prev === null || size > prev ? size : prev));
|
|
225
251
|
}
|
|
226
252
|
},
|
|
227
|
-
[
|
|
253
|
+
[hasExplicitSize, isVertical]
|
|
228
254
|
);
|
|
229
255
|
|
|
230
|
-
const
|
|
231
|
-
(
|
|
232
|
-
|
|
233
|
-
snapToItem(nextIndex, animated, fireCallback);
|
|
234
|
-
},
|
|
235
|
-
[data.length, snapToItem]
|
|
256
|
+
const slideWrapperStyle = useMemo(
|
|
257
|
+
() => (hasExplicitSize ? styles.slideFill : styles.slideAuto),
|
|
258
|
+
[hasExplicitSize]
|
|
236
259
|
);
|
|
237
260
|
|
|
238
|
-
const
|
|
239
|
-
(
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
[
|
|
261
|
+
const renderCoreItem = useCallback(
|
|
262
|
+
(info: { item: any; index: number; relativeProgress: SharedValue<number> }) => (
|
|
263
|
+
<View style={[slideWrapperStyle, slideStyle]} onLayout={handleItemLayout}>
|
|
264
|
+
{renderItem(info)}
|
|
265
|
+
</View>
|
|
266
|
+
),
|
|
267
|
+
[handleItemLayout, renderItem, slideStyle, slideWrapperStyle]
|
|
245
268
|
);
|
|
246
269
|
|
|
247
|
-
const
|
|
248
|
-
if (autoplayTimerRef.current) {
|
|
249
|
-
clearInterval(autoplayTimerRef.current);
|
|
250
|
-
autoplayTimerRef.current = null;
|
|
251
|
-
}
|
|
252
|
-
}, []);
|
|
270
|
+
const lastIndexRef = useRef(resolvedDefaultIndex);
|
|
253
271
|
|
|
254
|
-
const
|
|
255
|
-
isAutoplayPausedRef.current = true;
|
|
256
|
-
stopAutoplay();
|
|
257
|
-
}, [stopAutoplay]);
|
|
272
|
+
const needsProgress = !!(onProgressChange || onScroll || onScrollIndexChanged);
|
|
258
273
|
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
274
|
+
const handleProgressChange = useCallback(
|
|
275
|
+
(value: number) => {
|
|
276
|
+
onProgressChange?.(value);
|
|
277
|
+
onScroll?.(scrollEvent(value * pageSize));
|
|
262
278
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
autoplayTimerRef.current = setInterval(() => {
|
|
269
|
-
if (!isAutoplayPausedRef.current) {
|
|
270
|
-
snapToNext(true, true);
|
|
279
|
+
if (onScrollIndexChanged && data.length > 0) {
|
|
280
|
+
const index = positiveModulo(Math.round(value), data.length);
|
|
281
|
+
if (index !== lastIndexRef.current) {
|
|
282
|
+
lastIndexRef.current = index;
|
|
283
|
+
onScrollIndexChanged(index);
|
|
271
284
|
}
|
|
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);
|
|
307
285
|
}
|
|
308
286
|
},
|
|
309
|
-
[
|
|
287
|
+
[data.length, onProgressChange, onScroll, onScrollIndexChanged, pageSize]
|
|
310
288
|
);
|
|
311
289
|
|
|
312
|
-
const
|
|
313
|
-
(
|
|
314
|
-
|
|
315
|
-
const realIndex = getRealIndex(index);
|
|
316
|
-
|
|
317
|
-
if (realIndex !== currentIndexRef.current) {
|
|
318
|
-
currentIndexRef.current = realIndex;
|
|
319
|
-
setCurrentIndex(realIndex);
|
|
320
|
-
|
|
321
|
-
if (onScrollIndexChanged) {
|
|
322
|
-
onScrollIndexChanged(realIndex);
|
|
323
|
-
}
|
|
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);
|
|
334
|
-
|
|
335
|
-
currentIndexRef.current = realIndex;
|
|
336
|
-
setCurrentIndex(realIndex);
|
|
290
|
+
const handleSnapToItem = useCallback(
|
|
291
|
+
(index: number) => {
|
|
292
|
+
lastIndexRef.current = index;
|
|
337
293
|
|
|
338
|
-
if (
|
|
339
|
-
|
|
294
|
+
if (Date.now() < suppressUntilRef.current) {
|
|
295
|
+
suppressUntilRef.current = 0;
|
|
296
|
+
return;
|
|
340
297
|
}
|
|
341
298
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (onMomentumScrollEndProp) {
|
|
345
|
-
onMomentumScrollEndProp(event);
|
|
346
|
-
}
|
|
299
|
+
onSnapToItem?.(index);
|
|
347
300
|
},
|
|
348
|
-
[
|
|
301
|
+
[onSnapToItem]
|
|
349
302
|
);
|
|
350
303
|
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
if (onTouchStartProp) {
|
|
357
|
-
onTouchStartProp(event);
|
|
358
|
-
}
|
|
359
|
-
},
|
|
360
|
-
[autoplay, pauseAutoPlay, onTouchStartProp]
|
|
304
|
+
const needsGestureConfig = !!(
|
|
305
|
+
onConfigurePanGesture ||
|
|
306
|
+
onTouchStart ||
|
|
307
|
+
onTouchEnd
|
|
361
308
|
);
|
|
362
309
|
|
|
363
|
-
const
|
|
364
|
-
(
|
|
365
|
-
if (
|
|
366
|
-
|
|
310
|
+
const configurePanGesture = useCallback(
|
|
311
|
+
(gesture: CarouselPanGesture) => {
|
|
312
|
+
if (onTouchStart) {
|
|
313
|
+
gesture.onBegin(() => {
|
|
314
|
+
'worklet';
|
|
315
|
+
runOnJS(onTouchStart)();
|
|
316
|
+
});
|
|
367
317
|
}
|
|
368
|
-
if (
|
|
369
|
-
|
|
318
|
+
if (onTouchEnd) {
|
|
319
|
+
gesture.onFinalize(() => {
|
|
320
|
+
'worklet';
|
|
321
|
+
runOnJS(onTouchEnd)();
|
|
322
|
+
});
|
|
370
323
|
}
|
|
324
|
+
onConfigurePanGesture?.(gesture);
|
|
371
325
|
},
|
|
372
|
-
[
|
|
326
|
+
[onConfigurePanGesture, onTouchEnd, onTouchStart]
|
|
373
327
|
);
|
|
374
328
|
|
|
375
|
-
const
|
|
376
|
-
(
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if (onLayoutProp) {
|
|
383
|
-
onLayoutProp(event);
|
|
384
|
-
}
|
|
385
|
-
},
|
|
386
|
-
[onLayoutProp]
|
|
387
|
-
);
|
|
388
|
-
|
|
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
|
-
);
|
|
329
|
+
const clearAutoplayDelay = useCallback(() => {
|
|
330
|
+
if (autoplayDelayTimerRef.current) {
|
|
331
|
+
clearTimeout(autoplayDelayTimerRef.current);
|
|
332
|
+
autoplayDelayTimerRef.current = null;
|
|
333
|
+
}
|
|
334
|
+
}, []);
|
|
402
335
|
|
|
403
|
-
const
|
|
404
|
-
(
|
|
405
|
-
|
|
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
|
-
}
|
|
336
|
+
const startAutoplay = useCallback(() => {
|
|
337
|
+
clearAutoplayDelay();
|
|
338
|
+
if (!autoplay) return;
|
|
416
339
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
scrollX={scrollX}
|
|
422
|
-
inactiveSlideOpacity={inactiveSlideOpacity}
|
|
423
|
-
inactiveSlideScale={inactiveSlideScale}
|
|
424
|
-
slideStyle={slideStyle}
|
|
425
|
-
>
|
|
426
|
-
{renderItem({ item, index: realIndex })}
|
|
427
|
-
</CarouselItem>
|
|
340
|
+
if (autoplayDelay > 0) {
|
|
341
|
+
autoplayDelayTimerRef.current = setTimeout(
|
|
342
|
+
() => setIsAutoplayRunning(true),
|
|
343
|
+
autoplayDelay
|
|
428
344
|
);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
getRealIndex,
|
|
432
|
-
inactiveSlideOpacity,
|
|
433
|
-
inactiveSlideScale,
|
|
434
|
-
itemWidth,
|
|
435
|
-
slideStyle,
|
|
436
|
-
renderItem,
|
|
437
|
-
scrollX,
|
|
438
|
-
]
|
|
439
|
-
);
|
|
440
|
-
|
|
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
|
-
});
|
|
469
|
-
|
|
470
|
-
useEffect(() => {
|
|
471
|
-
if (isLayoutReady && firstItem > 0 && firstItem < data.length) {
|
|
472
|
-
setTimeout(() => {
|
|
473
|
-
snapToItem(firstItem, false, false);
|
|
474
|
-
}, 50);
|
|
345
|
+
} else {
|
|
346
|
+
setIsAutoplayRunning(true);
|
|
475
347
|
}
|
|
476
|
-
}, [
|
|
348
|
+
}, [autoplay, autoplayDelay, clearAutoplayDelay]);
|
|
349
|
+
|
|
350
|
+
const pauseAutoplay = useCallback(() => {
|
|
351
|
+
clearAutoplayDelay();
|
|
352
|
+
setIsAutoplayRunning(false);
|
|
353
|
+
}, [clearAutoplayDelay]);
|
|
477
354
|
|
|
478
355
|
useEffect(() => {
|
|
479
|
-
if (autoplay
|
|
480
|
-
|
|
481
|
-
}
|
|
356
|
+
if (autoplay) startAutoplay();
|
|
357
|
+
else pauseAutoplay();
|
|
482
358
|
|
|
483
|
-
return
|
|
484
|
-
|
|
485
|
-
};
|
|
486
|
-
}, [autoplay, data.length, startAutoplay, stopAutoplay]);
|
|
359
|
+
return clearAutoplayDelay;
|
|
360
|
+
}, [autoplay, clearAutoplayDelay, pauseAutoplay, startAutoplay]);
|
|
487
361
|
|
|
488
362
|
useEffect(() => {
|
|
489
|
-
if (apparitionDelay
|
|
490
|
-
const timer = setTimeout(() => {
|
|
491
|
-
setIsVisible(true);
|
|
492
|
-
}, apparitionDelay);
|
|
363
|
+
if (apparitionDelay <= 0) return;
|
|
493
364
|
|
|
494
|
-
|
|
495
|
-
|
|
365
|
+
const timer = setTimeout(() => setIsVisible(true), apparitionDelay);
|
|
366
|
+
return () => clearTimeout(timer);
|
|
496
367
|
}, [apparitionDelay]);
|
|
497
368
|
|
|
369
|
+
const suppressNextCallback = useCallback(() => {
|
|
370
|
+
suppressUntilRef.current = Date.now() + SUPPRESS_CALLBACK_WINDOW;
|
|
371
|
+
}, []);
|
|
372
|
+
|
|
498
373
|
useImperativeHandle(
|
|
499
374
|
ref,
|
|
500
375
|
() => ({
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
376
|
+
next: options => coreRef.current?.next(options),
|
|
377
|
+
prev: options => coreRef.current?.prev(options),
|
|
378
|
+
scrollTo: options => coreRef.current?.scrollTo(options),
|
|
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
|
+
},
|
|
504
393
|
startAutoplay,
|
|
505
|
-
pauseAutoplay
|
|
506
|
-
stopAutoplay,
|
|
394
|
+
pauseAutoplay,
|
|
395
|
+
stopAutoplay: pauseAutoplay,
|
|
507
396
|
}),
|
|
508
|
-
[
|
|
397
|
+
[pauseAutoplay, startAutoplay, suppressNextCallback]
|
|
509
398
|
);
|
|
510
399
|
|
|
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
|
+
|
|
511
413
|
if (!isVisible) {
|
|
512
|
-
return <View style={
|
|
414
|
+
return <View style={resolvedStyle} testID={testID} />;
|
|
513
415
|
}
|
|
514
416
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
417
|
+
// `layout` and `itemAnimation` are a union in the engine props; both are
|
|
418
|
+
// optional here, so the object is assembled before the cast.
|
|
419
|
+
const coreProps = {
|
|
420
|
+
data,
|
|
421
|
+
renderItem: renderCoreItem,
|
|
422
|
+
defaultIndex: resolvedDefaultIndex,
|
|
423
|
+
orientation,
|
|
424
|
+
snapMode: resolvedSnapMode,
|
|
425
|
+
autoplay: isAutoplayRunning,
|
|
426
|
+
style: resolvedStyle,
|
|
427
|
+
onLayout: handleContainerLayout,
|
|
428
|
+
onSnapToItem: handleSnapToItem,
|
|
429
|
+
...(keyExtractor ? { keyExtractor } : null),
|
|
430
|
+
...(loop !== undefined ? { loop } : null),
|
|
431
|
+
...(autoplayInterval !== undefined ? { autoplayInterval } : null),
|
|
432
|
+
...(autoplayDirection !== undefined ? { autoplayDirection } : null),
|
|
433
|
+
...(resolvedItemSize ? { itemSize: resolvedItemSize } : null),
|
|
434
|
+
...(scrollEnabled !== undefined ? { scrollEnabled } : null),
|
|
435
|
+
...(overscrollEnabled !== undefined ? { overscrollEnabled } : null),
|
|
436
|
+
...(renderWindowSize !== undefined ? { renderWindowSize } : null),
|
|
437
|
+
...(animation ? { animation } : null),
|
|
438
|
+
...(progress ? { progress } : null),
|
|
439
|
+
...(scrollOffsetValue ? { scrollOffsetValue } : null),
|
|
440
|
+
...(contentContainerStyle ? { contentContainerStyle } : null),
|
|
441
|
+
...(testID ? { testID } : null),
|
|
442
|
+
...(onScrollStart ? { onScrollStart } : null),
|
|
443
|
+
...(layout ? { layout } : null),
|
|
444
|
+
...(resolvedItemAnimation
|
|
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} />;
|
|
543
454
|
});
|
|
544
455
|
|
|
545
456
|
const styles = StyleSheet.create({
|
|
546
|
-
|
|
547
|
-
|
|
457
|
+
autoSize: {
|
|
458
|
+
flexGrow: 1,
|
|
459
|
+
flexShrink: 1,
|
|
460
|
+
},
|
|
461
|
+
slideAuto: {
|
|
462
|
+
width: '100%',
|
|
463
|
+
},
|
|
464
|
+
slideFill: {
|
|
465
|
+
width: '100%',
|
|
466
|
+
height: '100%',
|
|
548
467
|
},
|
|
549
468
|
});
|
|
550
469
|
|
|
551
|
-
|
|
470
|
+
CarouselImpl.displayName = 'Carousel';
|
|
471
|
+
|
|
472
|
+
type CarouselComponent = <Item>(
|
|
473
|
+
props: CarouselProps<Item> & { ref?: React.Ref<CarouselRef> }
|
|
474
|
+
) => React.ReactElement;
|
|
552
475
|
|
|
476
|
+
export const Carousel = CarouselImpl as CarouselComponent;
|
|
553
477
|
export default Carousel;
|
|
554
|
-
export { Carousel };
|
|
555
|
-
export type { CarouselProps, CarouselRef };
|
|
556
478
|
|
|
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';
|