react-native-reanimated-carousel 5.0.0-beta.4 → 5.0.0-beta.6
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/README.md +28 -6
- package/lib/commonjs/components/CarouselLayout.js +1 -1
- package/lib/commonjs/components/CarouselLayout.js.map +1 -1
- package/lib/commonjs/components/ItemLayout.js +1 -1
- package/lib/commonjs/components/ItemLayout.js.map +1 -1
- package/lib/commonjs/hooks/useCarouselController.js +1 -1
- package/lib/commonjs/hooks/useCarouselController.js.map +1 -1
- package/lib/commonjs/hooks/useVisibleRanges.js +1 -1
- package/lib/commonjs/hooks/useVisibleRanges.js.map +1 -1
- package/lib/module/components/CarouselLayout.js +1 -1
- package/lib/module/components/CarouselLayout.js.map +1 -1
- package/lib/module/components/ItemLayout.js +1 -1
- package/lib/module/components/ItemLayout.js.map +1 -1
- package/lib/module/hooks/useCarouselController.js +1 -1
- package/lib/module/hooks/useCarouselController.js.map +1 -1
- package/lib/module/hooks/useVisibleRanges.js +1 -1
- package/lib/module/hooks/useVisibleRanges.js.map +1 -1
- package/lib/typescript/components/CarouselLayout.d.ts +11 -0
- package/lib/typescript/components/CarouselLayout.d.ts.map +1 -1
- package/lib/typescript/components/ItemLayout.d.ts +5 -0
- package/lib/typescript/components/ItemLayout.d.ts.map +1 -1
- package/lib/typescript/hooks/useCarouselController.d.ts.map +1 -1
- package/lib/typescript/hooks/useVisibleRanges.d.ts.map +1 -1
- package/package.json +16 -6
- package/src/components/Carousel.test.tsx +152 -0
- package/src/components/CarouselLayout.tsx +51 -8
- package/src/components/ItemLayout.tsx +31 -7
- package/src/hooks/useCarouselController.test.tsx +93 -6
- package/src/hooks/useCarouselController.tsx +23 -25
- package/src/hooks/useVisibleRanges.test.tsx +29 -0
- package/src/hooks/useVisibleRanges.tsx +6 -1
|
@@ -15,6 +15,34 @@ import { ScrollViewGesture } from "./ScrollViewGesture";
|
|
|
15
15
|
|
|
16
16
|
export type TAnimationStyle = (value: number) => ViewStyle;
|
|
17
17
|
|
|
18
|
+
export function resolveCarouselLayoutStyle(params: {
|
|
19
|
+
flattenedStyle: Partial<ViewStyle>;
|
|
20
|
+
vertical: boolean;
|
|
21
|
+
measuredSize: number;
|
|
22
|
+
sizeExplicit: boolean;
|
|
23
|
+
legacyWidth?: number;
|
|
24
|
+
legacyHeight?: number;
|
|
25
|
+
}) {
|
|
26
|
+
"worklet";
|
|
27
|
+
|
|
28
|
+
const { flattenedStyle, vertical, measuredSize, sizeExplicit, legacyWidth, legacyHeight } =
|
|
29
|
+
params;
|
|
30
|
+
const { width, height } = flattenedStyle;
|
|
31
|
+
const hasLegacyMainAxisSize = vertical
|
|
32
|
+
? typeof legacyHeight === "number" && legacyHeight > 0
|
|
33
|
+
: typeof legacyWidth === "number" && legacyWidth > 0;
|
|
34
|
+
const shouldUseMeasuredMainAxisSize = sizeExplicit || hasLegacyMainAxisSize;
|
|
35
|
+
const resolvedMainAxisSize = measuredSize || "100%";
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
width:
|
|
39
|
+
width ?? (vertical ? "100%" : shouldUseMeasuredMainAxisSize ? resolvedMainAxisSize : "100%"),
|
|
40
|
+
height:
|
|
41
|
+
height ??
|
|
42
|
+
(vertical ? (shouldUseMeasuredMainAxisSize ? resolvedMainAxisSize : "100%") : "100%"),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
18
46
|
export const CarouselLayout = React.forwardRef<ICarouselInstance>((_props, ref) => {
|
|
19
47
|
const { props, common } = useGlobalState();
|
|
20
48
|
|
|
@@ -46,9 +74,11 @@ export const CarouselLayout = React.forwardRef<ICarouselInstance>((_props, ref)
|
|
|
46
74
|
onProgressChange,
|
|
47
75
|
customAnimation,
|
|
48
76
|
defaultIndex,
|
|
77
|
+
width: legacyWidth,
|
|
78
|
+
height: legacyHeight,
|
|
49
79
|
} = props;
|
|
50
80
|
|
|
51
|
-
const { size, handlerOffset, resolvedSize, sizePhase } = common;
|
|
81
|
+
const { size, handlerOffset, resolvedSize, sizePhase, sizeExplicit } = common;
|
|
52
82
|
const layoutConfig = useLayoutConfig({ ...props, size });
|
|
53
83
|
|
|
54
84
|
const isSizeReady = useDerivedValue(() => {
|
|
@@ -157,18 +187,31 @@ export const CarouselLayout = React.forwardRef<ICarouselInstance>((_props, ref)
|
|
|
157
187
|
const flattenedStyle = StyleSheet.flatten(style) || {};
|
|
158
188
|
|
|
159
189
|
const layoutStyle = useAnimatedStyle(() => {
|
|
160
|
-
const { width, height } = flattenedStyle;
|
|
161
190
|
const measuredSize = resolvedSize.value ?? 0;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
191
|
+
const { width, height } = resolveCarouselLayoutStyle({
|
|
192
|
+
flattenedStyle,
|
|
193
|
+
vertical: !!vertical,
|
|
194
|
+
measuredSize,
|
|
195
|
+
sizeExplicit,
|
|
196
|
+
legacyWidth,
|
|
197
|
+
legacyHeight,
|
|
198
|
+
});
|
|
165
199
|
|
|
166
200
|
return {
|
|
167
|
-
width
|
|
168
|
-
height
|
|
201
|
+
width,
|
|
202
|
+
height,
|
|
169
203
|
opacity: isSizeReady.value ? 1 : 0,
|
|
170
204
|
};
|
|
171
|
-
}, [
|
|
205
|
+
}, [
|
|
206
|
+
flattenedStyle,
|
|
207
|
+
isSizeReady,
|
|
208
|
+
vertical,
|
|
209
|
+
resolvedSize,
|
|
210
|
+
sizePhase,
|
|
211
|
+
sizeExplicit,
|
|
212
|
+
legacyWidth,
|
|
213
|
+
legacyHeight,
|
|
214
|
+
]);
|
|
172
215
|
|
|
173
216
|
return (
|
|
174
217
|
<GestureHandlerRootView testID={testID} style={[styles.layoutContainer, style]}>
|
|
@@ -19,6 +19,20 @@ import { sanitizeAnimationStyle } from "../utils/sanitize-animation-style";
|
|
|
19
19
|
|
|
20
20
|
export type TAnimationStyle = NonNullable<TCarouselProps["customAnimation"]>;
|
|
21
21
|
|
|
22
|
+
export function resolveItemMainAxisSize(params: {
|
|
23
|
+
explicitSize?: number;
|
|
24
|
+
effectivePageSize?: number;
|
|
25
|
+
measuredSize: number | null;
|
|
26
|
+
}) {
|
|
27
|
+
"worklet";
|
|
28
|
+
|
|
29
|
+
const { explicitSize, effectivePageSize, measuredSize } = params;
|
|
30
|
+
|
|
31
|
+
return typeof explicitSize === "number"
|
|
32
|
+
? explicitSize
|
|
33
|
+
: (effectivePageSize ?? measuredSize ?? "100%");
|
|
34
|
+
}
|
|
35
|
+
|
|
22
36
|
export const ItemLayout: React.FC<{
|
|
23
37
|
index: number;
|
|
24
38
|
handlerOffset: SharedValue<number>;
|
|
@@ -71,15 +85,25 @@ export const ItemLayout: React.FC<{
|
|
|
71
85
|
const widthCandidate = vertical ? width : (explicitItemSize ?? explicitAxisSize);
|
|
72
86
|
const heightCandidate = vertical ? (explicitItemSize ?? explicitAxisSize) : height;
|
|
73
87
|
|
|
74
|
-
const computedWidth =
|
|
75
|
-
typeof widthCandidate === "number"
|
|
88
|
+
const computedWidth = vertical
|
|
89
|
+
? typeof widthCandidate === "number"
|
|
76
90
|
? widthCandidate
|
|
77
|
-
: (measuredSize.value.width ??
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
91
|
+
: (measuredSize.value.width ?? "100%")
|
|
92
|
+
: resolveItemMainAxisSize({
|
|
93
|
+
explicitSize: widthCandidate,
|
|
94
|
+
effectivePageSize,
|
|
95
|
+
measuredSize: measuredSize.value.width,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const computedHeight = vertical
|
|
99
|
+
? resolveItemMainAxisSize({
|
|
100
|
+
explicitSize: heightCandidate,
|
|
101
|
+
effectivePageSize,
|
|
102
|
+
measuredSize: measuredSize.value.height,
|
|
103
|
+
})
|
|
104
|
+
: typeof heightCandidate === "number"
|
|
81
105
|
? heightCandidate
|
|
82
|
-
: (measuredSize.value.height ??
|
|
106
|
+
: (measuredSize.value.height ?? "100%");
|
|
83
107
|
|
|
84
108
|
return {
|
|
85
109
|
width: computedWidth,
|
|
@@ -228,6 +228,76 @@ describe("useCarouselController", () => {
|
|
|
228
228
|
expect(mockHandlerOffset.value).toBe(-900); // size * 3
|
|
229
229
|
});
|
|
230
230
|
|
|
231
|
+
it("should keep negative offsets when scrollTo() moves backward in non-loop mode", () => {
|
|
232
|
+
const { result } = renderHook(
|
|
233
|
+
() =>
|
|
234
|
+
useCarouselController({
|
|
235
|
+
...defaultProps,
|
|
236
|
+
loop: false,
|
|
237
|
+
}),
|
|
238
|
+
{ wrapper }
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
act(() => {
|
|
242
|
+
result.current.scrollTo({ index: 3, animated: false });
|
|
243
|
+
});
|
|
244
|
+
expect(mockHandlerOffset.value).toBe(-900);
|
|
245
|
+
|
|
246
|
+
act(() => {
|
|
247
|
+
result.current.scrollTo({ index: 2, animated: false });
|
|
248
|
+
});
|
|
249
|
+
expect(mockHandlerOffset.value).toBe(-600);
|
|
250
|
+
|
|
251
|
+
act(() => {
|
|
252
|
+
result.current.scrollTo({ index: 1, animated: false });
|
|
253
|
+
});
|
|
254
|
+
expect(mockHandlerOffset.value).toBe(-300);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("should keep negative offsets when animated backward scrollTo() runs in non-loop mode", () => {
|
|
258
|
+
const onFinished = jest.fn();
|
|
259
|
+
const { result } = renderHook(
|
|
260
|
+
() =>
|
|
261
|
+
useCarouselController({
|
|
262
|
+
...defaultProps,
|
|
263
|
+
loop: false,
|
|
264
|
+
}),
|
|
265
|
+
{ wrapper }
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
act(() => {
|
|
269
|
+
result.current.scrollTo({ index: 3, animated: true });
|
|
270
|
+
});
|
|
271
|
+
expect(mockHandlerOffset.value).toBe(-900);
|
|
272
|
+
|
|
273
|
+
act(() => {
|
|
274
|
+
result.current.scrollTo({ index: 2, animated: true, onFinished });
|
|
275
|
+
});
|
|
276
|
+
expect(mockHandlerOffset.value).toBe(-600);
|
|
277
|
+
expect(onFinished).toHaveBeenCalled();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("should map scrollTo({ index: 0 }) to zero offset from end in non-loop mode", () => {
|
|
281
|
+
const { result } = renderHook(
|
|
282
|
+
() =>
|
|
283
|
+
useCarouselController({
|
|
284
|
+
...defaultProps,
|
|
285
|
+
loop: false,
|
|
286
|
+
}),
|
|
287
|
+
{ wrapper }
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
act(() => {
|
|
291
|
+
result.current.scrollTo({ index: 4, animated: false });
|
|
292
|
+
});
|
|
293
|
+
expect(mockHandlerOffset.value).toBe(-1200);
|
|
294
|
+
|
|
295
|
+
act(() => {
|
|
296
|
+
result.current.scrollTo({ index: 0, animated: false });
|
|
297
|
+
});
|
|
298
|
+
expect(Math.abs(mockHandlerOffset.value)).toBe(0);
|
|
299
|
+
});
|
|
300
|
+
|
|
231
301
|
it("should handle animation callbacks", () => {
|
|
232
302
|
const onFinished = jest.fn();
|
|
233
303
|
const { result } = renderHook(() => useCarouselController(defaultProps), { wrapper });
|
|
@@ -1004,13 +1074,31 @@ describe("useCarouselController edge cases and uncovered lines", () => {
|
|
|
1004
1074
|
expect(typeof currentIndex).toBe("number");
|
|
1005
1075
|
});
|
|
1006
1076
|
|
|
1007
|
-
it("should handle fixed direction in scrollTo", () => {
|
|
1077
|
+
it("should handle positive fixed direction in scrollTo", () => {
|
|
1078
|
+
const { result } = renderHook(
|
|
1079
|
+
() =>
|
|
1080
|
+
useCarouselController({
|
|
1081
|
+
...defaultProps,
|
|
1082
|
+
loop: true,
|
|
1083
|
+
fixedDirection: "positive",
|
|
1084
|
+
}),
|
|
1085
|
+
{ wrapper }
|
|
1086
|
+
);
|
|
1087
|
+
|
|
1088
|
+
act(() => {
|
|
1089
|
+
result.current.scrollTo({ index: 3, animated: false });
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
expect(mockHandlerOffset.value).toBe(900); // size * 3
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
it("should handle negative fixed direction in scrollTo", () => {
|
|
1008
1096
|
const { result } = renderHook(
|
|
1009
1097
|
() =>
|
|
1010
1098
|
useCarouselController({
|
|
1011
1099
|
...defaultProps,
|
|
1012
1100
|
loop: true,
|
|
1013
|
-
fixedDirection:
|
|
1101
|
+
fixedDirection: "negative",
|
|
1014
1102
|
}),
|
|
1015
1103
|
{ wrapper }
|
|
1016
1104
|
);
|
|
@@ -1022,7 +1110,7 @@ describe("useCarouselController edge cases and uncovered lines", () => {
|
|
|
1022
1110
|
expect(mockHandlerOffset.value).toBe(-900); // size * 3
|
|
1023
1111
|
});
|
|
1024
1112
|
|
|
1025
|
-
it("should handle
|
|
1113
|
+
it("should handle scrollTo when loop position is close to next cycle", () => {
|
|
1026
1114
|
const { result } = renderHook(
|
|
1027
1115
|
() =>
|
|
1028
1116
|
useCarouselController({
|
|
@@ -1032,14 +1120,13 @@ describe("useCarouselController edge cases and uncovered lines", () => {
|
|
|
1032
1120
|
{ wrapper }
|
|
1033
1121
|
);
|
|
1034
1122
|
|
|
1035
|
-
//
|
|
1036
|
-
mockHandlerOffset.value = -1800; // 6 * size, beyond data length
|
|
1123
|
+
mockHandlerOffset.value = -900; // 3 * size, more than half-way through current cycle
|
|
1037
1124
|
|
|
1038
1125
|
act(() => {
|
|
1039
1126
|
result.current.scrollTo({ index: 1, animated: false });
|
|
1040
1127
|
});
|
|
1041
1128
|
|
|
1042
|
-
expect(
|
|
1129
|
+
expect(mockHandlerOffset.value).toBe(-1800);
|
|
1043
1130
|
});
|
|
1044
1131
|
|
|
1045
1132
|
it("should get shared index correctly", () => {
|
|
@@ -352,34 +352,32 @@ export function useCarouselController(options: IOpts): ICarouselController {
|
|
|
352
352
|
if (!canSliding()) return;
|
|
353
353
|
|
|
354
354
|
onScrollStart?.();
|
|
355
|
-
|
|
356
|
-
let direction: -1 | 1;
|
|
357
|
-
if (fixedDirection === "positive") direction = 1;
|
|
358
|
-
else if (fixedDirection === "negative") direction = -1;
|
|
359
|
-
else if (!loop) {
|
|
360
|
-
const currentPage = currentFixedPage();
|
|
361
|
-
direction = i >= currentPage ? -1 : 1;
|
|
362
|
-
} else {
|
|
363
|
-
direction = handlerOffsetDirection(handlerOffset);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// target offset
|
|
367
|
-
const offset = i * size * direction;
|
|
368
|
-
// page width size * page count
|
|
369
|
-
const totalSize = dataInfo.length * size;
|
|
370
|
-
|
|
371
|
-
let isCloseToNextLoop = false;
|
|
355
|
+
let finalOffset: number;
|
|
372
356
|
|
|
373
|
-
if (loop) {
|
|
374
|
-
|
|
357
|
+
if (!loop) {
|
|
358
|
+
// Non-loop mode always uses direct page-to-offset mapping.
|
|
359
|
+
// This avoids sign flips when moving to lower indices.
|
|
360
|
+
finalOffset = -i * size;
|
|
361
|
+
} else {
|
|
362
|
+
// direction -> 1 | -1
|
|
363
|
+
let direction: -1 | 1;
|
|
364
|
+
if (fixedDirection === "positive") direction = 1;
|
|
365
|
+
else if (fixedDirection === "negative") direction = -1;
|
|
366
|
+
else direction = handlerOffsetDirection(handlerOffset);
|
|
367
|
+
|
|
368
|
+
// target offset
|
|
369
|
+
const offset = i * size * direction;
|
|
370
|
+
// page width size * page count
|
|
371
|
+
const totalSize = dataInfo.length * size;
|
|
372
|
+
const isCloseToNextLoop = Math.abs(handlerOffset.value % totalSize) / totalSize >= 0.5;
|
|
373
|
+
|
|
374
|
+
finalOffset =
|
|
375
|
+
(Math.floor(Math.abs(handlerOffset.value / totalSize)) + (isCloseToNextLoop ? 1 : 0)) *
|
|
376
|
+
totalSize *
|
|
377
|
+
direction +
|
|
378
|
+
offset;
|
|
375
379
|
}
|
|
376
380
|
|
|
377
|
-
const finalOffset =
|
|
378
|
-
(Math.floor(Math.abs(handlerOffset.value / totalSize)) + (isCloseToNextLoop ? 1 : 0)) *
|
|
379
|
-
totalSize *
|
|
380
|
-
direction +
|
|
381
|
-
offset;
|
|
382
|
-
|
|
383
381
|
if (animated) {
|
|
384
382
|
index.value = i;
|
|
385
383
|
handlerOffset.value = scrollWithTiming(finalOffset, onFinished);
|
|
@@ -67,6 +67,35 @@ describe("useVisibleRanges", () => {
|
|
|
67
67
|
`);
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
+
it.each([
|
|
71
|
+
{ total: 1, windowSize: 1, currentIndex: 0 },
|
|
72
|
+
{ total: 2, windowSize: 5, currentIndex: 1 },
|
|
73
|
+
{ total: 3, windowSize: 3, currentIndex: 2 },
|
|
74
|
+
{ total: 4, windowSize: 5, currentIndex: 1 },
|
|
75
|
+
])(
|
|
76
|
+
"should display every item when windowSize=$windowSize is at least total=$total in loop mode",
|
|
77
|
+
({ total, windowSize, currentIndex }) => {
|
|
78
|
+
const ranges = renderHook(() => {
|
|
79
|
+
const translation = useSharedValue(-currentIndex * viewSize);
|
|
80
|
+
return useVisibleRanges({
|
|
81
|
+
total,
|
|
82
|
+
translation,
|
|
83
|
+
viewSize,
|
|
84
|
+
windowSize,
|
|
85
|
+
loop: true,
|
|
86
|
+
});
|
|
87
|
+
}).result.current.value;
|
|
88
|
+
|
|
89
|
+
const visibleIndexes = Array.from({ length: total }, (_, index) => index).filter(
|
|
90
|
+
(index) =>
|
|
91
|
+
(index >= ranges.negativeRange[0] && index <= ranges.negativeRange[1]) ||
|
|
92
|
+
(index >= ranges.positiveRange[0] && index <= ranges.positiveRange[1])
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
expect(visibleIndexes).toEqual(Array.from({ length: total }, (_, index) => index));
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
|
|
70
99
|
it("should shows the increased range of the list when the loop is false and swiped the carousel.", async () => {
|
|
71
100
|
const slide0hook = renderHook(() => {
|
|
72
101
|
const translation = useSharedValue(-0 * viewSize);
|
|
@@ -50,7 +50,12 @@ export function useVisibleRanges(options: {
|
|
|
50
50
|
|
|
51
51
|
let newRanges: VisibleRanges;
|
|
52
52
|
|
|
53
|
-
if (
|
|
53
|
+
if (loop && windowSize >= total) {
|
|
54
|
+
newRanges = {
|
|
55
|
+
negativeRange: [0, 0],
|
|
56
|
+
positiveRange: [0, total - 1],
|
|
57
|
+
};
|
|
58
|
+
} else if (!loop) {
|
|
54
59
|
// Clamp currentIndex to valid range [0, total-1] for non-loop mode
|
|
55
60
|
// When overdragging right, translation.value becomes positive, making currentIndex negative
|
|
56
61
|
currentIndex = Math.max(0, Math.min(total - 1, currentIndex));
|