@seakoi/native-ui 1.0.0 → 1.1.0

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.
Files changed (51) hide show
  1. package/CHANGELOG.md +559 -265
  2. package/dist/commonjs/components/base/carousel/carousel-item.js +21 -0
  3. package/dist/commonjs/components/base/carousel/carousel.js +489 -0
  4. package/dist/commonjs/components/base/carousel/index.js +36 -0
  5. package/dist/commonjs/components/base/carousel/style/index.js +47 -0
  6. package/dist/commonjs/components/base/carousel/types.js +5 -0
  7. package/dist/commonjs/components/base/carousel/utils.js +27 -0
  8. package/dist/commonjs/components/base/index.js +34 -23
  9. package/dist/module/components/base/carousel/carousel-item.js +16 -0
  10. package/dist/module/components/base/carousel/carousel.js +484 -0
  11. package/dist/module/components/base/carousel/index.js +9 -0
  12. package/dist/module/components/base/carousel/style/index.js +43 -0
  13. package/dist/module/components/base/carousel/types.js +3 -0
  14. package/dist/module/components/base/carousel/utils.js +20 -0
  15. package/dist/module/components/base/index.js +1 -0
  16. package/dist/typescript/commonjs/src/components/base/carousel/carousel-item.d.ts +4 -0
  17. package/dist/typescript/commonjs/src/components/base/carousel/carousel-item.d.ts.map +1 -0
  18. package/dist/typescript/commonjs/src/components/base/carousel/carousel.d.ts +7 -0
  19. package/dist/typescript/commonjs/src/components/base/carousel/carousel.d.ts.map +1 -0
  20. package/dist/typescript/commonjs/src/components/base/carousel/index.d.ts +7 -0
  21. package/dist/typescript/commonjs/src/components/base/carousel/index.d.ts.map +1 -0
  22. package/dist/typescript/commonjs/src/components/base/carousel/style/index.d.ts +41 -0
  23. package/dist/typescript/commonjs/src/components/base/carousel/style/index.d.ts.map +1 -0
  24. package/dist/typescript/commonjs/src/components/base/carousel/types.d.ts +113 -0
  25. package/dist/typescript/commonjs/src/components/base/carousel/types.d.ts.map +1 -0
  26. package/dist/typescript/commonjs/src/components/base/carousel/utils.d.ts +6 -0
  27. package/dist/typescript/commonjs/src/components/base/carousel/utils.d.ts.map +1 -0
  28. package/dist/typescript/commonjs/src/components/base/index.d.ts +1 -0
  29. package/dist/typescript/commonjs/src/components/base/index.d.ts.map +1 -1
  30. package/dist/typescript/module/src/components/base/carousel/carousel-item.d.ts +4 -0
  31. package/dist/typescript/module/src/components/base/carousel/carousel-item.d.ts.map +1 -0
  32. package/dist/typescript/module/src/components/base/carousel/carousel.d.ts +7 -0
  33. package/dist/typescript/module/src/components/base/carousel/carousel.d.ts.map +1 -0
  34. package/dist/typescript/module/src/components/base/carousel/index.d.ts +7 -0
  35. package/dist/typescript/module/src/components/base/carousel/index.d.ts.map +1 -0
  36. package/dist/typescript/module/src/components/base/carousel/style/index.d.ts +41 -0
  37. package/dist/typescript/module/src/components/base/carousel/style/index.d.ts.map +1 -0
  38. package/dist/typescript/module/src/components/base/carousel/types.d.ts +113 -0
  39. package/dist/typescript/module/src/components/base/carousel/types.d.ts.map +1 -0
  40. package/dist/typescript/module/src/components/base/carousel/utils.d.ts +6 -0
  41. package/dist/typescript/module/src/components/base/carousel/utils.d.ts.map +1 -0
  42. package/dist/typescript/module/src/components/base/index.d.ts +1 -0
  43. package/dist/typescript/module/src/components/base/index.d.ts.map +1 -1
  44. package/package.json +3 -2
  45. package/src/components/base/carousel/carousel-item.tsx +11 -0
  46. package/src/components/base/carousel/carousel.tsx +691 -0
  47. package/src/components/base/carousel/index.ts +8 -0
  48. package/src/components/base/carousel/style/index.ts +42 -0
  49. package/src/components/base/carousel/types.ts +134 -0
  50. package/src/components/base/carousel/utils.ts +26 -0
  51. package/src/components/base/index.ts +1 -0
@@ -0,0 +1,691 @@
1
+ import { clamp } from 'lodash-es';
2
+ import React, {
3
+ forwardRef,
4
+ type ReactElement,
5
+ useCallback,
6
+ useEffect,
7
+ useImperativeHandle,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ } from 'react';
12
+ import {
13
+ Animated,
14
+ Easing,
15
+ type LayoutChangeEvent,
16
+ PanResponder,
17
+ type PanResponderInstance,
18
+ type StyleProp,
19
+ StyleSheet,
20
+ View,
21
+ type ViewStyle,
22
+ } from 'react-native';
23
+
24
+ import {
25
+ getDefaultTotal,
26
+ isRenderPropChildren,
27
+ normalizeChildren,
28
+ } from '#components/base/carousel/utils';
29
+ import { useTheme } from '#native-provider';
30
+
31
+ import { useCarouselStyles } from './style';
32
+ import type { CarouselProps, CarouselRef } from './types';
33
+
34
+ const getBoundaryIndexRange = ({
35
+ total,
36
+ slideSize,
37
+ trackOffset,
38
+ stuckAtBoundary,
39
+ }: {
40
+ total: number;
41
+ slideSize: number;
42
+ trackOffset: number;
43
+ stuckAtBoundary: boolean;
44
+ }) => {
45
+ if (total <= 0) return { min: 0, max: 0 };
46
+ if (!stuckAtBoundary) return { min: 0, max: total - 1 };
47
+
48
+ const slideRatio = slideSize / 100;
49
+ const offsetRatio = trackOffset / 100;
50
+
51
+ const min = 0 + offsetRatio / (slideRatio || 1);
52
+ const max = total - 1 - (1 - slideRatio - offsetRatio) / (slideRatio || 1);
53
+ return { min, max };
54
+ };
55
+
56
+ interface LayoutState {
57
+ width: number;
58
+ height: number;
59
+ }
60
+
61
+ /**
62
+ * 轮播图
63
+ */
64
+ export const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
65
+ const {
66
+ defaultIndex = 0,
67
+ allowTouchMove = true,
68
+ autoplay = false,
69
+ autoplayInterval = 3000,
70
+ loop = false,
71
+ direction = 'horizontal',
72
+ onIndexChange,
73
+ indicatorProps,
74
+ indicator,
75
+ slideSize = 100,
76
+ trackOffset = 0,
77
+ stuckAtBoundary = true,
78
+ rubberband = true,
79
+ virtualOverscan = 2,
80
+ total: totalProp,
81
+ children,
82
+ style,
83
+ } = props;
84
+
85
+ const styles = useCarouselStyles();
86
+ const theme = useTheme();
87
+
88
+ const [layout, setLayout] = useState<LayoutState>({ width: 0, height: 0 });
89
+ const [dragging, setDragging] = useState(false);
90
+ const [animating, setAnimating] = useState(false);
91
+ const [current, setCurrent] = useState(0);
92
+
93
+ const position = useRef(new Animated.Value(0));
94
+ const positionValueRef = useRef(0);
95
+
96
+ const normalizedSlideSize = useMemo(() => {
97
+ if (!Number.isFinite(slideSize)) return 100;
98
+ return clamp(slideSize, 0, 100);
99
+ }, [slideSize]);
100
+
101
+ const normalizedTrackOffset = useMemo(() => {
102
+ if (stuckAtBoundary) {
103
+ return 100 - normalizedSlideSize;
104
+ }
105
+ if (!Number.isFinite(trackOffset)) return 0;
106
+ return clamp(trackOffset, 0, 100 - normalizedSlideSize);
107
+ }, [normalizedSlideSize, trackOffset, stuckAtBoundary]);
108
+
109
+ const total = useMemo(() => {
110
+ const normalizedDefault = getDefaultTotal(children);
111
+ return totalProp ?? normalizedDefault;
112
+ }, [children, totalProp]);
113
+
114
+ const loopEnabled = loop && total > 1;
115
+ // 若是非全屏的轮播项,则在循环播放开启的情况下,则会在下一轮展示第一项时看见下一项为空白(或闪烁),这需要额外渲染更多的项(与offset、slideSize相关)
116
+ const clonesBefore = loopEnabled
117
+ ? Math.max(
118
+ 1,
119
+ normalizedSlideSize > 0
120
+ ? Math.ceil(normalizedTrackOffset / normalizedSlideSize) + 1
121
+ : 1,
122
+ )
123
+ : 0;
124
+ const clonesAfter = loopEnabled
125
+ ? Math.max(
126
+ 1,
127
+ normalizedSlideSize > 0
128
+ ? Math.ceil(
129
+ (100 - normalizedTrackOffset - normalizedSlideSize) /
130
+ normalizedSlideSize,
131
+ ) + 1
132
+ : 1,
133
+ )
134
+ : 0;
135
+ const extTotal = loopEnabled ? total + clonesBefore + clonesAfter : total;
136
+
137
+ const slidePixels = useMemo(() => {
138
+ const trackSize = direction === 'horizontal' ? layout.width : layout.height;
139
+ if (trackSize <= 0) return 0;
140
+ return trackSize * (normalizedSlideSize / 100);
141
+ }, [direction, layout.height, layout.width, normalizedSlideSize]);
142
+
143
+ const trackOffsetPixels = useMemo(() => {
144
+ const trackSize = direction === 'horizontal' ? layout.width : layout.height;
145
+ if (trackSize <= 0) return 0;
146
+ return trackSize * (normalizedTrackOffset / 100);
147
+ }, [direction, layout.height, layout.width, normalizedTrackOffset]);
148
+
149
+ const extIndexRef = useRef(0);
150
+ const reportIndexRef = useRef<number | null>(null);
151
+
152
+ const staticSlides = useMemo(() => {
153
+ if (isRenderPropChildren(children)) return [];
154
+ return normalizeChildren(children, total);
155
+ }, [children, total]);
156
+
157
+ const updateCurrent = useCallback(
158
+ (next: number) => {
159
+ const normalized = total > 0 ? clamp(next, 0, total - 1) : 0;
160
+ setCurrent(normalized);
161
+ },
162
+ [total],
163
+ );
164
+
165
+ const maybeReportIndex = useCallback(
166
+ (index: number) => {
167
+ if (!onIndexChange) return;
168
+ if (reportIndexRef.current === index) return;
169
+ reportIndexRef.current = index;
170
+ onIndexChange(index);
171
+ },
172
+ [onIndexChange],
173
+ );
174
+
175
+ const getExtIndexFromIndex = useCallback(
176
+ (index: number) => {
177
+ if (!loopEnabled) return index;
178
+ return clamp(index, 0, total - 1) + clonesBefore;
179
+ },
180
+ [clonesBefore, loopEnabled, total],
181
+ );
182
+
183
+ const getIndexFromExtIndex = useCallback(
184
+ (extIndex: number) => {
185
+ if (!loopEnabled) return clamp(extIndex, 0, total - 1);
186
+ if (extIndex < clonesBefore) {
187
+ return (((extIndex - clonesBefore) % total) + total) % total;
188
+ }
189
+ if (extIndex >= clonesBefore + total) {
190
+ return (extIndex - clonesBefore - total) % total;
191
+ }
192
+ return extIndex - clonesBefore;
193
+ },
194
+ [clonesBefore, loopEnabled, total],
195
+ );
196
+
197
+ const getTargetExtIndexForIndex = useCallback(
198
+ (targetIndex: number) => {
199
+ if (!loopEnabled) return clamp(targetIndex, 0, total - 1);
200
+
201
+ const currentExt = extIndexRef.current;
202
+ const normalizedTarget = clamp(targetIndex, 0, total - 1);
203
+ const targetExt = normalizedTarget + clonesBefore;
204
+
205
+ if (currentExt === clonesBefore && normalizedTarget === total - 1)
206
+ return clonesBefore - 1;
207
+ if (currentExt === clonesBefore + total - 1 && normalizedTarget === 0)
208
+ return clonesBefore + total;
209
+ return targetExt;
210
+ },
211
+ [clonesBefore, loopEnabled, total],
212
+ );
213
+
214
+ const getBoundedPosition = useCallback(
215
+ (nextPosition: number) => {
216
+ if (loopEnabled) return nextPosition;
217
+ if (slidePixels <= 0) return nextPosition;
218
+ if (total <= 0) return 0;
219
+
220
+ const { min, max } = getBoundaryIndexRange({
221
+ total,
222
+ slideSize: normalizedSlideSize,
223
+ trackOffset: normalizedTrackOffset,
224
+ stuckAtBoundary,
225
+ });
226
+ const minPos = min * slidePixels;
227
+ const maxPos = max * slidePixels;
228
+
229
+ if (nextPosition < minPos) {
230
+ if (!rubberband) return minPos;
231
+ return minPos - (minPos - nextPosition) * 0.35;
232
+ }
233
+ if (nextPosition > maxPos) {
234
+ if (!rubberband) return maxPos;
235
+ return maxPos + (nextPosition - maxPos) * 0.35;
236
+ }
237
+ return nextPosition;
238
+ },
239
+ [
240
+ loopEnabled,
241
+ rubberband,
242
+ slidePixels,
243
+ normalizedSlideSize,
244
+ stuckAtBoundary,
245
+ total,
246
+ normalizedTrackOffset,
247
+ ],
248
+ );
249
+
250
+ const animateTo = useCallback((toValue: number, onFinished?: () => void) => {
251
+ const listenerId = position.current.addListener(({ value }) => {
252
+ positionValueRef.current = value;
253
+ });
254
+ Animated.timing(position.current, {
255
+ toValue,
256
+ duration: 300,
257
+ easing: Easing.out(Easing.cubic),
258
+ useNativeDriver: true,
259
+ }).start(({ finished }) => {
260
+ position.current.removeListener(listenerId);
261
+ if (finished) {
262
+ positionValueRef.current = toValue;
263
+ onFinished?.();
264
+ }
265
+ });
266
+ }, []);
267
+
268
+ const setPositionImmediate = useCallback((toValue: number) => {
269
+ position.current.stopAnimation();
270
+ position.current.setValue(toValue);
271
+ positionValueRef.current = toValue;
272
+ }, []);
273
+
274
+ const finalizeLoopJumpIfNeeded = useCallback(() => {
275
+ if (!loopEnabled) return;
276
+ const currentExt = extIndexRef.current;
277
+ if (currentExt < clonesBefore) {
278
+ const targetExt = currentExt + total;
279
+ extIndexRef.current = targetExt;
280
+ if (slidePixels > 0) setPositionImmediate(targetExt * slidePixels);
281
+ return;
282
+ }
283
+ if (currentExt >= clonesBefore + total) {
284
+ const targetExt = currentExt - total;
285
+ extIndexRef.current = targetExt;
286
+ if (slidePixels > 0) setPositionImmediate(targetExt * slidePixels);
287
+ }
288
+ }, [clonesBefore, loopEnabled, setPositionImmediate, slidePixels, total]);
289
+
290
+ const swipeToExtIndex = useCallback(
291
+ (targetExtIndex: number) => {
292
+ if (total <= 0) return;
293
+ if (slidePixels <= 0) {
294
+ extIndexRef.current = clamp(targetExtIndex, 0, extTotal - 1);
295
+ setAnimating(false);
296
+ updateCurrent(getIndexFromExtIndex(extIndexRef.current));
297
+ return;
298
+ }
299
+
300
+ const boundedExtIndex = clamp(targetExtIndex, 0, extTotal - 1);
301
+ extIndexRef.current = boundedExtIndex;
302
+
303
+ const nextIndex = getIndexFromExtIndex(boundedExtIndex);
304
+ updateCurrent(nextIndex);
305
+
306
+ const rawPosition = boundedExtIndex * slidePixels;
307
+ const toPosition = getBoundedPosition(rawPosition);
308
+
309
+ setAnimating(true);
310
+ animateTo(toPosition, () => {
311
+ finalizeLoopJumpIfNeeded();
312
+ const normalizedIndex = getIndexFromExtIndex(extIndexRef.current);
313
+ updateCurrent(normalizedIndex);
314
+ maybeReportIndex(normalizedIndex);
315
+ setAnimating(false);
316
+ });
317
+ },
318
+ [
319
+ animateTo,
320
+ extTotal,
321
+ finalizeLoopJumpIfNeeded,
322
+ getBoundedPosition,
323
+ getIndexFromExtIndex,
324
+ maybeReportIndex,
325
+ slidePixels,
326
+ total,
327
+ updateCurrent,
328
+ ],
329
+ );
330
+
331
+ const swipeTo = useCallback(
332
+ (index: number) => {
333
+ if (total <= 0) return;
334
+ const targetIndex = clamp(Math.round(index), 0, total - 1);
335
+ const targetExtIndex = getTargetExtIndexForIndex(targetIndex);
336
+ swipeToExtIndex(targetExtIndex);
337
+ },
338
+ [getTargetExtIndexForIndex, swipeToExtIndex, total],
339
+ );
340
+
341
+ const swipeNext = useCallback(() => {
342
+ if (total <= 1) return;
343
+ let currentExt = extIndexRef.current;
344
+ // 若当前处于 after-clone 区域(快速连击导致 finalizeLoopJumpIfNeeded 尚未执行),
345
+ // 立即将位置平移回真实 item 区域(视觉内容相同,不可感知),再继续前进
346
+ if (clonesBefore > 0 && currentExt >= clonesBefore + total) {
347
+ currentExt -= total;
348
+ extIndexRef.current = currentExt;
349
+ if (slidePixels > 0) setPositionImmediate(currentExt * slidePixels);
350
+ }
351
+ swipeToExtIndex(currentExt + 1);
352
+ }, [clonesBefore, setPositionImmediate, slidePixels, swipeToExtIndex, total]);
353
+
354
+ const swipePrev = useCallback(() => {
355
+ if (total <= 1) return;
356
+ let currentExt = extIndexRef.current;
357
+ // 若当前处于 before-clone 区域(快速连击导致 finalizeLoopJumpIfNeeded 尚未执行),
358
+ // 立即将位置平移回真实 item 区域(视觉内容相同,不可感知),再继续后退
359
+ if (clonesBefore > 0 && currentExt < clonesBefore) {
360
+ currentExt += total;
361
+ extIndexRef.current = currentExt;
362
+ if (slidePixels > 0) setPositionImmediate(currentExt * slidePixels);
363
+ }
364
+ swipeToExtIndex(currentExt - 1);
365
+ }, [clonesBefore, setPositionImmediate, slidePixels, swipeToExtIndex, total]);
366
+
367
+ useImperativeHandle(
368
+ ref,
369
+ () => ({
370
+ swipeTo,
371
+ swipeNext,
372
+ swipePrev,
373
+ }),
374
+ [swipeNext, swipePrev, swipeTo],
375
+ );
376
+
377
+ useEffect(() => {
378
+ if (total <= 0) {
379
+ updateCurrent(0);
380
+ reportIndexRef.current = null;
381
+ extIndexRef.current = 0;
382
+ setPositionImmediate(0);
383
+ return;
384
+ }
385
+
386
+ const normalizedDefault = clamp(Math.round(defaultIndex), 0, total - 1);
387
+ updateCurrent(normalizedDefault);
388
+ reportIndexRef.current = normalizedDefault;
389
+
390
+ const extIndex = getExtIndexFromIndex(normalizedDefault);
391
+ extIndexRef.current = extIndex;
392
+ if (slidePixels > 0) {
393
+ setPositionImmediate(getBoundedPosition(extIndex * slidePixels));
394
+ }
395
+ }, [
396
+ defaultIndex,
397
+ getExtIndexFromIndex,
398
+ getBoundedPosition,
399
+ setPositionImmediate,
400
+ slidePixels,
401
+ total,
402
+ updateCurrent,
403
+ ]);
404
+
405
+ useEffect(() => {
406
+ if (slidePixels <= 0) return;
407
+ const extIndex = extIndexRef.current;
408
+ setPositionImmediate(getBoundedPosition(extIndex * slidePixels));
409
+ }, [getBoundedPosition, setPositionImmediate, slidePixels]);
410
+
411
+ // 每当 current 变化时,若允许自动轮播 且 开启了自动轮播,则执行轮播
412
+ useEffect(() => {
413
+ if (!autoplay) return;
414
+ if (total <= 1) return;
415
+ if (dragging) return;
416
+ if (autoplayInterval <= 0) return;
417
+
418
+ const timer = setTimeout(() => {
419
+ if (autoplay === 'reverse') swipePrev();
420
+ else swipeNext();
421
+ }, autoplayInterval);
422
+
423
+ return () => {
424
+ clearTimeout(timer);
425
+ };
426
+ }, [autoplay, autoplayInterval, dragging, swipeNext, swipePrev, total, current]);
427
+
428
+ const onLayoutTrack = useCallback((e: LayoutChangeEvent) => {
429
+ const { width, height } = e.nativeEvent.layout;
430
+ setLayout(prev => {
431
+ if (prev.width === width && prev.height === height) return prev;
432
+ return { width, height };
433
+ });
434
+ }, []);
435
+
436
+ const panResponder: PanResponderInstance | null = useMemo(() => {
437
+ if (!allowTouchMove) return null;
438
+ if (total <= 1) return null;
439
+ if (slidePixels <= 0) return null;
440
+
441
+ let startPosition = 0;
442
+ const axis = direction === 'horizontal' ? 'x' : 'y';
443
+
444
+ const shouldSet = (_: unknown, gestureState: { dx: number; dy: number }) => {
445
+ const primary = axis === 'x' ? gestureState.dx : gestureState.dy;
446
+ // const secondary = axis === 'x' ? gestureState.dy : gestureState.dx;
447
+ if (Math.abs(primary) < 5) return false;
448
+ // return Math.abs(primary) >= Math.abs(secondary);
449
+ return true;
450
+ };
451
+
452
+ return PanResponder.create({
453
+ onMoveShouldSetPanResponder: shouldSet,
454
+ onPanResponderGrant: () => {
455
+ setDragging(true);
456
+ setAnimating(false);
457
+ position.current.stopAnimation();
458
+ startPosition = positionValueRef.current;
459
+ },
460
+ onPanResponderMove: (_evt, gestureState) => {
461
+ const delta = axis === 'x' ? gestureState.dx : gestureState.dy;
462
+ let nextPosition = startPosition - delta;
463
+
464
+ // loop 开启时,拖动到最外层有效克隆中心之外则循环偏移,保证视口内始终有内容
465
+ // minPos/maxPos 是 outermost before/after clone 的中心位置,由 clonesBefore 公式保证此范围内视口始终被覆盖
466
+ if (extTotal > total && slidePixels > 0) {
467
+ const totalPixels = total * slidePixels;
468
+ const minPos = (clonesBefore - 1) * slidePixels;
469
+ const maxPos = (clonesBefore + total) * slidePixels;
470
+ while (nextPosition < minPos) {
471
+ nextPosition += totalPixels;
472
+ startPosition += totalPixels;
473
+ }
474
+ while (nextPosition > maxPos) {
475
+ nextPosition -= totalPixels;
476
+ startPosition -= totalPixels;
477
+ }
478
+ }
479
+
480
+ const bounded = getBoundedPosition(nextPosition);
481
+ position.current.setValue(bounded);
482
+ positionValueRef.current = bounded;
483
+
484
+ const nextExtIndex = clamp(
485
+ Math.round(bounded / slidePixels),
486
+ 0,
487
+ extTotal - 1,
488
+ );
489
+ extIndexRef.current = nextExtIndex;
490
+ updateCurrent(getIndexFromExtIndex(nextExtIndex));
491
+ },
492
+ onPanResponderRelease: (_evt, gestureState) => {
493
+ setDragging(false);
494
+ const velocity = axis === 'x' ? gestureState.vx : gestureState.vy;
495
+ position.current.stopAnimation(value => {
496
+ const baseIndex = value / slidePixels;
497
+ const projectedIndex = (value - velocity * 2000) / slidePixels;
498
+
499
+ const minAdj = Math.floor(baseIndex);
500
+ const maxAdj = minAdj + 1;
501
+ const rounded = Math.round(projectedIndex);
502
+ const adjacent = clamp(rounded, minAdj, maxAdj);
503
+ swipeToExtIndex(adjacent);
504
+ });
505
+ },
506
+ onPanResponderTerminate: () => {
507
+ setDragging(false);
508
+ swipeToExtIndex(extIndexRef.current);
509
+ },
510
+ onPanResponderTerminationRequest: () => false,
511
+ onShouldBlockNativeResponder: () => true,
512
+ });
513
+ }, [
514
+ allowTouchMove,
515
+ clonesBefore,
516
+ direction,
517
+ extTotal,
518
+ getBoundedPosition,
519
+ getIndexFromExtIndex,
520
+ slidePixels,
521
+ swipeToExtIndex,
522
+ total,
523
+ updateCurrent,
524
+ ]);
525
+
526
+ const trackTransformStyle: StyleProp<ViewStyle> = useMemo(() => {
527
+ const translate = Animated.add(
528
+ Animated.multiply(position.current, -1),
529
+ trackOffsetPixels,
530
+ );
531
+ return direction === 'horizontal'
532
+ ? { transform: [{ translateX: translate }] }
533
+ : { transform: [{ translateY: translate }] };
534
+ }, [direction, trackOffsetPixels]);
535
+
536
+ const renderSlides = useCallback(() => {
537
+ if (total <= 0) return null;
538
+ if (slidePixels <= 0) return null;
539
+
540
+ const isVirtual =
541
+ isRenderPropChildren(children) && typeof totalProp === 'number';
542
+ const effectiveOverscan = isVirtual
543
+ ? Math.max(virtualOverscan, dragging || animating ? 1 : 0)
544
+ : virtualOverscan;
545
+
546
+ const currentExtIndex = extIndexRef.current;
547
+ const start = isVirtual
548
+ ? clamp(currentExtIndex - effectiveOverscan, 0, extTotal - 1)
549
+ : 0;
550
+ const end = isVirtual
551
+ ? clamp(currentExtIndex + effectiveOverscan, 0, extTotal - 1)
552
+ : extTotal - 1;
553
+
554
+ const leadingSize = start * slidePixels;
555
+ const trailingSize = (extTotal - end - 1) * slidePixels;
556
+
557
+ const leadSpacerStyle: ViewStyle =
558
+ direction === 'horizontal' ? { width: leadingSize } : { height: leadingSize };
559
+ const trailSpacerStyle: ViewStyle =
560
+ direction === 'horizontal'
561
+ ? { width: trailingSize }
562
+ : { height: trailingSize };
563
+
564
+ const slideWrapperBaseStyle: ViewStyle =
565
+ direction === 'horizontal'
566
+ ? { width: slidePixels, height: '100%' }
567
+ : { height: slidePixels, width: '100%' };
568
+
569
+ const getElementForExtIndex = (extIndex: number) => {
570
+ const logicalIndex = loopEnabled
571
+ ? getIndexFromExtIndex(extIndex)
572
+ : clamp(extIndex, 0, total - 1);
573
+
574
+ if (isRenderPropChildren(children)) return children(logicalIndex);
575
+ return staticSlides[logicalIndex];
576
+ };
577
+
578
+ const slideElements: ReactElement[] = [];
579
+ for (let extIndex = start; extIndex <= end; extIndex += 1) {
580
+ const element = getElementForExtIndex(extIndex);
581
+ if (!element) continue;
582
+ slideElements.push(
583
+ <View key={extIndex} style={[styles.slide, slideWrapperBaseStyle]}>
584
+ {element}
585
+ </View>,
586
+ );
587
+ }
588
+
589
+ const leadSpacer =
590
+ isVirtual && leadingSize > 0 ? <View style={leadSpacerStyle} /> : null;
591
+ const trailSpacer =
592
+ isVirtual && trailingSize > 0 ? <View style={trailSpacerStyle} /> : null;
593
+
594
+ return (
595
+ <>
596
+ {leadSpacer}
597
+ {slideElements}
598
+ {trailSpacer}
599
+ </>
600
+ );
601
+ }, [
602
+ children,
603
+ direction,
604
+ extTotal,
605
+ getIndexFromExtIndex,
606
+ dragging,
607
+ animating,
608
+ loopEnabled,
609
+ slidePixels,
610
+ staticSlides,
611
+ styles.slide,
612
+ total,
613
+ totalProp,
614
+ virtualOverscan,
615
+ ]);
616
+
617
+ const renderIndicator = useCallback(() => {
618
+ if (indicator === false) return null;
619
+ if (total <= 1) return null;
620
+
621
+ if (typeof indicator === 'function') {
622
+ return indicator(total, current);
623
+ }
624
+
625
+ const activeColor = indicatorProps?.color ?? theme.palette.brand7;
626
+ const inactiveColor = theme.palette.fontGray5;
627
+
628
+ return (
629
+ <View
630
+ style={StyleSheet.flatten([
631
+ styles.indicatorContainer,
632
+ indicatorProps?.style,
633
+ ])}
634
+ >
635
+ {Array.from({ length: total }, (_, i) => {
636
+ const isActive = i === current;
637
+ return (
638
+ <View
639
+ key={i}
640
+ style={StyleSheet.flatten([
641
+ styles.indicatorDot,
642
+ { backgroundColor: isActive ? activeColor : inactiveColor },
643
+ isActive && styles.indicatorDotActive,
644
+ ])}
645
+ />
646
+ );
647
+ })}
648
+ </View>
649
+ );
650
+ }, [
651
+ current,
652
+ indicator,
653
+ indicatorProps?.color,
654
+ indicatorProps?.style,
655
+ styles.indicatorContainer,
656
+ styles.indicatorDot,
657
+ styles.indicatorDotActive,
658
+ theme.palette.brand7,
659
+ theme.palette.fontGray5,
660
+ total,
661
+ ]);
662
+
663
+ const trackInnerStyle: ViewStyle = useMemo(() => {
664
+ return direction === 'horizontal'
665
+ ? { flexDirection: 'row', height: '100%' }
666
+ : { flexDirection: 'column', width: '100%' };
667
+ }, [direction]);
668
+
669
+ return (
670
+ <View style={StyleSheet.flatten([styles.container, style])}>
671
+ <View
672
+ style={styles.track}
673
+ onLayout={onLayoutTrack}
674
+ {...panResponder?.panHandlers}
675
+ >
676
+ <Animated.View
677
+ style={StyleSheet.flatten([
678
+ styles.trackInner,
679
+ trackInnerStyle,
680
+ trackTransformStyle,
681
+ ])}
682
+ >
683
+ {renderSlides()}
684
+ </Animated.View>
685
+ {renderIndicator()}
686
+ </View>
687
+ </View>
688
+ );
689
+ });
690
+
691
+ Carousel.displayName = 'Carousel';
@@ -0,0 +1,8 @@
1
+ import { Carousel as _Carousel } from './carousel';
2
+ import { CarouselItem } from './carousel-item';
3
+
4
+ export * from './carousel';
5
+ export * from './carousel-item';
6
+ export type * from './types';
7
+
8
+ export const Carousel = Object.assign(_Carousel, { Item: CarouselItem });