@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.
- package/lib/commonjs/components/Carousel.js +290 -195
- package/lib/commonjs/components/Carousel.js.map +1 -1
- package/lib/commonjs/components/CarouselContainer.js +17 -31
- package/lib/commonjs/components/CarouselContainer.js.map +1 -1
- package/lib/commonjs/components/PageItem.js +53 -46
- package/lib/commonjs/components/PageItem.js.map +1 -1
- package/lib/commonjs/components/PaginationIndicator.js +23 -34
- package/lib/commonjs/components/PaginationIndicator.js.map +1 -1
- package/lib/commonjs/components/useCarouselContext.js +3 -8
- package/lib/commonjs/components/useCarouselContext.js.map +1 -1
- package/lib/commonjs/components/useInternalCarouselContext.js +3 -6
- package/lib/commonjs/components/useInternalCarouselContext.js.map +1 -1
- package/lib/commonjs/index.js +7 -17
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/types.js +4 -0
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/utils.js +24 -18
- package/lib/commonjs/utils.js.map +1 -1
- package/lib/module/components/Carousel.js +291 -180
- package/lib/module/components/Carousel.js.map +1 -1
- package/lib/module/components/CarouselContainer.js +18 -23
- package/lib/module/components/CarouselContainer.js.map +1 -1
- package/lib/module/components/PageItem.js +54 -38
- package/lib/module/components/PageItem.js.map +1 -1
- package/lib/module/components/PaginationIndicator.js +23 -24
- package/lib/module/components/PaginationIndicator.js.map +1 -1
- package/lib/module/components/useCarouselContext.js +2 -2
- package/lib/module/components/useCarouselContext.js.map +1 -1
- package/lib/module/components/useInternalCarouselContext.js +2 -0
- package/lib/module/components/useInternalCarouselContext.js.map +1 -1
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -1
- package/lib/module/utils.js +22 -12
- package/lib/module/utils.js.map +1 -1
- package/lib/typescript/components/Carousel.d.ts +3 -1
- package/lib/typescript/components/Carousel.d.ts.map +1 -0
- package/lib/typescript/components/CarouselContainer.d.ts +3 -2
- package/lib/typescript/components/CarouselContainer.d.ts.map +1 -0
- package/lib/typescript/components/PageItem.d.ts +5 -3
- package/lib/typescript/components/PageItem.d.ts.map +1 -0
- package/lib/typescript/components/PaginationIndicator.d.ts +3 -1
- package/lib/typescript/components/PaginationIndicator.d.ts.map +1 -0
- package/lib/typescript/components/useCarouselContext.d.ts +1 -1
- package/lib/typescript/components/useCarouselContext.d.ts.map +1 -0
- package/lib/typescript/components/useInternalCarouselContext.d.ts +1 -1
- package/lib/typescript/components/useInternalCarouselContext.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +1 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +17 -15
- package/lib/typescript/types.d.ts.map +1 -0
- package/lib/typescript/utils.d.ts +4 -1
- package/lib/typescript/utils.d.ts.map +1 -0
- package/package.json +26 -29
- package/src/components/Carousel.tsx +404 -206
- package/src/components/CarouselContainer.tsx +4 -4
- package/src/components/PageItem.tsx +55 -34
- package/src/components/PaginationIndicator.tsx +2 -2
- package/src/components/useCarouselContext.ts +0 -1
- package/src/components/useInternalCarouselContext.ts +0 -1
- package/src/types.ts +9 -7
- package/src/utils.ts +31 -11
|
@@ -1,19 +1,46 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
3
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
|
4
|
-
import React, { useState,
|
|
5
|
-
import {
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
const
|
|
44
|
-
const
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
109
|
+
return [...headItems, ...data, ...tailItems];
|
|
110
|
+
}
|
|
111
|
+
return data;
|
|
112
|
+
}, [data, loop, additionalPagesPerSide]);
|
|
76
113
|
|
|
77
|
-
|
|
78
|
-
pageMapper.current[i] = (data.length - additionalPagesPerSide + i) % data.length;
|
|
79
|
-
}
|
|
114
|
+
// ---- side effects -------------------------------------------------------
|
|
80
115
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
pageMapper.current[i] = i;
|
|
85
|
-
}
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
totalPage.value = data.length;
|
|
118
|
+
}, [data.length, totalPage]);
|
|
86
119
|
|
|
87
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
97
|
-
|
|
160
|
+
const handlePageChange = useCallback(logicalPage => {
|
|
161
|
+
animatedPage.value = logicalPage;
|
|
162
|
+
if (onPageChange) {
|
|
163
|
+
onPageChange(logicalPage);
|
|
98
164
|
}
|
|
165
|
+
}, [onPageChange, animatedPage]);
|
|
99
166
|
|
|
100
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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
|
-
|
|
265
|
+
// ---- scroll handler (UI thread worklet) ---------------------------------
|
|
203
266
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
267
|
+
const scrollHandler = useAnimatedScrollHandler({
|
|
268
|
+
onScroll: event => {
|
|
269
|
+
animatedScroll.value = event.contentOffset.x;
|
|
207
270
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
}, [
|
|
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
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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:
|
|
253
|
-
paddingRight:
|
|
358
|
+
paddingLeft: logicalIndex === 0 ? 0 : spaceBetween / 2,
|
|
359
|
+
paddingRight: logicalIndex === data.length - 1 ? 0 : spaceBetween / 2
|
|
254
360
|
};
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
}, [
|
|
269
|
-
const contentContainerStyle = useMemo(() => {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
return /*#__PURE__*/
|
|
277
|
-
|
|
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:
|
|
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:
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
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: {
|