@momo-kits/carousel 0.159.1-beta.5 → 0.160.1-beta.10-test.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/index.tsx +200 -63
- 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,32 +320,82 @@ 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
|
-
|
|
273
|
-
|
|
387
|
+
// Report the final resting index here as well. The reanimated scroll
|
|
388
|
+
// handler isn't guaranteed to deliver the last momentum frame to JS in
|
|
389
|
+
// every runtime (e.g. miniapp hosts), so relying on handleScroll alone
|
|
390
|
+
// can leave onScrollIndexChanged stuck one item short of the end.
|
|
391
|
+
if (realIndex !== currentIndexRef.current) {
|
|
392
|
+
currentIndexRef.current = realIndex;
|
|
393
|
+
setCurrentIndex(realIndex);
|
|
394
|
+
|
|
395
|
+
if (onScrollIndexChanged) {
|
|
396
|
+
onScrollIndexChanged(realIndex);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
274
399
|
|
|
275
400
|
if (onSnapToItem) {
|
|
276
401
|
onSnapToItem(realIndex);
|
|
@@ -282,7 +407,33 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
|
|
|
282
407
|
onMomentumScrollEndProp(event);
|
|
283
408
|
}
|
|
284
409
|
},
|
|
285
|
-
[
|
|
410
|
+
[resolveIndex, getRealIndex, onScrollIndexChanged, onSnapToItem, handleLoopReposition, onMomentumScrollEndProp]
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
const handleScrollEndDrag = useCallback(
|
|
414
|
+
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
415
|
+
// Fallback for a drag released without enough velocity to fire a momentum
|
|
416
|
+
// scroll: report the resting index so onScrollIndexChanged isn't left
|
|
417
|
+
// stuck one item short of the end.
|
|
418
|
+
const { contentOffset, layoutMeasurement, contentSize } =
|
|
419
|
+
event.nativeEvent;
|
|
420
|
+
const index = resolveIndex(
|
|
421
|
+
contentOffset.x,
|
|
422
|
+
layoutMeasurement.width,
|
|
423
|
+
contentSize.width
|
|
424
|
+
);
|
|
425
|
+
const realIndex = getRealIndex(index);
|
|
426
|
+
|
|
427
|
+
if (realIndex !== currentIndexRef.current) {
|
|
428
|
+
currentIndexRef.current = realIndex;
|
|
429
|
+
setCurrentIndex(realIndex);
|
|
430
|
+
|
|
431
|
+
if (onScrollIndexChanged) {
|
|
432
|
+
onScrollIndexChanged(realIndex);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
[resolveIndex, getRealIndex, onScrollIndexChanged]
|
|
286
437
|
);
|
|
287
438
|
|
|
288
439
|
const handleTouchStart = useCallback(
|
|
@@ -351,41 +502,17 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
|
|
|
351
502
|
);
|
|
352
503
|
}
|
|
353
504
|
|
|
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
505
|
return (
|
|
377
|
-
<
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
},
|
|
385
|
-
]}
|
|
506
|
+
<CarouselItem
|
|
507
|
+
index={index}
|
|
508
|
+
itemWidth={itemWidth}
|
|
509
|
+
scrollX={scrollX}
|
|
510
|
+
inactiveSlideOpacity={inactiveSlideOpacity}
|
|
511
|
+
inactiveSlideScale={inactiveSlideScale}
|
|
512
|
+
slideStyle={slideStyle}
|
|
386
513
|
>
|
|
387
514
|
{renderItem({ item, index: realIndex })}
|
|
388
|
-
</
|
|
515
|
+
</CarouselItem>
|
|
389
516
|
);
|
|
390
517
|
},
|
|
391
518
|
[
|
|
@@ -395,7 +522,7 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
|
|
|
395
522
|
itemWidth,
|
|
396
523
|
slideStyle,
|
|
397
524
|
renderItem,
|
|
398
|
-
|
|
525
|
+
scrollX,
|
|
399
526
|
]
|
|
400
527
|
);
|
|
401
528
|
|
|
@@ -409,24 +536,28 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
|
|
|
409
536
|
[keyExtractor]
|
|
410
537
|
);
|
|
411
538
|
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
539
|
+
const emitScrollProp = useCallback(
|
|
540
|
+
(offsetX: number) => {
|
|
541
|
+
if (onScrollProp) {
|
|
542
|
+
onScrollProp({
|
|
543
|
+
nativeEvent: { contentOffset: { x: offsetX, y: 0 } },
|
|
544
|
+
} as NativeSyntheticEvent<NativeScrollEvent>);
|
|
418
545
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
return (event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
423
|
-
scrollHandler(event);
|
|
424
|
-
onScrollProp(event);
|
|
425
|
-
};
|
|
426
|
-
}
|
|
546
|
+
},
|
|
547
|
+
[onScrollProp],
|
|
548
|
+
);
|
|
427
549
|
|
|
428
|
-
|
|
429
|
-
|
|
550
|
+
const onScrollEvent = useAnimatedScrollHandler({
|
|
551
|
+
onScroll: (event) => {
|
|
552
|
+
scrollX.value = event.contentOffset.x;
|
|
553
|
+
runOnJS(handleScroll)(
|
|
554
|
+
event.contentOffset.x,
|
|
555
|
+
event.layoutMeasurement.width,
|
|
556
|
+
event.contentSize.width,
|
|
557
|
+
);
|
|
558
|
+
runOnJS(emitScrollProp)(event.contentOffset.x);
|
|
559
|
+
},
|
|
560
|
+
});
|
|
430
561
|
|
|
431
562
|
useEffect(() => {
|
|
432
563
|
if (isLayoutReady && firstItem > 0 && firstItem < data.length) {
|
|
@@ -486,9 +617,15 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
|
|
|
486
617
|
scrollEventThrottle={16}
|
|
487
618
|
onScroll={onScrollEvent}
|
|
488
619
|
onMomentumScrollEnd={handleMomentumScrollEnd}
|
|
620
|
+
onScrollEndDrag={handleScrollEndDrag}
|
|
489
621
|
onTouchStart={handleTouchStart}
|
|
490
622
|
onTouchEnd={handleTouchEnd}
|
|
491
|
-
|
|
623
|
+
snapToOffsets={snapOffsets}
|
|
624
|
+
snapToInterval={
|
|
625
|
+
enableSnap && snapToIntervalProp !== undefined
|
|
626
|
+
? (snapToInterval as number)
|
|
627
|
+
: undefined
|
|
628
|
+
}
|
|
492
629
|
snapToAlignment={enableSnap ? 'start' : undefined}
|
|
493
630
|
decelerationRate={enableSnap ? 'fast' : 'normal'}
|
|
494
631
|
disableIntervalMomentum={disableIntervalMomentum}
|