@r0b0t3d/react-native-carousel 3.4.5 → 3.4.7

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 (64) hide show
  1. package/lib/commonjs/components/Carousel.js +290 -195
  2. package/lib/commonjs/components/Carousel.js.map +1 -1
  3. package/lib/commonjs/components/CarouselContainer.js +17 -31
  4. package/lib/commonjs/components/CarouselContainer.js.map +1 -1
  5. package/lib/commonjs/components/PageItem.js +53 -46
  6. package/lib/commonjs/components/PageItem.js.map +1 -1
  7. package/lib/commonjs/components/PaginationIndicator.js +23 -34
  8. package/lib/commonjs/components/PaginationIndicator.js.map +1 -1
  9. package/lib/commonjs/components/useCarouselContext.js +3 -8
  10. package/lib/commonjs/components/useCarouselContext.js.map +1 -1
  11. package/lib/commonjs/components/useInternalCarouselContext.js +3 -6
  12. package/lib/commonjs/components/useInternalCarouselContext.js.map +1 -1
  13. package/lib/commonjs/index.js +7 -17
  14. package/lib/commonjs/index.js.map +1 -1
  15. package/lib/commonjs/package.json +1 -0
  16. package/lib/commonjs/types.js +4 -0
  17. package/lib/commonjs/types.js.map +1 -1
  18. package/lib/commonjs/utils.js +24 -18
  19. package/lib/commonjs/utils.js.map +1 -1
  20. package/lib/module/components/Carousel.js +291 -180
  21. package/lib/module/components/Carousel.js.map +1 -1
  22. package/lib/module/components/CarouselContainer.js +18 -23
  23. package/lib/module/components/CarouselContainer.js.map +1 -1
  24. package/lib/module/components/PageItem.js +54 -38
  25. package/lib/module/components/PageItem.js.map +1 -1
  26. package/lib/module/components/PaginationIndicator.js +23 -24
  27. package/lib/module/components/PaginationIndicator.js.map +1 -1
  28. package/lib/module/components/useCarouselContext.js +2 -2
  29. package/lib/module/components/useCarouselContext.js.map +1 -1
  30. package/lib/module/components/useInternalCarouselContext.js +2 -0
  31. package/lib/module/components/useInternalCarouselContext.js.map +1 -1
  32. package/lib/module/index.js +2 -0
  33. package/lib/module/index.js.map +1 -1
  34. package/lib/module/types.js +2 -0
  35. package/lib/module/types.js.map +1 -1
  36. package/lib/module/utils.js +22 -12
  37. package/lib/module/utils.js.map +1 -1
  38. package/lib/typescript/components/Carousel.d.ts +3 -1
  39. package/lib/typescript/components/Carousel.d.ts.map +1 -0
  40. package/lib/typescript/components/CarouselContainer.d.ts +3 -2
  41. package/lib/typescript/components/CarouselContainer.d.ts.map +1 -0
  42. package/lib/typescript/components/PageItem.d.ts +5 -3
  43. package/lib/typescript/components/PageItem.d.ts.map +1 -0
  44. package/lib/typescript/components/PaginationIndicator.d.ts +3 -1
  45. package/lib/typescript/components/PaginationIndicator.d.ts.map +1 -0
  46. package/lib/typescript/components/useCarouselContext.d.ts +1 -1
  47. package/lib/typescript/components/useCarouselContext.d.ts.map +1 -0
  48. package/lib/typescript/components/useInternalCarouselContext.d.ts +1 -1
  49. package/lib/typescript/components/useInternalCarouselContext.d.ts.map +1 -0
  50. package/lib/typescript/index.d.ts +1 -0
  51. package/lib/typescript/index.d.ts.map +1 -0
  52. package/lib/typescript/types.d.ts +17 -15
  53. package/lib/typescript/types.d.ts.map +1 -0
  54. package/lib/typescript/utils.d.ts +4 -1
  55. package/lib/typescript/utils.d.ts.map +1 -0
  56. package/package.json +26 -29
  57. package/src/components/Carousel.tsx +404 -206
  58. package/src/components/CarouselContainer.tsx +4 -4
  59. package/src/components/PageItem.tsx +55 -34
  60. package/src/components/PaginationIndicator.tsx +2 -2
  61. package/src/components/useCarouselContext.ts +0 -1
  62. package/src/components/useInternalCarouselContext.ts +0 -1
  63. package/src/types.ts +9 -7
  64. package/src/utils.ts +31 -11
@@ -1,19 +1,46 @@
1
- function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
1
+ "use strict";
2
2
 
3
3
  /* eslint-disable @typescript-eslint/ban-ts-comment */
4
- import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
5
- import { View, Dimensions, StyleSheet, Platform } from 'react-native';
4
+ import React, { useState, useEffect, useCallback, useMemo } from 'react';
5
+ import { Dimensions, StyleSheet, Platform, AppState } from 'react-native';
6
6
  import { useInterval } from '@r0b0t3d/react-native-hooks';
7
- import Animated, { useSharedValue, useAnimatedScrollHandler, runOnJS, useAnimatedRef } from 'react-native-reanimated';
7
+ import Animated, { useSharedValue, useAnimatedScrollHandler, useAnimatedRef, scrollTo } from 'react-native-reanimated';
8
+ import { scheduleOnRN, scheduleOnUI } from 'react-native-worklets';
8
9
  import PageItem from './PageItem';
9
- import { findNearestPage, generateOffsets } from '../utils';
10
+ import { findNearestPage, generateOffsets, getLogicalPage, getLoopBoundaryTarget } from '../utils';
10
11
  import { useCarouselContext } from './useCarouselContext';
11
12
  import { useInternalCarouselContext } from './useInternalCarouselContext';
12
13
 
14
+ // ---------------------------------------------------------------------------
15
+ // Carousel state machine
16
+ // ---------------------------------------------------------------------------
17
+ // Transitions:
18
+ //
19
+ // IDLE ── onBeginDrag ──► DRAGGING
20
+ // DRAGGING ── onMomentumEnd ──► IDLE (or LOOP_JUMP at boundary)
21
+ // LOOP_JUMP ── onScroll (arrival) ──► IDLE
22
+ // LOOP_JUMP ── onBeginDrag (user overrides) ──► DRAGGING
23
+ //
24
+ // Page change (onPageChange / animatedPage) only fires at IDLE transitions.
25
+ // Autoplay only advances when state === IDLE.
26
+ // ---------------------------------------------------------------------------
27
+ import { jsx as _jsx } from "react/jsx-runtime";
28
+ var CarouselState = /*#__PURE__*/function (CarouselState) {
29
+ CarouselState[CarouselState["IDLE"] = 0] = "IDLE";
30
+ CarouselState[CarouselState["DRAGGING"] = 1] = "DRAGGING";
31
+ CarouselState[CarouselState["LOOP_JUMP"] = 2] = "LOOP_JUMP";
32
+ return CarouselState;
33
+ }(CarouselState || {}); // ---------------------------------------------------------------------------
34
+ // Terminology
35
+ // ---------------------------------------------------------------------------
36
+ // render index – position in the padded pageItems array (includes loop
37
+ // clones at head/tail). Used for scroll offsets.
38
+ // logical index – position in the original data array (0 .. data.length-1).
39
+ // All consumer-facing callbacks receive logical indices.
40
+ // ---------------------------------------------------------------------------
13
41
  const getScreenWidth = () => {
14
42
  return Dimensions.get('screen').width;
15
43
  };
16
-
17
44
  function Carousel({
18
45
  style,
19
46
  data,
@@ -34,26 +61,36 @@ function Carousel({
34
61
  onPageChange,
35
62
  scrollViewProps = {},
36
63
  keyExtractor,
37
- onItemPress
64
+ onItemPress,
65
+ disableItemPress = false,
66
+ scrollViewRef: externalScrollViewRef
38
67
  }) {
39
- const currentPage = useSharedValue(0);
68
+ // ---- shared values (UI-thread state) ------------------------------------
69
+
70
+ const currentRenderPage = useSharedValue(0);
71
+ const currentLogicalPage = useSharedValue(0);
40
72
  const animatedScroll = useSharedValue(0);
73
+ const carouselState = useSharedValue(CarouselState.IDLE);
74
+ // Destination render page for a loop jump. Used to detect arrival via
75
+ // onScroll and release the jump lock.
76
+ const jumpDestination = useSharedValue(-1);
77
+ // Suppresses parallax/opacity animations during instant scrolls on
78
+ // Android where the scroll isn't truly synchronous.
41
79
  const freeze = useSharedValue(false);
42
- const [isDragging, setDragging] = useState(false);
43
- const expectedPosition = useSharedValue(-1);
44
- const pageMapper = useRef({});
80
+ // When false, autoplay is suppressed (app backgrounded).
81
+ const appActive = useSharedValue(true);
82
+ const internalAnimatedRef = useAnimatedRef();
45
83
  const {
46
84
  currentPage: animatedPage,
47
85
  totalPage
48
86
  } = useCarouselContext();
49
- useEffect(() => {
50
- animatedScroll.value = currentPage.value * sliderWidth;
51
- }, []);
87
+
88
+ // ---- derived values -----------------------------------------------------
89
+
52
90
  const horizontalPadding = useMemo(() => {
53
91
  const padding = (sliderWidth - itemWidth) / 2;
54
92
  return firstItemAlignment === 'center' || loop ? padding : spaceHeadTail;
55
93
  }, [sliderWidth, itemWidth, firstItemAlignment, loop, spaceHeadTail]);
56
- const scrollViewRef = useAnimatedRef();
57
94
  const offsets = useMemo(() => {
58
95
  return generateOffsets({
59
96
  sliderWidth,
@@ -61,89 +98,122 @@ function Carousel({
61
98
  itemCount: data.length + (loop ? additionalPagesPerSide * 2 : 0),
62
99
  horizontalPadding
63
100
  });
64
- }, [sliderWidth, itemWidth, data, horizontalPadding]);
101
+ }, [sliderWidth, itemWidth, data.length, loop, additionalPagesPerSide, horizontalPadding]);
65
102
  const pageItems = useMemo(() => {
66
- if (!data) {
103
+ if (!data || data.length === 0) {
67
104
  return [];
68
105
  }
69
-
70
- totalPage.value = data.length;
71
-
72
106
  if (loop) {
73
107
  const headItems = data.slice(data.length - additionalPagesPerSide, data.length);
74
108
  const tailItems = data.slice(0, additionalPagesPerSide);
75
- const newItems = [...headItems, ...data, ...tailItems];
109
+ return [...headItems, ...data, ...tailItems];
110
+ }
111
+ return data;
112
+ }, [data, loop, additionalPagesPerSide]);
76
113
 
77
- for (let i = 0; i < newItems.length; i++) {
78
- pageMapper.current[i] = (data.length - additionalPagesPerSide + i) % data.length;
79
- }
114
+ // ---- side effects -------------------------------------------------------
80
115
 
81
- return newItems;
82
- } else {
83
- for (let i = 0; i < data.length; i++) {
84
- pageMapper.current[i] = i;
85
- }
116
+ useEffect(() => {
117
+ totalPage.value = data.length;
118
+ }, [data.length, totalPage]);
86
119
 
87
- return data;
120
+ // When the carousel layout or data changes (orientation, split-screen,
121
+ // toggling loop, data shrinking), reposition so the current logical
122
+ // page stays visible at its new render index and scroll offset.
123
+ useEffect(() => {
124
+ if (data.length === 0 || offsets.length === 0) return;
125
+
126
+ // Clamp logical page in case data shrank below current position.
127
+ const clampedLogical = Math.min(currentLogicalPage.value, data.length - 1);
128
+ const renderIdx = loop ? clampedLogical + additionalPagesPerSide : clampedLogical;
129
+
130
+ // Defensive: ensure render index is within bounds.
131
+ const safeRenderIdx = Math.max(0, Math.min(renderIdx, offsets.length - 1));
132
+ const safeLogicalIdx = getLogicalPage(safeRenderIdx, data.length, additionalPagesPerSide, loop);
133
+ currentRenderPage.value = safeRenderIdx;
134
+ currentLogicalPage.value = safeLogicalIdx;
135
+ animatedPage.value = safeLogicalIdx;
136
+ scheduleOnUI(idx => {
137
+ 'worklet';
138
+
139
+ scrollTo(internalAnimatedRef, offsets[idx], 0, false);
140
+ }, safeRenderIdx);
141
+ animatedScroll.value = offsets[safeRenderIdx] ?? 0;
142
+ // Fire onPageChange if logical page actually changed (e.g. data shrank).
143
+ if (safeLogicalIdx !== currentLogicalPage.value) {
144
+ scheduleOnRN(handlePageChange, safeLogicalIdx);
88
145
  }
89
- }, [data, loop]);
90
- const getActualPage = useCallback(page => {
91
- return pageMapper.current[page];
92
- }, []);
93
- const getRef = useCallback(() => {
94
- if (!scrollViewRef.current) return;
146
+ // eslint-disable-next-line react-hooks/exhaustive-deps
147
+ }, [data.length, sliderWidth, itemWidth, loop, additionalPagesPerSide]);
148
+
149
+ // ---- app-state listener (pause autoplay when backgrounded) --------------
150
+
151
+ useEffect(() => {
152
+ const sub = AppState.addEventListener('change', state => {
153
+ appActive.value = state === 'active';
154
+ });
155
+ return () => sub.remove();
156
+ }, [appActive]);
157
+
158
+ // ---- page-change callback (JS thread) -----------------------------------
95
159
 
96
- if (scrollViewRef.current.scrollTo) {
97
- return scrollViewRef.current;
160
+ const handlePageChange = useCallback(logicalPage => {
161
+ animatedPage.value = logicalPage;
162
+ if (onPageChange) {
163
+ onPageChange(logicalPage);
98
164
  }
165
+ }, [onPageChange, animatedPage]);
99
166
 
100
- return scrollViewRef.current.getNode();
101
- }, []);
167
+ // ---- scroll helpers -----------------------------------------------------
168
+
169
+ // Safe accessor: returns offset for a render index, clamping to valid
170
+ // range and falling back to 0 if offsets is empty.
171
+ const renderOffset = useCallback(renderIdx => {
172
+ if (offsets.length === 0) return 0;
173
+ const clamped = Math.max(0, Math.min(renderIdx, offsets.length - 1));
174
+ return offsets[clamped];
175
+ }, [offsets]);
102
176
  const handleScrollTo = useCallback((page, animated = true) => {
103
- if (getRef()) {
104
- getRef().scrollTo({
105
- x: offsets[page],
106
- y: 0,
107
- animated
108
- });
109
- }
110
- }, [getRef, offsets]);
111
- const jumpTo = useCallback((page, delay = 200) => {
112
- expectedPosition.value = page;
177
+ scheduleOnUI((pageIndex, isAnimated) => {
178
+ 'worklet';
113
179
 
114
- if (Platform.OS === 'android') {
115
- freeze.value = true;
116
- }
180
+ scrollTo(internalAnimatedRef, offsets[pageIndex], 0, isAnimated);
181
+ }, page, animated);
182
+ }, [offsets, internalAnimatedRef]);
183
+
184
+ // ---- navigation ---------------------------------------------------------
117
185
 
118
- setTimeout(() => {
119
- animatedScroll.value = offsets[page];
120
- handleScrollTo(page, false);
121
- }, delay);
122
- }, [handleScrollTo, animatedScroll, freeze]);
123
186
  const goNext = useCallback(() => {
124
- const next = currentPage.value + 1;
125
- handleScrollTo(next);
126
- }, [handleScrollTo]);
187
+ if (carouselState.value !== CarouselState.IDLE || data.length === 0) return;
188
+ const next = currentRenderPage.value + 1;
189
+ if (next < offsets.length) {
190
+ currentRenderPage.value = next;
191
+ handleScrollTo(next);
192
+ }
193
+ }, [handleScrollTo, offsets.length, data.length]);
127
194
  const goPrev = useCallback(() => {
128
- const prev = currentPage.value - 1;
129
- handleScrollTo(prev);
195
+ if (carouselState.value !== CarouselState.IDLE || data.length === 0) return;
196
+ const prev = currentRenderPage.value - 1;
197
+ if (prev >= 0) {
198
+ currentRenderPage.value = prev;
199
+ handleScrollTo(prev);
200
+ }
130
201
  }, [handleScrollTo]);
131
202
  const snapToItem = useCallback((index, animated = true) => {
203
+ if (data.length === 0) return;
132
204
  if (index < 0 || index >= data.length) {
133
205
  console.error(`Index not valid ${index}`);
134
206
  return;
135
207
  }
208
+ const renderIndex = loop ? index + additionalPagesPerSide : index;
209
+ const clamped = Math.max(0, Math.min(renderIndex, offsets.length - 1));
210
+ currentRenderPage.value = clamped;
211
+ currentLogicalPage.value = index;
212
+ handleScrollTo(clamped, animated);
213
+ }, [loop, additionalPagesPerSide, offsets.length, handleScrollTo, data.length]);
136
214
 
137
- let pageIndex = index;
138
-
139
- if (loop) {
140
- const indices = Object.keys(pageMapper.current).filter(idx => pageMapper.current[Number(idx)] === index).map(idx => Number(idx));
141
- const toIndex = findNearestPage(currentPage.value, indices, 10);
142
- pageIndex = indices[toIndex];
143
- }
215
+ // ---- context wiring -----------------------------------------------------
144
216
 
145
- handleScrollTo(pageIndex, animated);
146
- }, [handleScrollTo]);
147
217
  const {
148
218
  setCarouselHandlers
149
219
  } = useInternalCarouselContext();
@@ -156,128 +226,163 @@ function Carousel({
156
226
  });
157
227
  }
158
228
  }, [goNext, goPrev, snapToItem, setCarouselHandlers]);
159
- const handlePageChange = useCallback(page => {
160
- const actualPage = getActualPage(page);
161
- animatedPage.value = actualPage;
162
229
 
163
- if (onPageChange) {
164
- onPageChange(actualPage);
165
- }
166
-
167
- if (!loop) return;
168
-
169
- if (page === pageItems.length - 1) {
170
- jumpTo(additionalPagesPerSide * 2 - 1);
171
- } else if (page === 0) {
172
- jumpTo(pageItems.length - additionalPagesPerSide * 2);
173
- }
174
- }, [onPageChange, loop, getActualPage, jumpTo, pageItems]);
175
- const refreshPage = useCallback(offset => {
176
- 'worklet';
177
-
178
- const pageNum = findNearestPage(offset, offsets, 20);
230
+ // ---- autoplay -----------------------------------------------------------
179
231
 
180
- if (pageNum === -1) {
181
- return;
232
+ // isDragging acts as a debounce — autoplay resumes 200ms after the
233
+ // user lifts their finger, preventing an immediate auto-advance.
234
+ const [autoplayPaused, setAutoplayPaused] = useState(false);
235
+ useInterval(() => {
236
+ // Only advance when carousel is idle (not dragging, not in a
237
+ // loop jump) and the app is in the foreground.
238
+ if (carouselState.value === CarouselState.IDLE && appActive.value) {
239
+ goNext();
182
240
  }
241
+ }, !autoPlay || !loop || autoplayPaused ? -1 : duration);
183
242
 
184
- if (pageNum !== currentPage.value) {
185
- if (expectedPosition.value === pageNum) {
186
- freeze.value = false;
187
- }
243
+ // ---- initial page setup -------------------------------------------------
188
244
 
189
- currentPage.value = pageNum;
190
- runOnJS(handlePageChange)(pageNum);
191
- }
192
- }, [isDragging, offsets, handlePageChange]);
193
- useInterval(() => {
194
- goNext();
195
- }, !autoPlay || !loop || isDragging ? -1 : duration);
196
245
  useEffect(() => {
246
+ if (data.length === 0) return;
197
247
  if (initialPage < 0 || initialPage >= data.length) {
198
248
  console.error(`Invalid initialPage ${initialPage}`);
199
249
  return;
200
250
  }
251
+ const renderIndex = loop ? initialPage + additionalPagesPerSide : initialPage;
252
+ const safeIdx = Math.max(0, Math.min(renderIndex, offsets.length - 1));
253
+ currentRenderPage.value = safeIdx;
254
+ currentLogicalPage.value = initialPage;
255
+ animatedPage.value = initialPage;
256
+ scheduleOnUI(idx => {
257
+ 'worklet';
258
+
259
+ scrollTo(internalAnimatedRef, offsets[idx], 0, false);
260
+ }, safeIdx);
261
+ animatedScroll.value = offsets[safeIdx] ?? 0;
262
+ // eslint-disable-next-line react-hooks/exhaustive-deps
263
+ }, []);
201
264
 
202
- let pageIndex = initialPage;
265
+ // ---- scroll handler (UI thread worklet) ---------------------------------
203
266
 
204
- if (loop) {
205
- pageIndex = initialPage + additionalPagesPerSide;
206
- }
267
+ const scrollHandler = useAnimatedScrollHandler({
268
+ onScroll: event => {
269
+ animatedScroll.value = event.contentOffset.x;
207
270
 
208
- if (currentPage.value !== pageIndex) {
209
- setTimeout(() => {
210
- handleScrollTo(pageIndex, false);
271
+ // During a loop jump, detect when the scroll arrives at the
272
+ // destination and transition back to IDLE.
273
+ if (carouselState.value === CarouselState.LOOP_JUMP && jumpDestination.value !== -1) {
274
+ const nearest = findNearestPage(event.contentOffset.x, offsets);
275
+ if (nearest === jumpDestination.value) {
276
+ freeze.value = false;
277
+ carouselState.value = CarouselState.IDLE;
278
+ jumpDestination.value = -1;
279
+ }
280
+ }
281
+ },
282
+ onBeginDrag: _e => {
283
+ // User gesture always wins. Cancel any in-flight loop jump
284
+ // so the carousel doesn't fight the user's drag.
285
+ if (carouselState.value === CarouselState.LOOP_JUMP) {
286
+ jumpDestination.value = -1;
211
287
  freeze.value = false;
212
- });
213
- }
214
- }, []);
215
- const beginDrag = useCallback(() => {
216
- if (autoPlay) {
217
- setDragging(true);
218
- }
219
- }, [autoPlay]);
220
- const endDrag = useCallback(() => {
221
- if (autoPlay) {
222
- setTimeout(() => setDragging(false), 200);
288
+ }
289
+ carouselState.value = CarouselState.DRAGGING;
290
+ scheduleOnRN(setAutoplayPaused, true);
291
+ },
292
+ onEndDrag: _e => {
293
+ // Debounce autoplay resume — let momentum settle first.
294
+ scheduleOnRN(setAutoplayPaused, false);
295
+ },
296
+ onMomentumEnd: event => {
297
+ // Only process page changes when dragging ended naturally.
298
+ // Loop jumps are handled in onScroll.
299
+ if (carouselState.value !== CarouselState.DRAGGING) {
300
+ return;
301
+ }
302
+ const offset = event.contentOffset.x;
303
+ if (isNaN(offset) || offsets.length === 0) {
304
+ carouselState.value = CarouselState.IDLE;
305
+ return;
306
+ }
307
+ const renderPage = findNearestPage(offset, offsets);
308
+
309
+ // Loop boundary — user scrolled into the cloned head/tail pages.
310
+ if (loop) {
311
+ const boundaryTarget = getLoopBoundaryTarget(renderPage, data.length, additionalPagesPerSide);
312
+ if (boundaryTarget !== -1) {
313
+ // Jump instantly to the matching real page.
314
+ // On Android, freeze animations during the jump to prevent
315
+ // a visual flash from the clones.
316
+ carouselState.value = CarouselState.LOOP_JUMP;
317
+ jumpDestination.value = boundaryTarget;
318
+ freeze.value = Platform.OS === 'android';
319
+ animatedScroll.value = offsets[boundaryTarget];
320
+ scrollTo(internalAnimatedRef, offsets[boundaryTarget], 0, false);
321
+ currentRenderPage.value = boundaryTarget;
322
+ currentLogicalPage.value = getLogicalPage(boundaryTarget, data.length, additionalPagesPerSide, loop);
323
+ scheduleOnRN(handlePageChange, currentLogicalPage.value);
324
+ return;
325
+ }
326
+ }
327
+
328
+ // Normal page change — no loop boundary involved.
329
+ carouselState.value = CarouselState.IDLE;
330
+ currentRenderPage.value = renderPage;
331
+ currentLogicalPage.value = getLogicalPage(renderPage, data.length, additionalPagesPerSide, loop);
332
+ scheduleOnRN(handlePageChange, currentLogicalPage.value);
223
333
  }
224
- }, [autoPlay]);
334
+ }, [offsets, loop, data.length, additionalPagesPerSide, handlePageChange, internalAnimatedRef]);
335
+
336
+ // ---- render helpers -----------------------------------------------------
337
+
225
338
  const getItemKey = useCallback((item, index) => {
226
339
  if (keyExtractor) {
227
340
  return `${keyExtractor(item, index)}-${index}`;
228
341
  }
229
-
230
342
  if (item.id) {
231
343
  return `${item.id}-${index}`;
232
344
  }
233
-
234
- console.error('You need implement keyExtractor');
235
- return '';
345
+ return `carousel-item-${index}`;
236
346
  }, [keyExtractor]);
237
- const scrollHandler = useAnimatedScrollHandler({
238
- onScroll: event => {
239
- animatedScroll.value = event.contentOffset.x;
240
- refreshPage(animatedScroll.value);
241
- },
242
- onBeginDrag: _e => {
243
- runOnJS(beginDrag)();
244
- },
245
- onEndDrag: _e => {
246
- runOnJS(endDrag)();
247
- }
248
- }, [beginDrag, endDrag, refreshPage]);
249
- const containerStyle = useCallback(index => {
250
- if (firstItemAlignment === 'start') {
347
+ const containerStyles = useMemo(() => {
348
+ if (data.length === 0) return [];
349
+ return pageItems.map((_, i) => {
350
+ if (firstItemAlignment !== 'start') {
351
+ return {
352
+ paddingLeft: spaceBetween / 2,
353
+ paddingRight: spaceBetween / 2
354
+ };
355
+ }
356
+ const logicalIndex = loop ? getLogicalPage(i, data.length, additionalPagesPerSide, loop) : i;
251
357
  return {
252
- paddingLeft: index === 0 ? 0 : spaceBetween / 2,
253
- paddingRight: index === data.length - 1 ? 0 : spaceBetween / 2
358
+ paddingLeft: logicalIndex === 0 ? 0 : spaceBetween / 2,
359
+ paddingRight: logicalIndex === data.length - 1 ? 0 : spaceBetween / 2
254
360
  };
255
- }
256
-
257
- return {
258
- paddingLeft: spaceBetween / 2,
259
- paddingRight: spaceBetween / 2
260
- };
261
- }, [spaceBetween, firstItemAlignment]);
262
- const handleItemPress = useCallback((item, index) => () => {
263
- handleScrollTo(index);
264
-
265
- if (onItemPress) {
266
- onItemPress(item, index);
267
- }
268
- }, [onItemPress, handlePageChange]);
269
- const contentContainerStyle = useMemo(() => {
270
- return {
271
- paddingHorizontal: horizontalPadding
272
- };
273
- }, [horizontalPadding]);
274
-
275
- function renderPage(item, i) {
276
- return /*#__PURE__*/React.createElement(PageItem, {
277
- key: getItemKey(item, i),
278
- containerStyle: containerStyle(i),
361
+ });
362
+ }, [pageItems, firstItemAlignment, spaceBetween, data.length, loop, additionalPagesPerSide]);
363
+ const itemPressHandlers = useMemo(() => {
364
+ if (disableItemPress) return [];
365
+ return pageItems.map((item, i) => {
366
+ const logicalIndex = loop ? getLogicalPage(i, data.length, additionalPagesPerSide, loop) : i;
367
+ return () => {
368
+ handleScrollTo(i);
369
+ if (onItemPress) {
370
+ onItemPress(item, logicalIndex);
371
+ }
372
+ };
373
+ });
374
+ }, [pageItems, disableItemPress, loop, data.length, additionalPagesPerSide, handleScrollTo, onItemPress]);
375
+ const contentContainerStyle = useMemo(() => ({
376
+ paddingHorizontal: horizontalPadding
377
+ }), [horizontalPadding]);
378
+ const renderPage = useCallback((item, i) => {
379
+ // Compute the logical index so consumer callbacks (renderItem,
380
+ // onItemPress via `index` prop) receive logical, not render.
381
+ const logicalIndex = loop ? getLogicalPage(i, data.length, additionalPagesPerSide, loop) : i;
382
+ return /*#__PURE__*/_jsx(PageItem, {
383
+ containerStyle: containerStyles[i],
279
384
  item: item,
280
- index: i,
385
+ index: logicalIndex,
281
386
  offset: offsets[i],
282
387
  itemWidth: itemWidth,
283
388
  animatedValue: animatedScroll,
@@ -286,15 +391,21 @@ function Carousel({
286
391
  freeze: freeze,
287
392
  inactiveOpacity: inactiveOpacity,
288
393
  inactiveScale: inactiveScale,
289
- onPress: handleItemPress(item, i)
290
- });
291
- }
292
-
293
- return /*#__PURE__*/React.createElement(View, {
294
- style: style
295
- }, /*#__PURE__*/React.createElement(Animated.ScrollView, _extends({}, scrollViewProps, {
296
- ref: scrollViewRef,
297
- style: styles.container,
394
+ onPress: itemPressHandlers[i]
395
+ }, getItemKey(item, i));
396
+ }, [loop, data.length, additionalPagesPerSide, getItemKey, containerStyles, offsets, itemWidth, animatedScroll, animation, renderItem, freeze, inactiveOpacity, inactiveScale, itemPressHandlers]);
397
+
398
+ // ---- render -------------------------------------------------------------
399
+
400
+ return /*#__PURE__*/_jsx(Animated.ScrollView, {
401
+ ...scrollViewProps,
402
+ ref: nativeRef => {
403
+ internalAnimatedRef(nativeRef);
404
+ if (externalScrollViewRef) {
405
+ externalScrollViewRef.current = nativeRef;
406
+ }
407
+ },
408
+ style: [styles.container, style],
298
409
  horizontal: true,
299
410
  disableScrollViewPanResponder: true,
300
411
  disableIntervalMomentum: true,
@@ -306,10 +417,10 @@ function Carousel({
306
417
  scrollEventThrottle: 4,
307
418
  onScroll: scrollHandler,
308
419
  bounces: false,
309
- contentContainerStyle: contentContainerStyle
310
- }), pageItems.map(renderPage)));
420
+ contentContainerStyle: contentContainerStyle,
421
+ children: pageItems.map(renderPage)
422
+ });
311
423
  }
312
-
313
424
  export default Carousel;
314
425
  const styles = StyleSheet.create({
315
426
  container: {