@momo-kits/carousel 0.160.1-beta.12 → 0.160.1-beta.14
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 +121 -71
- package/package.json +1 -1
- package/types.ts +2 -0
package/index.tsx
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
NativeSyntheticEvent,
|
|
17
17
|
Platform,
|
|
18
18
|
StyleSheet,
|
|
19
|
+
Text,
|
|
19
20
|
View,
|
|
20
21
|
} from 'react-native';
|
|
21
22
|
import Animated, {
|
|
@@ -31,6 +32,11 @@ import { CarouselProps, CarouselRef } from './types';
|
|
|
31
32
|
|
|
32
33
|
const { width: viewportWidth } = Dimensions.get('window');
|
|
33
34
|
|
|
35
|
+
// TEMP: force the diagnostic overlay on for every carousel in this beta so it
|
|
36
|
+
// shows regardless of whether the consumer screen passes `debug`. Flip to false
|
|
37
|
+
// (or delete) once the miniapp index bug is root-caused.
|
|
38
|
+
const FORCE_DEBUG = true;
|
|
39
|
+
|
|
34
40
|
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
|
|
35
41
|
|
|
36
42
|
type CarouselItemProps = {
|
|
@@ -122,19 +128,37 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
|
|
|
122
128
|
onTouchEnd: onTouchEndProp,
|
|
123
129
|
onLayout: onLayoutProp,
|
|
124
130
|
getItemLayout: getItemLayoutProp,
|
|
131
|
+
debug = false,
|
|
125
132
|
} = props;
|
|
126
133
|
|
|
134
|
+
const debugEnabled = debug || FORCE_DEBUG;
|
|
135
|
+
|
|
127
136
|
const flatListRef = useRef<FlatList>(null);
|
|
128
137
|
const autoplayTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
129
138
|
const scrollX = useSharedValue(0);
|
|
130
139
|
const containerWidthRef = useRef(viewportWidth);
|
|
131
140
|
const isAutoplayPausedRef = useRef(false);
|
|
132
141
|
const currentIndexRef = useRef(firstItem);
|
|
142
|
+
// DEBUG: fire counters for each index-reporting source (visible via overlay).
|
|
143
|
+
const debugCountsRef = useRef({ s: 0, m: 0, d: 0, p: 0 });
|
|
133
144
|
|
|
134
145
|
const [currentIndex, setCurrentIndex] = useState(firstItem);
|
|
135
146
|
const [containerWidth, setContainerWidth] = useState(viewportWidth);
|
|
136
147
|
const [isVisible, setIsVisible] = useState(apparitionDelay === 0);
|
|
137
148
|
const [isLayoutReady, setIsLayoutReady] = useState(false);
|
|
149
|
+
const [debugInfo, setDebugInfo] = useState({
|
|
150
|
+
s: 0,
|
|
151
|
+
m: 0,
|
|
152
|
+
d: 0,
|
|
153
|
+
p: 0,
|
|
154
|
+
tag: '-',
|
|
155
|
+
off: 0,
|
|
156
|
+
lw: 0,
|
|
157
|
+
cw: 0,
|
|
158
|
+
iw: 0,
|
|
159
|
+
idx: -1,
|
|
160
|
+
real: -1,
|
|
161
|
+
});
|
|
138
162
|
|
|
139
163
|
const itemWidth = useMemo(() => {
|
|
140
164
|
if (full) {
|
|
@@ -355,99 +379,92 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
|
|
|
355
379
|
[dataWithClones.length, itemWidth]
|
|
356
380
|
);
|
|
357
381
|
|
|
358
|
-
|
|
359
|
-
|
|
382
|
+
// Single index reporter shared by every source (scroll frames + settle
|
|
383
|
+
// events). `tag` identifies which source fired, surfaced in the debug overlay.
|
|
384
|
+
const reportResting = useCallback(
|
|
385
|
+
(offsetX: number, layoutWidth: number, contentWidth: number, tag: string) => {
|
|
360
386
|
const index = resolveIndex(offsetX, layoutWidth, contentWidth);
|
|
361
387
|
const realIndex = getRealIndex(index);
|
|
388
|
+
const changed = realIndex !== currentIndexRef.current;
|
|
389
|
+
|
|
390
|
+
if (debugEnabled && (tag !== 's' || changed)) {
|
|
391
|
+
const c = debugCountsRef.current;
|
|
392
|
+
setDebugInfo({
|
|
393
|
+
s: c.s,
|
|
394
|
+
m: c.m,
|
|
395
|
+
d: c.d,
|
|
396
|
+
p: c.p,
|
|
397
|
+
tag,
|
|
398
|
+
off: offsetX,
|
|
399
|
+
lw: layoutWidth,
|
|
400
|
+
cw: contentWidth,
|
|
401
|
+
iw: itemWidth,
|
|
402
|
+
idx: index,
|
|
403
|
+
real: realIndex,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
362
406
|
|
|
363
|
-
if (
|
|
364
|
-
// eslint-disable-next-line no-console
|
|
365
|
-
console.log(
|
|
366
|
-
`[Carousel] SCROLL idx=${index} real=${realIndex} offsetX=${offsetX.toFixed(1)} layoutW=${layoutWidth.toFixed(1)} contentW=${contentWidth.toFixed(1)} itemWidth=${itemWidth.toFixed(1)} count=${dataWithClones.length}`
|
|
367
|
-
);
|
|
407
|
+
if (changed) {
|
|
368
408
|
currentIndexRef.current = realIndex;
|
|
369
409
|
setCurrentIndex(realIndex);
|
|
370
|
-
|
|
371
410
|
if (onScrollIndexChanged) {
|
|
372
411
|
onScrollIndexChanged(realIndex);
|
|
373
412
|
}
|
|
374
413
|
}
|
|
414
|
+
return index;
|
|
375
415
|
},
|
|
376
|
-
[resolveIndex, getRealIndex, onScrollIndexChanged,
|
|
416
|
+
[resolveIndex, getRealIndex, onScrollIndexChanged, debugEnabled, itemWidth]
|
|
377
417
|
);
|
|
378
418
|
|
|
379
|
-
const
|
|
380
|
-
(
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
layoutMeasurement.width,
|
|
387
|
-
contentSize.width
|
|
388
|
-
);
|
|
389
|
-
const realIndex = getRealIndex(index);
|
|
390
|
-
|
|
391
|
-
// eslint-disable-next-line no-console
|
|
392
|
-
console.log(
|
|
393
|
-
`[Carousel] MOMENTUM_END idx=${index} real=${realIndex} offsetX=${offsetX.toFixed(1)} layoutW=${layoutMeasurement.width.toFixed(1)} contentW=${contentSize.width.toFixed(1)} itemWidth=${itemWidth.toFixed(1)} count=${dataWithClones.length}`
|
|
394
|
-
);
|
|
395
|
-
|
|
396
|
-
// Report the final resting index here as well. The reanimated scroll
|
|
397
|
-
// handler isn't guaranteed to deliver the last momentum frame to JS in
|
|
398
|
-
// every runtime (e.g. miniapp hosts), so relying on handleScroll alone
|
|
399
|
-
// can leave onScrollIndexChanged stuck one item short of the end.
|
|
400
|
-
if (realIndex !== currentIndexRef.current) {
|
|
401
|
-
currentIndexRef.current = realIndex;
|
|
402
|
-
setCurrentIndex(realIndex);
|
|
403
|
-
|
|
404
|
-
if (onScrollIndexChanged) {
|
|
405
|
-
onScrollIndexChanged(realIndex);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
419
|
+
const handleScroll = useCallback(
|
|
420
|
+
(offsetX: number, layoutWidth: number, contentWidth: number) => {
|
|
421
|
+
debugCountsRef.current.s += 1;
|
|
422
|
+
reportResting(offsetX, layoutWidth, contentWidth, 's');
|
|
423
|
+
},
|
|
424
|
+
[reportResting]
|
|
425
|
+
);
|
|
408
426
|
|
|
427
|
+
// Reliable settle path: reanimated owns the native scroll subscription, so its
|
|
428
|
+
// onMomentumEnd / onEndDrag worklets fire even when the plain FlatList
|
|
429
|
+
// onMomentumScrollEnd prop doesn't (observed in the miniapp host).
|
|
430
|
+
const handleMomentumSettle = useCallback(
|
|
431
|
+
(offsetX: number, layoutWidth: number, contentWidth: number) => {
|
|
432
|
+
debugCountsRef.current.m += 1;
|
|
433
|
+
const index = reportResting(offsetX, layoutWidth, contentWidth, 'm');
|
|
409
434
|
if (onSnapToItem) {
|
|
410
|
-
onSnapToItem(
|
|
435
|
+
onSnapToItem(getRealIndex(index));
|
|
411
436
|
}
|
|
412
|
-
|
|
413
437
|
handleLoopReposition(index);
|
|
438
|
+
},
|
|
439
|
+
[reportResting, onSnapToItem, getRealIndex, handleLoopReposition]
|
|
440
|
+
);
|
|
414
441
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
442
|
+
const handleDragSettle = useCallback(
|
|
443
|
+
(offsetX: number, layoutWidth: number, contentWidth: number) => {
|
|
444
|
+
debugCountsRef.current.d += 1;
|
|
445
|
+
reportResting(offsetX, layoutWidth, contentWidth, 'd');
|
|
418
446
|
},
|
|
419
|
-
[
|
|
447
|
+
[reportResting]
|
|
420
448
|
);
|
|
421
449
|
|
|
422
|
-
|
|
450
|
+
// Kept so the consumer's onMomentumScrollEnd prop still fires, and to measure
|
|
451
|
+
// (counter `p`) whether the plain FlatList momentum event reaches JS at all.
|
|
452
|
+
const handleMomentumScrollEnd = useCallback(
|
|
423
453
|
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
424
|
-
// Fallback for a drag released without enough velocity to fire a momentum
|
|
425
|
-
// scroll: report the resting index so onScrollIndexChanged isn't left
|
|
426
|
-
// stuck one item short of the end.
|
|
427
454
|
const { contentOffset, layoutMeasurement, contentSize } =
|
|
428
455
|
event.nativeEvent;
|
|
429
|
-
|
|
456
|
+
debugCountsRef.current.p += 1;
|
|
457
|
+
reportResting(
|
|
430
458
|
contentOffset.x,
|
|
431
459
|
layoutMeasurement.width,
|
|
432
|
-
contentSize.width
|
|
433
|
-
|
|
434
|
-
const realIndex = getRealIndex(index);
|
|
435
|
-
|
|
436
|
-
// eslint-disable-next-line no-console
|
|
437
|
-
console.log(
|
|
438
|
-
`[Carousel] DRAG_END idx=${index} real=${realIndex} offsetX=${contentOffset.x.toFixed(1)} layoutW=${layoutMeasurement.width.toFixed(1)} contentW=${contentSize.width.toFixed(1)} itemWidth=${itemWidth.toFixed(1)} count=${dataWithClones.length}`
|
|
460
|
+
contentSize.width,
|
|
461
|
+
'p'
|
|
439
462
|
);
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
currentIndexRef.current = realIndex;
|
|
443
|
-
setCurrentIndex(realIndex);
|
|
444
|
-
|
|
445
|
-
if (onScrollIndexChanged) {
|
|
446
|
-
onScrollIndexChanged(realIndex);
|
|
447
|
-
}
|
|
463
|
+
if (onMomentumScrollEndProp) {
|
|
464
|
+
onMomentumScrollEndProp(event);
|
|
448
465
|
}
|
|
449
466
|
},
|
|
450
|
-
[
|
|
467
|
+
[reportResting, onMomentumScrollEndProp]
|
|
451
468
|
);
|
|
452
469
|
|
|
453
470
|
const handleTouchStart = useCallback(
|
|
@@ -477,10 +494,6 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
|
|
|
477
494
|
const handleLayout = useCallback(
|
|
478
495
|
(event: LayoutChangeEvent) => {
|
|
479
496
|
const { width } = event.nativeEvent.layout;
|
|
480
|
-
// eslint-disable-next-line no-console
|
|
481
|
-
console.log(
|
|
482
|
-
`[Carousel] LAYOUT width=${width.toFixed(1)} window=${viewportWidth.toFixed(1)} full=${full}`
|
|
483
|
-
);
|
|
484
497
|
containerWidthRef.current = width;
|
|
485
498
|
setContainerWidth(width);
|
|
486
499
|
setIsLayoutReady(true);
|
|
@@ -489,7 +502,7 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
|
|
|
489
502
|
onLayoutProp(event);
|
|
490
503
|
}
|
|
491
504
|
},
|
|
492
|
-
[onLayoutProp
|
|
505
|
+
[onLayoutProp]
|
|
493
506
|
);
|
|
494
507
|
|
|
495
508
|
const getItemLayout = useCallback(
|
|
@@ -575,6 +588,20 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
|
|
|
575
588
|
);
|
|
576
589
|
runOnJS(emitScrollProp)(event.contentOffset.x);
|
|
577
590
|
},
|
|
591
|
+
onMomentumEnd: (event) => {
|
|
592
|
+
runOnJS(handleMomentumSettle)(
|
|
593
|
+
event.contentOffset.x,
|
|
594
|
+
event.layoutMeasurement.width,
|
|
595
|
+
event.contentSize.width,
|
|
596
|
+
);
|
|
597
|
+
},
|
|
598
|
+
onEndDrag: (event) => {
|
|
599
|
+
runOnJS(handleDragSettle)(
|
|
600
|
+
event.contentOffset.x,
|
|
601
|
+
event.layoutMeasurement.width,
|
|
602
|
+
event.contentSize.width,
|
|
603
|
+
);
|
|
604
|
+
},
|
|
578
605
|
});
|
|
579
606
|
|
|
580
607
|
useEffect(() => {
|
|
@@ -635,7 +662,6 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
|
|
|
635
662
|
scrollEventThrottle={16}
|
|
636
663
|
onScroll={onScrollEvent}
|
|
637
664
|
onMomentumScrollEnd={handleMomentumScrollEnd}
|
|
638
|
-
onScrollEndDrag={handleScrollEndDrag}
|
|
639
665
|
onTouchStart={handleTouchStart}
|
|
640
666
|
onTouchEnd={handleTouchEnd}
|
|
641
667
|
snapToOffsets={snapOffsets}
|
|
@@ -654,6 +680,16 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
|
|
|
654
680
|
maxToRenderPerBatch={5}
|
|
655
681
|
windowSize={5}
|
|
656
682
|
/>
|
|
683
|
+
{debugEnabled && (
|
|
684
|
+
<View style={styles.debugOverlay} pointerEvents="none">
|
|
685
|
+
<Text style={styles.debugText}>
|
|
686
|
+
{`S${debugInfo.s} M${debugInfo.m} D${debugInfo.d} P${debugInfo.p} [${debugInfo.tag}] idx=${debugInfo.idx} real=${debugInfo.real}`}
|
|
687
|
+
</Text>
|
|
688
|
+
<Text style={styles.debugText}>
|
|
689
|
+
{`off=${debugInfo.off.toFixed(0)} lW=${debugInfo.lw.toFixed(0)} cW=${debugInfo.cw.toFixed(0)} iw=${debugInfo.iw.toFixed(0)}`}
|
|
690
|
+
</Text>
|
|
691
|
+
</View>
|
|
692
|
+
)}
|
|
657
693
|
</View>
|
|
658
694
|
);
|
|
659
695
|
});
|
|
@@ -662,6 +698,20 @@ const styles = StyleSheet.create({
|
|
|
662
698
|
container: {
|
|
663
699
|
overflow: 'hidden',
|
|
664
700
|
},
|
|
701
|
+
debugOverlay: {
|
|
702
|
+
position: 'absolute',
|
|
703
|
+
top: 4,
|
|
704
|
+
left: 4,
|
|
705
|
+
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
706
|
+
paddingHorizontal: 6,
|
|
707
|
+
paddingVertical: 4,
|
|
708
|
+
borderRadius: 4,
|
|
709
|
+
},
|
|
710
|
+
debugText: {
|
|
711
|
+
color: '#0f0',
|
|
712
|
+
fontSize: 11,
|
|
713
|
+
fontWeight: '700',
|
|
714
|
+
},
|
|
665
715
|
});
|
|
666
716
|
|
|
667
717
|
Carousel.displayName = 'Carousel';
|
package/package.json
CHANGED
package/types.ts
CHANGED
|
@@ -43,6 +43,8 @@ export type CarouselProps = {
|
|
|
43
43
|
contentContainerStyle?: StyleProp<ViewStyle>;
|
|
44
44
|
full?: boolean;
|
|
45
45
|
snapToInterval?: number | Animated.Value | Animated.AnimatedInterpolation<number | string> | undefined
|
|
46
|
+
/** Renders an on-screen diagnostic overlay (fire counters + native scroll geometry). */
|
|
47
|
+
debug?: boolean;
|
|
46
48
|
};
|
|
47
49
|
|
|
48
50
|
export type CarouselRef = {
|