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