@momo-kits/carousel 0.159.1-beta.4 → 0.160.1-beta.10
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/index.tsx +161 -61
- package/package.json +1 -1
package/index.tsx
CHANGED
|
@@ -8,7 +8,6 @@ import React, {
|
|
|
8
8
|
useState,
|
|
9
9
|
} from 'react';
|
|
10
10
|
import {
|
|
11
|
-
Animated,
|
|
12
11
|
Dimensions,
|
|
13
12
|
FlatList,
|
|
14
13
|
GestureResponderEvent,
|
|
@@ -17,14 +16,79 @@ import {
|
|
|
17
16
|
NativeSyntheticEvent,
|
|
18
17
|
Platform,
|
|
19
18
|
StyleSheet,
|
|
20
|
-
View
|
|
19
|
+
View,
|
|
21
20
|
} from 'react-native';
|
|
21
|
+
import Animated, {
|
|
22
|
+
Extrapolation,
|
|
23
|
+
interpolate,
|
|
24
|
+
runOnJS,
|
|
25
|
+
useAnimatedScrollHandler,
|
|
26
|
+
useAnimatedStyle,
|
|
27
|
+
useSharedValue,
|
|
28
|
+
type SharedValue,
|
|
29
|
+
} from 'react-native-reanimated';
|
|
22
30
|
import { CarouselProps, CarouselRef } from './types';
|
|
23
31
|
|
|
24
32
|
const { width: viewportWidth } = Dimensions.get('window');
|
|
25
33
|
|
|
26
34
|
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
|
|
27
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
|
+
|
|
28
92
|
const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
|
|
29
93
|
const {
|
|
30
94
|
data,
|
|
@@ -62,7 +126,7 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
|
|
|
62
126
|
|
|
63
127
|
const flatListRef = useRef<FlatList>(null);
|
|
64
128
|
const autoplayTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
65
|
-
const
|
|
129
|
+
const scrollX = useSharedValue(0);
|
|
66
130
|
const containerWidthRef = useRef(viewportWidth);
|
|
67
131
|
const isAutoplayPausedRef = useRef(false);
|
|
68
132
|
const currentIndexRef = useRef(firstItem);
|
|
@@ -111,6 +175,17 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
|
|
|
111
175
|
return result;
|
|
112
176
|
}, [data, loop, loopClonesPerSide]);
|
|
113
177
|
|
|
178
|
+
// Explicit per-item snap offsets for the default snap path. Unlike
|
|
179
|
+
// snapToInterval (a single fixed step that can overshoot the content end and
|
|
180
|
+
// leave the last item unreachable), snapToOffsets combined with RN's
|
|
181
|
+
// snapToEnd (default true) always lets the scroll settle on the last item.
|
|
182
|
+
const snapOffsets = useMemo(() => {
|
|
183
|
+
if (!enableSnap || snapToIntervalProp !== undefined) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
return dataWithClones.map((_, i) => i * itemWidth);
|
|
187
|
+
}, [enableSnap, snapToIntervalProp, dataWithClones, itemWidth]);
|
|
188
|
+
|
|
114
189
|
const getRealIndex = useCallback(
|
|
115
190
|
(index: number) => {
|
|
116
191
|
if (!loop) return index;
|
|
@@ -245,28 +320,68 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
|
|
|
245
320
|
[loop, data.length, loopClonesPerSide, itemWidth, getLoopIndex]
|
|
246
321
|
);
|
|
247
322
|
|
|
323
|
+
const resolveIndex = useCallback(
|
|
324
|
+
(offsetX: number, layoutWidth: number, contentWidth: number) => {
|
|
325
|
+
const count = dataWithClones.length;
|
|
326
|
+
// Derive the per-item width from the *native* scroll geometry rather than
|
|
327
|
+
// the JS-side itemWidth (measured via onLayout / Dimensions). In some hosts
|
|
328
|
+
// (e.g. miniapp runtimes) those two coordinate spaces don't line up, which
|
|
329
|
+
// made Math.round(offsetX / itemWidth) land one item short of the end.
|
|
330
|
+
const measuredItemWidth =
|
|
331
|
+
contentWidth > 0 && count > 0 ? contentWidth / count : itemWidth;
|
|
332
|
+
const unit = measuredItemWidth || itemWidth;
|
|
333
|
+
let index = unit > 0 ? Math.round(offsetX / unit) : 0;
|
|
334
|
+
|
|
335
|
+
// Once the content end is reached, snap to the very last item regardless of
|
|
336
|
+
// sub-pixel rounding drift (only when a single item fills the viewport).
|
|
337
|
+
const singleVisible =
|
|
338
|
+
layoutWidth > 0 && layoutWidth <= measuredItemWidth + 1;
|
|
339
|
+
if (
|
|
340
|
+
singleVisible &&
|
|
341
|
+
contentWidth > 0 &&
|
|
342
|
+
offsetX + layoutWidth >= contentWidth - 1
|
|
343
|
+
) {
|
|
344
|
+
index = count - 1;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (index < 0) {
|
|
348
|
+
index = 0;
|
|
349
|
+
}
|
|
350
|
+
if (count > 0 && index > count - 1) {
|
|
351
|
+
index = count - 1;
|
|
352
|
+
}
|
|
353
|
+
return index;
|
|
354
|
+
},
|
|
355
|
+
[dataWithClones.length, itemWidth]
|
|
356
|
+
);
|
|
357
|
+
|
|
248
358
|
const handleScroll = useCallback(
|
|
249
|
-
(
|
|
250
|
-
const
|
|
251
|
-
const index = Math.round(offsetX / itemWidth);
|
|
359
|
+
(offsetX: number, layoutWidth: number, contentWidth: number) => {
|
|
360
|
+
const index = resolveIndex(offsetX, layoutWidth, contentWidth);
|
|
252
361
|
const realIndex = getRealIndex(index);
|
|
253
362
|
|
|
254
363
|
if (realIndex !== currentIndexRef.current) {
|
|
255
364
|
currentIndexRef.current = realIndex;
|
|
256
365
|
setCurrentIndex(realIndex);
|
|
257
|
-
|
|
366
|
+
|
|
258
367
|
if (onScrollIndexChanged) {
|
|
259
368
|
onScrollIndexChanged(realIndex);
|
|
260
369
|
}
|
|
261
370
|
}
|
|
262
371
|
},
|
|
263
|
-
[
|
|
372
|
+
[resolveIndex, getRealIndex, onScrollIndexChanged]
|
|
264
373
|
);
|
|
265
374
|
|
|
266
375
|
const handleMomentumScrollEnd = useCallback(
|
|
267
376
|
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
268
|
-
const
|
|
269
|
-
|
|
377
|
+
const { contentOffset, layoutMeasurement, contentSize } =
|
|
378
|
+
event.nativeEvent;
|
|
379
|
+
const offsetX = contentOffset.x;
|
|
380
|
+
const index = resolveIndex(
|
|
381
|
+
offsetX,
|
|
382
|
+
layoutMeasurement.width,
|
|
383
|
+
contentSize.width
|
|
384
|
+
);
|
|
270
385
|
const realIndex = getRealIndex(index);
|
|
271
386
|
|
|
272
387
|
currentIndexRef.current = realIndex;
|
|
@@ -282,7 +397,7 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
|
|
|
282
397
|
onMomentumScrollEndProp(event);
|
|
283
398
|
}
|
|
284
399
|
},
|
|
285
|
-
[
|
|
400
|
+
[resolveIndex, getRealIndex, onSnapToItem, handleLoopReposition, onMomentumScrollEndProp]
|
|
286
401
|
);
|
|
287
402
|
|
|
288
403
|
const handleTouchStart = useCallback(
|
|
@@ -351,41 +466,17 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
|
|
|
351
466
|
);
|
|
352
467
|
}
|
|
353
468
|
|
|
354
|
-
const inputRange = [
|
|
355
|
-
(index - 1) * itemWidth,
|
|
356
|
-
index * itemWidth,
|
|
357
|
-
(index + 1) * itemWidth,
|
|
358
|
-
];
|
|
359
|
-
|
|
360
|
-
const opacity = hasOpacityAnimation
|
|
361
|
-
? scrollXRef.interpolate({
|
|
362
|
-
inputRange,
|
|
363
|
-
outputRange: [inactiveSlideOpacity, 1, inactiveSlideOpacity],
|
|
364
|
-
extrapolate: 'clamp',
|
|
365
|
-
})
|
|
366
|
-
: 1;
|
|
367
|
-
|
|
368
|
-
const scale = hasScaleAnimation
|
|
369
|
-
? scrollXRef.interpolate({
|
|
370
|
-
inputRange,
|
|
371
|
-
outputRange: [inactiveSlideScale, 1, inactiveSlideScale],
|
|
372
|
-
extrapolate: 'clamp',
|
|
373
|
-
})
|
|
374
|
-
: 1;
|
|
375
|
-
|
|
376
469
|
return (
|
|
377
|
-
<
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
},
|
|
385
|
-
]}
|
|
470
|
+
<CarouselItem
|
|
471
|
+
index={index}
|
|
472
|
+
itemWidth={itemWidth}
|
|
473
|
+
scrollX={scrollX}
|
|
474
|
+
inactiveSlideOpacity={inactiveSlideOpacity}
|
|
475
|
+
inactiveSlideScale={inactiveSlideScale}
|
|
476
|
+
slideStyle={slideStyle}
|
|
386
477
|
>
|
|
387
478
|
{renderItem({ item, index: realIndex })}
|
|
388
|
-
</
|
|
479
|
+
</CarouselItem>
|
|
389
480
|
);
|
|
390
481
|
},
|
|
391
482
|
[
|
|
@@ -395,7 +486,7 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
|
|
|
395
486
|
itemWidth,
|
|
396
487
|
slideStyle,
|
|
397
488
|
renderItem,
|
|
398
|
-
|
|
489
|
+
scrollX,
|
|
399
490
|
]
|
|
400
491
|
);
|
|
401
492
|
|
|
@@ -409,24 +500,28 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
|
|
|
409
500
|
[keyExtractor]
|
|
410
501
|
);
|
|
411
502
|
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
503
|
+
const emitScrollProp = useCallback(
|
|
504
|
+
(offsetX: number) => {
|
|
505
|
+
if (onScrollProp) {
|
|
506
|
+
onScrollProp({
|
|
507
|
+
nativeEvent: { contentOffset: { x: offsetX, y: 0 } },
|
|
508
|
+
} as NativeSyntheticEvent<NativeScrollEvent>);
|
|
418
509
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
return (event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
423
|
-
scrollHandler(event);
|
|
424
|
-
onScrollProp(event);
|
|
425
|
-
};
|
|
426
|
-
}
|
|
510
|
+
},
|
|
511
|
+
[onScrollProp],
|
|
512
|
+
);
|
|
427
513
|
|
|
428
|
-
|
|
429
|
-
|
|
514
|
+
const onScrollEvent = useAnimatedScrollHandler({
|
|
515
|
+
onScroll: (event) => {
|
|
516
|
+
scrollX.value = event.contentOffset.x;
|
|
517
|
+
runOnJS(handleScroll)(
|
|
518
|
+
event.contentOffset.x,
|
|
519
|
+
event.layoutMeasurement.width,
|
|
520
|
+
event.contentSize.width,
|
|
521
|
+
);
|
|
522
|
+
runOnJS(emitScrollProp)(event.contentOffset.x);
|
|
523
|
+
},
|
|
524
|
+
});
|
|
430
525
|
|
|
431
526
|
useEffect(() => {
|
|
432
527
|
if (isLayoutReady && firstItem > 0 && firstItem < data.length) {
|
|
@@ -488,7 +583,12 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
|
|
|
488
583
|
onMomentumScrollEnd={handleMomentumScrollEnd}
|
|
489
584
|
onTouchStart={handleTouchStart}
|
|
490
585
|
onTouchEnd={handleTouchEnd}
|
|
491
|
-
|
|
586
|
+
snapToOffsets={snapOffsets}
|
|
587
|
+
snapToInterval={
|
|
588
|
+
enableSnap && snapToIntervalProp !== undefined
|
|
589
|
+
? (snapToInterval as number)
|
|
590
|
+
: undefined
|
|
591
|
+
}
|
|
492
592
|
snapToAlignment={enableSnap ? 'start' : undefined}
|
|
493
593
|
decelerationRate={enableSnap ? 'fast' : 'normal'}
|
|
494
594
|
disableIntervalMomentum={disableIntervalMomentum}
|