react-native-ultra-carousel 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +81 -0
- package/src/animations/basic/fade.ts +51 -0
- package/src/animations/basic/overlap.ts +69 -0
- package/src/animations/basic/parallax.ts +65 -0
- package/src/animations/basic/peek.ts +79 -0
- package/src/animations/basic/scale.ts +63 -0
- package/src/animations/basic/scaleFade.ts +73 -0
- package/src/animations/basic/slide.ts +53 -0
- package/src/animations/basic/slideFade.ts +60 -0
- package/src/animations/basic/vertical.ts +50 -0
- package/src/animations/basic/verticalFade.ts +60 -0
- package/src/animations/index.ts +45 -0
- package/src/animations/registry.ts +175 -0
- package/src/animations/types.ts +11 -0
- package/src/animations/utils.ts +75 -0
- package/src/components/AutoPlayController.tsx +38 -0
- package/src/components/Carousel.tsx +371 -0
- package/src/components/CarouselItem.tsx +98 -0
- package/src/components/Pagination/BarPagination.tsx +141 -0
- package/src/components/Pagination/CustomPagination.tsx +48 -0
- package/src/components/Pagination/DotPagination.tsx +137 -0
- package/src/components/Pagination/NumberPagination.tsx +117 -0
- package/src/components/Pagination/Pagination.tsx +82 -0
- package/src/components/Pagination/ProgressPagination.tsx +70 -0
- package/src/components/Pagination/index.ts +11 -0
- package/src/components/ParallaxImage.tsx +89 -0
- package/src/gestures/FlingGestureManager.ts +49 -0
- package/src/gestures/PanGestureManager.ts +202 -0
- package/src/gestures/ScrollViewCompat.ts +28 -0
- package/src/gestures/types.ts +6 -0
- package/src/hooks/useAnimationProgress.ts +33 -0
- package/src/hooks/useAutoPlay.ts +115 -0
- package/src/hooks/useCarousel.ts +118 -0
- package/src/hooks/useCarouselGesture.ts +109 -0
- package/src/hooks/useItemAnimation.ts +44 -0
- package/src/hooks/usePagination.ts +39 -0
- package/src/hooks/useSnapPoints.ts +31 -0
- package/src/hooks/useVirtualization.ts +63 -0
- package/src/index.ts +71 -0
- package/src/plugins/PluginManager.ts +150 -0
- package/src/plugins/types.ts +6 -0
- package/src/types/animation.ts +72 -0
- package/src/types/carousel.ts +188 -0
- package/src/types/gesture.ts +42 -0
- package/src/types/index.ts +41 -0
- package/src/types/pagination.ts +65 -0
- package/src/types/plugin.ts +27 -0
- package/src/utils/accessibility.ts +71 -0
- package/src/utils/constants.ts +45 -0
- package/src/utils/layout.ts +115 -0
- package/src/utils/math.ts +78 -0
- package/src/utils/platform.ts +33 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Pan gesture manager
|
|
3
|
+
* @description Handles pan gesture configuration and snap calculations for the carousel
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Gesture } from 'react-native-gesture-handler';
|
|
7
|
+
import {
|
|
8
|
+
useSharedValue,
|
|
9
|
+
withSpring,
|
|
10
|
+
runOnJS,
|
|
11
|
+
} from 'react-native-reanimated';
|
|
12
|
+
import { useCallback, useMemo } from 'react';
|
|
13
|
+
import { DEFAULT_SPRING_CONFIG, DEFAULT_VELOCITY_THRESHOLD } from '../utils/constants';
|
|
14
|
+
import { clamp } from '../utils/math';
|
|
15
|
+
import { findNearestSnapIndex } from '../utils/layout';
|
|
16
|
+
|
|
17
|
+
/** Configuration for the pan gesture hook */
|
|
18
|
+
export interface PanGestureOptions {
|
|
19
|
+
/** Total number of items */
|
|
20
|
+
totalItems: number;
|
|
21
|
+
/** Size of each item (width or height depending on direction) */
|
|
22
|
+
itemSize: number;
|
|
23
|
+
/** Gap between items */
|
|
24
|
+
gap: number;
|
|
25
|
+
/** All snap point offsets */
|
|
26
|
+
snapPoints: number[];
|
|
27
|
+
/** Whether carousel is horizontal */
|
|
28
|
+
isHorizontal: boolean;
|
|
29
|
+
/** Whether loop mode is enabled */
|
|
30
|
+
loop: boolean;
|
|
31
|
+
/** Whether gesture is enabled */
|
|
32
|
+
enabled: boolean;
|
|
33
|
+
/** Active offset X threshold */
|
|
34
|
+
activeOffsetX: [number, number];
|
|
35
|
+
/** Active offset Y threshold */
|
|
36
|
+
activeOffsetY: [number, number];
|
|
37
|
+
/** Velocity threshold for fling */
|
|
38
|
+
velocityThreshold: number;
|
|
39
|
+
/** Callback when index changes */
|
|
40
|
+
onIndexChange?: (index: number) => void;
|
|
41
|
+
/** Callback when scroll starts */
|
|
42
|
+
onScrollStart?: () => void;
|
|
43
|
+
/** Callback when scroll ends */
|
|
44
|
+
onScrollEnd?: (index: number) => void;
|
|
45
|
+
/** Custom gesture configuration callback */
|
|
46
|
+
onConfigurePanGesture?: (gesture: ReturnType<typeof Gesture.Pan>) => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Creates and manages the pan gesture for carousel navigation.
|
|
51
|
+
*
|
|
52
|
+
* @param options - Configuration for the gesture
|
|
53
|
+
* @returns Object containing the gesture, shared values, and control methods
|
|
54
|
+
*/
|
|
55
|
+
export const usePanGesture = (options: PanGestureOptions) => {
|
|
56
|
+
const {
|
|
57
|
+
totalItems,
|
|
58
|
+
itemSize,
|
|
59
|
+
gap,
|
|
60
|
+
snapPoints,
|
|
61
|
+
isHorizontal,
|
|
62
|
+
loop,
|
|
63
|
+
enabled,
|
|
64
|
+
activeOffsetX,
|
|
65
|
+
activeOffsetY,
|
|
66
|
+
velocityThreshold,
|
|
67
|
+
onIndexChange,
|
|
68
|
+
onScrollStart,
|
|
69
|
+
onScrollEnd,
|
|
70
|
+
onConfigurePanGesture,
|
|
71
|
+
} = options;
|
|
72
|
+
|
|
73
|
+
const offset = useSharedValue(0);
|
|
74
|
+
const activeIndex = useSharedValue(0);
|
|
75
|
+
const isGestureActive = useSharedValue(false);
|
|
76
|
+
|
|
77
|
+
const stepSize = itemSize + gap;
|
|
78
|
+
const maxOffset = (totalItems - 1) * stepSize;
|
|
79
|
+
|
|
80
|
+
const handleIndexChange = useCallback(
|
|
81
|
+
(index: number) => {
|
|
82
|
+
onIndexChange?.(index);
|
|
83
|
+
},
|
|
84
|
+
[onIndexChange]
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const handleScrollStart = useCallback(() => {
|
|
88
|
+
onScrollStart?.();
|
|
89
|
+
}, [onScrollStart]);
|
|
90
|
+
|
|
91
|
+
const handleScrollEnd = useCallback(
|
|
92
|
+
(index: number) => {
|
|
93
|
+
onScrollEnd?.(index);
|
|
94
|
+
},
|
|
95
|
+
[onScrollEnd]
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const snapToIndex = useCallback(
|
|
99
|
+
(index: number, animated = true) => {
|
|
100
|
+
const targetIndex = loop
|
|
101
|
+
? index
|
|
102
|
+
: clamp(index, 0, totalItems - 1);
|
|
103
|
+
const targetOffset = snapPoints[targetIndex] ?? targetIndex * stepSize;
|
|
104
|
+
|
|
105
|
+
if (animated) {
|
|
106
|
+
offset.value = withSpring(targetOffset, DEFAULT_SPRING_CONFIG);
|
|
107
|
+
} else {
|
|
108
|
+
offset.value = targetOffset;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (activeIndex.value !== targetIndex) {
|
|
112
|
+
activeIndex.value = targetIndex;
|
|
113
|
+
runOnJS(handleIndexChange)(targetIndex);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
[loop, totalItems, snapPoints, stepSize, offset, activeIndex, handleIndexChange]
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const gesture = useMemo(() => {
|
|
120
|
+
const pan = Gesture.Pan()
|
|
121
|
+
.activeOffsetX(activeOffsetX)
|
|
122
|
+
.activeOffsetY(activeOffsetY)
|
|
123
|
+
.enabled(enabled)
|
|
124
|
+
.onStart(() => {
|
|
125
|
+
isGestureActive.value = true;
|
|
126
|
+
runOnJS(handleScrollStart)();
|
|
127
|
+
})
|
|
128
|
+
.onUpdate((event) => {
|
|
129
|
+
const translation = isHorizontal
|
|
130
|
+
? event.translationX
|
|
131
|
+
: event.translationY;
|
|
132
|
+
const startOffset = activeIndex.value * stepSize;
|
|
133
|
+
let newOffset = startOffset - translation;
|
|
134
|
+
|
|
135
|
+
if (!loop) {
|
|
136
|
+
newOffset = clamp(newOffset, 0, maxOffset);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
offset.value = newOffset;
|
|
140
|
+
})
|
|
141
|
+
.onEnd((event) => {
|
|
142
|
+
isGestureActive.value = false;
|
|
143
|
+
|
|
144
|
+
const velocity = isHorizontal ? event.velocityX : event.velocityY;
|
|
145
|
+
const isFling = Math.abs(velocity) > velocityThreshold;
|
|
146
|
+
|
|
147
|
+
let targetIndex: number;
|
|
148
|
+
|
|
149
|
+
if (isFling) {
|
|
150
|
+
targetIndex = velocity < 0
|
|
151
|
+
? activeIndex.value + 1
|
|
152
|
+
: activeIndex.value - 1;
|
|
153
|
+
} else {
|
|
154
|
+
targetIndex = findNearestSnapIndex(offset.value, snapPoints);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!loop) {
|
|
158
|
+
targetIndex = clamp(targetIndex, 0, totalItems - 1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const targetOffset = snapPoints[targetIndex] ?? targetIndex * stepSize;
|
|
162
|
+
|
|
163
|
+
offset.value = withSpring(targetOffset, DEFAULT_SPRING_CONFIG);
|
|
164
|
+
|
|
165
|
+
if (activeIndex.value !== targetIndex) {
|
|
166
|
+
activeIndex.value = targetIndex;
|
|
167
|
+
runOnJS(handleIndexChange)(targetIndex);
|
|
168
|
+
}
|
|
169
|
+
runOnJS(handleScrollEnd)(targetIndex);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
onConfigurePanGesture?.(pan);
|
|
173
|
+
|
|
174
|
+
return pan;
|
|
175
|
+
}, [
|
|
176
|
+
activeOffsetX,
|
|
177
|
+
activeOffsetY,
|
|
178
|
+
enabled,
|
|
179
|
+
isHorizontal,
|
|
180
|
+
loop,
|
|
181
|
+
maxOffset,
|
|
182
|
+
stepSize,
|
|
183
|
+
totalItems,
|
|
184
|
+
snapPoints,
|
|
185
|
+
velocityThreshold,
|
|
186
|
+
offset,
|
|
187
|
+
activeIndex,
|
|
188
|
+
isGestureActive,
|
|
189
|
+
handleIndexChange,
|
|
190
|
+
handleScrollStart,
|
|
191
|
+
handleScrollEnd,
|
|
192
|
+
onConfigurePanGesture,
|
|
193
|
+
]);
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
gesture,
|
|
197
|
+
offset,
|
|
198
|
+
activeIndex,
|
|
199
|
+
isGestureActive,
|
|
200
|
+
snapToIndex,
|
|
201
|
+
};
|
|
202
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file ScrollView compatibility manager
|
|
3
|
+
* @description Resolves gesture conflicts between carousel and parent ScrollView
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Gesture } from 'react-native-gesture-handler';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Configures a pan gesture for compatibility with a parent ScrollView.
|
|
10
|
+
* Sets appropriate active offsets so horizontal swipes activate the carousel
|
|
11
|
+
* while vertical swipes pass through to the ScrollView.
|
|
12
|
+
*
|
|
13
|
+
* @param panGesture - The pan gesture to configure
|
|
14
|
+
* @param isHorizontal - Whether the carousel scrolls horizontally
|
|
15
|
+
* @returns The configured gesture
|
|
16
|
+
*/
|
|
17
|
+
export const configureScrollViewCompat = (
|
|
18
|
+
panGesture: ReturnType<typeof Gesture.Pan>,
|
|
19
|
+
isHorizontal: boolean
|
|
20
|
+
) => {
|
|
21
|
+
if (isHorizontal) {
|
|
22
|
+
panGesture.activeOffsetX([-10, 10]).activeOffsetY([-50, 50]);
|
|
23
|
+
} else {
|
|
24
|
+
panGesture.activeOffsetY([-10, 10]).activeOffsetX([-50, 50]);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return panGesture;
|
|
28
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file useAnimationProgress hook
|
|
3
|
+
* @description Computes normalized animation progress for each carousel item
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useDerivedValue, type SharedValue } from 'react-native-reanimated';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Computes the normalized animation progress for a specific item.
|
|
10
|
+
* Progress of 0 means the item is fully active/centered.
|
|
11
|
+
* Progress of -1 means the item is one position to the left (prev).
|
|
12
|
+
* Progress of +1 means the item is one position to the right (next).
|
|
13
|
+
*
|
|
14
|
+
* @param index - The item index
|
|
15
|
+
* @param scrollOffset - The current scroll offset shared value
|
|
16
|
+
* @param itemSize - Width or height of each item
|
|
17
|
+
* @param gap - Gap between items
|
|
18
|
+
* @returns Shared value containing the normalized progress
|
|
19
|
+
*/
|
|
20
|
+
export const useAnimationProgress = (
|
|
21
|
+
index: number,
|
|
22
|
+
scrollOffset: SharedValue<number>,
|
|
23
|
+
itemSize: number,
|
|
24
|
+
gap: number
|
|
25
|
+
): SharedValue<number> => {
|
|
26
|
+
const stepSize = itemSize + gap;
|
|
27
|
+
|
|
28
|
+
return useDerivedValue(() => {
|
|
29
|
+
if (stepSize === 0) return 0;
|
|
30
|
+
const itemOffset = index * stepSize;
|
|
31
|
+
return (scrollOffset.value - itemOffset) / stepSize;
|
|
32
|
+
}, [index, stepSize]);
|
|
33
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file useAutoPlay hook
|
|
3
|
+
* @description Manages automatic slide advancement with pause/resume controls
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
7
|
+
import type { AutoPlayConfig } from '../types';
|
|
8
|
+
import { DEFAULT_AUTO_PLAY_INTERVAL } from '../utils/constants';
|
|
9
|
+
|
|
10
|
+
/** Return type of the useAutoPlay hook */
|
|
11
|
+
export interface UseAutoPlayReturn {
|
|
12
|
+
/** Whether auto play is currently running */
|
|
13
|
+
isPlaying: boolean;
|
|
14
|
+
/** Start auto play */
|
|
15
|
+
start: () => void;
|
|
16
|
+
/** Stop auto play permanently until start is called */
|
|
17
|
+
stop: () => void;
|
|
18
|
+
/** Pause temporarily (e.g., during user interaction) */
|
|
19
|
+
pause: () => void;
|
|
20
|
+
/** Resume after a pause */
|
|
21
|
+
resume: () => void;
|
|
22
|
+
/** Notify the hook that user interacted (resets pause timer) */
|
|
23
|
+
onInteraction: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Manages automatic carousel advancement.
|
|
28
|
+
*
|
|
29
|
+
* @param config - Auto play configuration
|
|
30
|
+
* @param onAdvance - Callback to advance to the next or previous item
|
|
31
|
+
* @returns Auto play control methods and state
|
|
32
|
+
*/
|
|
33
|
+
export const useAutoPlay = (
|
|
34
|
+
config: AutoPlayConfig | boolean | undefined,
|
|
35
|
+
onAdvance: (direction: 'forward' | 'backward') => void
|
|
36
|
+
): UseAutoPlayReturn => {
|
|
37
|
+
const normalizedConfig: AutoPlayConfig = typeof config === 'boolean'
|
|
38
|
+
? { enabled: config }
|
|
39
|
+
: config ?? { enabled: false };
|
|
40
|
+
|
|
41
|
+
const {
|
|
42
|
+
enabled,
|
|
43
|
+
interval = DEFAULT_AUTO_PLAY_INTERVAL,
|
|
44
|
+
pauseOnInteraction = true,
|
|
45
|
+
direction = 'forward',
|
|
46
|
+
} = normalizedConfig;
|
|
47
|
+
|
|
48
|
+
const [isPlaying, setIsPlaying] = useState(enabled);
|
|
49
|
+
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
50
|
+
const isPausedRef = useRef(false);
|
|
51
|
+
|
|
52
|
+
const clearTimer = useCallback(() => {
|
|
53
|
+
if (intervalRef.current !== null) {
|
|
54
|
+
clearInterval(intervalRef.current);
|
|
55
|
+
intervalRef.current = null;
|
|
56
|
+
}
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
const startTimer = useCallback(() => {
|
|
60
|
+
clearTimer();
|
|
61
|
+
intervalRef.current = setInterval(() => {
|
|
62
|
+
if (!isPausedRef.current) {
|
|
63
|
+
onAdvance(direction);
|
|
64
|
+
}
|
|
65
|
+
}, interval);
|
|
66
|
+
}, [clearTimer, interval, direction, onAdvance]);
|
|
67
|
+
|
|
68
|
+
const start = useCallback(() => {
|
|
69
|
+
setIsPlaying(true);
|
|
70
|
+
isPausedRef.current = false;
|
|
71
|
+
startTimer();
|
|
72
|
+
}, [startTimer]);
|
|
73
|
+
|
|
74
|
+
const stop = useCallback(() => {
|
|
75
|
+
setIsPlaying(false);
|
|
76
|
+
isPausedRef.current = false;
|
|
77
|
+
clearTimer();
|
|
78
|
+
}, [clearTimer]);
|
|
79
|
+
|
|
80
|
+
const pause = useCallback(() => {
|
|
81
|
+
isPausedRef.current = true;
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
const resume = useCallback(() => {
|
|
85
|
+
isPausedRef.current = false;
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
const onInteraction = useCallback(() => {
|
|
89
|
+
if (pauseOnInteraction && isPlaying) {
|
|
90
|
+
pause();
|
|
91
|
+
setTimeout(() => {
|
|
92
|
+
resume();
|
|
93
|
+
}, interval);
|
|
94
|
+
}
|
|
95
|
+
}, [pauseOnInteraction, isPlaying, pause, resume, interval]);
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (enabled) {
|
|
99
|
+
start();
|
|
100
|
+
} else {
|
|
101
|
+
stop();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return clearTimer;
|
|
105
|
+
}, [enabled, start, stop, clearTimer]);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
isPlaying,
|
|
109
|
+
start,
|
|
110
|
+
stop,
|
|
111
|
+
pause,
|
|
112
|
+
resume,
|
|
113
|
+
onInteraction,
|
|
114
|
+
};
|
|
115
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file useCarousel hook
|
|
3
|
+
* @description Main hook for external carousel control and state management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback, useRef, useState } from 'react';
|
|
7
|
+
import type { CarouselRef } from '../types';
|
|
8
|
+
|
|
9
|
+
/** Return type of the useCarousel hook */
|
|
10
|
+
export interface UseCarouselReturn {
|
|
11
|
+
/** Ref to attach to the Carousel component */
|
|
12
|
+
ref: React.RefObject<CarouselRef>;
|
|
13
|
+
/** Current active index (reactive) */
|
|
14
|
+
activeIndex: number;
|
|
15
|
+
/** Total number of items (set when carousel mounts) */
|
|
16
|
+
totalItems: number;
|
|
17
|
+
/** Scroll to a specific index */
|
|
18
|
+
scrollTo: (index: number, animated?: boolean) => void;
|
|
19
|
+
/** Go to next item */
|
|
20
|
+
next: (animated?: boolean) => void;
|
|
21
|
+
/** Go to previous item */
|
|
22
|
+
prev: (animated?: boolean) => void;
|
|
23
|
+
/** Whether carousel is currently animating */
|
|
24
|
+
isAnimating: boolean;
|
|
25
|
+
/** Auto play controls */
|
|
26
|
+
autoPlay: {
|
|
27
|
+
start: () => void;
|
|
28
|
+
stop: () => void;
|
|
29
|
+
pause: () => void;
|
|
30
|
+
isPlaying: boolean;
|
|
31
|
+
};
|
|
32
|
+
/** Internal: callback for carousel to report index changes */
|
|
33
|
+
_onIndexChange: (index: number) => void;
|
|
34
|
+
/** Internal: callback for carousel to report total items */
|
|
35
|
+
_setTotalItems: (count: number) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* External hook for controlling and observing a Carousel component.
|
|
40
|
+
* Attach the returned ref to a Carousel component for full control.
|
|
41
|
+
*
|
|
42
|
+
* @returns Carousel control interface
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```tsx
|
|
46
|
+
* const carousel = useCarousel();
|
|
47
|
+
*
|
|
48
|
+
* <Carousel ref={carousel.ref} data={data} renderItem={renderItem} />
|
|
49
|
+
* <Button onPress={carousel.next} title="Next" />
|
|
50
|
+
* <Text>{carousel.activeIndex + 1} / {carousel.totalItems}</Text>
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export const useCarousel = (): UseCarouselReturn => {
|
|
54
|
+
const ref = useRef<CarouselRef>(null);
|
|
55
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
56
|
+
const [totalItems, setTotalItems] = useState(0);
|
|
57
|
+
const [isAnimating, setIsAnimating] = useState(false);
|
|
58
|
+
const [isPlaying, setIsPlaying] = useState(false);
|
|
59
|
+
|
|
60
|
+
const scrollTo = useCallback((index: number, animated = true) => {
|
|
61
|
+
setIsAnimating(true);
|
|
62
|
+
ref.current?.scrollTo(index, animated);
|
|
63
|
+
setTimeout(() => setIsAnimating(false), 300);
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
const next = useCallback((animated = true) => {
|
|
67
|
+
setIsAnimating(true);
|
|
68
|
+
ref.current?.next(animated);
|
|
69
|
+
setTimeout(() => setIsAnimating(false), 300);
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
const prev = useCallback((animated = true) => {
|
|
73
|
+
setIsAnimating(true);
|
|
74
|
+
ref.current?.prev(animated);
|
|
75
|
+
setTimeout(() => setIsAnimating(false), 300);
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
const startAutoPlay = useCallback(() => {
|
|
79
|
+
ref.current?.startAutoPlay();
|
|
80
|
+
setIsPlaying(true);
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
const stopAutoPlay = useCallback(() => {
|
|
84
|
+
ref.current?.stopAutoPlay();
|
|
85
|
+
setIsPlaying(false);
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
const pauseAutoPlay = useCallback(() => {
|
|
89
|
+
ref.current?.pauseAutoPlay();
|
|
90
|
+
setIsPlaying(false);
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
const _onIndexChange = useCallback((index: number) => {
|
|
94
|
+
setActiveIndex(index);
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
const _setTotalItems = useCallback((count: number) => {
|
|
98
|
+
setTotalItems(count);
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
ref,
|
|
103
|
+
activeIndex,
|
|
104
|
+
totalItems,
|
|
105
|
+
scrollTo,
|
|
106
|
+
next,
|
|
107
|
+
prev,
|
|
108
|
+
isAnimating,
|
|
109
|
+
autoPlay: {
|
|
110
|
+
start: startAutoPlay,
|
|
111
|
+
stop: stopAutoPlay,
|
|
112
|
+
pause: pauseAutoPlay,
|
|
113
|
+
isPlaying,
|
|
114
|
+
},
|
|
115
|
+
_onIndexChange,
|
|
116
|
+
_setTotalItems,
|
|
117
|
+
};
|
|
118
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file useCarouselGesture hook
|
|
3
|
+
* @description Orchestrates gesture handling for the carousel component
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useMemo } from 'react';
|
|
7
|
+
import type { GestureConfig, CarouselDirection } from '../types';
|
|
8
|
+
import { usePanGesture, type PanGestureOptions } from '../gestures/PanGestureManager';
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_ACTIVE_OFFSET_X,
|
|
11
|
+
DEFAULT_ACTIVE_OFFSET_Y,
|
|
12
|
+
DEFAULT_VELOCITY_THRESHOLD,
|
|
13
|
+
} from '../utils/constants';
|
|
14
|
+
|
|
15
|
+
/** Options for the carousel gesture hook */
|
|
16
|
+
export interface UseCarouselGestureOptions {
|
|
17
|
+
/** Total items in the carousel */
|
|
18
|
+
totalItems: number;
|
|
19
|
+
/** Size of each item */
|
|
20
|
+
itemSize: number;
|
|
21
|
+
/** Gap between items */
|
|
22
|
+
gap: number;
|
|
23
|
+
/** Snap points array */
|
|
24
|
+
snapPoints: number[];
|
|
25
|
+
/** Scroll direction */
|
|
26
|
+
direction: CarouselDirection;
|
|
27
|
+
/** Whether loop is enabled */
|
|
28
|
+
loop: boolean;
|
|
29
|
+
/** Whether interactions are enabled */
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
/** Custom gesture config */
|
|
32
|
+
gestureConfig?: GestureConfig;
|
|
33
|
+
/** Index change callback */
|
|
34
|
+
onIndexChange?: (index: number) => void;
|
|
35
|
+
/** Scroll start callback */
|
|
36
|
+
onScrollStart?: () => void;
|
|
37
|
+
/** Scroll end callback */
|
|
38
|
+
onScrollEnd?: (index: number) => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Sets up gesture handling for the carousel.
|
|
43
|
+
* Bridges user-facing gesture config with the internal PanGestureManager.
|
|
44
|
+
*
|
|
45
|
+
* @param options - Gesture configuration options
|
|
46
|
+
* @returns Gesture, shared values, and snap control
|
|
47
|
+
*/
|
|
48
|
+
export const useCarouselGesture = (options: UseCarouselGestureOptions) => {
|
|
49
|
+
const {
|
|
50
|
+
totalItems,
|
|
51
|
+
itemSize,
|
|
52
|
+
gap,
|
|
53
|
+
snapPoints,
|
|
54
|
+
direction,
|
|
55
|
+
loop,
|
|
56
|
+
enabled,
|
|
57
|
+
gestureConfig,
|
|
58
|
+
onIndexChange,
|
|
59
|
+
onScrollStart,
|
|
60
|
+
onScrollEnd,
|
|
61
|
+
} = options;
|
|
62
|
+
|
|
63
|
+
const isHorizontal = direction === 'horizontal';
|
|
64
|
+
|
|
65
|
+
const panOptions: PanGestureOptions = useMemo(() => {
|
|
66
|
+
const activeOffsetX = gestureConfig?.activeOffsetX
|
|
67
|
+
? (Array.isArray(gestureConfig.activeOffsetX)
|
|
68
|
+
? gestureConfig.activeOffsetX
|
|
69
|
+
: [-gestureConfig.activeOffsetX, gestureConfig.activeOffsetX]) as [number, number]
|
|
70
|
+
: (isHorizontal ? DEFAULT_ACTIVE_OFFSET_X : DEFAULT_ACTIVE_OFFSET_Y);
|
|
71
|
+
|
|
72
|
+
const activeOffsetY = gestureConfig?.activeOffsetY
|
|
73
|
+
? (Array.isArray(gestureConfig.activeOffsetY)
|
|
74
|
+
? gestureConfig.activeOffsetY
|
|
75
|
+
: [-gestureConfig.activeOffsetY, gestureConfig.activeOffsetY]) as [number, number]
|
|
76
|
+
: (isHorizontal ? DEFAULT_ACTIVE_OFFSET_Y : DEFAULT_ACTIVE_OFFSET_X);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
totalItems,
|
|
80
|
+
itemSize,
|
|
81
|
+
gap,
|
|
82
|
+
snapPoints,
|
|
83
|
+
isHorizontal,
|
|
84
|
+
loop,
|
|
85
|
+
enabled,
|
|
86
|
+
activeOffsetX,
|
|
87
|
+
activeOffsetY,
|
|
88
|
+
velocityThreshold: gestureConfig?.velocityThreshold ?? DEFAULT_VELOCITY_THRESHOLD,
|
|
89
|
+
onIndexChange,
|
|
90
|
+
onScrollStart,
|
|
91
|
+
onScrollEnd,
|
|
92
|
+
onConfigurePanGesture: gestureConfig?.onConfigurePanGesture,
|
|
93
|
+
};
|
|
94
|
+
}, [
|
|
95
|
+
totalItems,
|
|
96
|
+
itemSize,
|
|
97
|
+
gap,
|
|
98
|
+
snapPoints,
|
|
99
|
+
isHorizontal,
|
|
100
|
+
loop,
|
|
101
|
+
enabled,
|
|
102
|
+
gestureConfig,
|
|
103
|
+
onIndexChange,
|
|
104
|
+
onScrollStart,
|
|
105
|
+
onScrollEnd,
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
return usePanGesture(panOptions);
|
|
109
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file useItemAnimation hook
|
|
3
|
+
* @description Applies animation preset to a carousel item based on scroll progress
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useAnimatedStyle, type SharedValue } from 'react-native-reanimated';
|
|
7
|
+
import type { AnimationPresetFn, CustomAnimationFn } from '../types';
|
|
8
|
+
import { getPreset } from '../animations/registry';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Applies an animation preset to a carousel item.
|
|
12
|
+
* Returns an animated style that can be applied to an Animated.View.
|
|
13
|
+
*
|
|
14
|
+
* @param progress - Shared value with normalized animation progress
|
|
15
|
+
* @param preset - Preset name string or custom animation function
|
|
16
|
+
* @param animationConfig - Optional config overrides for the preset
|
|
17
|
+
* @param index - Item index (for custom animation functions)
|
|
18
|
+
* @param totalItems - Total number of items (for custom animation functions)
|
|
19
|
+
* @returns Animated style for the item
|
|
20
|
+
*/
|
|
21
|
+
export const useItemAnimation = (
|
|
22
|
+
progress: SharedValue<number>,
|
|
23
|
+
preset: string | CustomAnimationFn | undefined,
|
|
24
|
+
animationConfig?: Record<string, number>,
|
|
25
|
+
index: number = 0,
|
|
26
|
+
totalItems: number = 0
|
|
27
|
+
) => {
|
|
28
|
+
return useAnimatedStyle(() => {
|
|
29
|
+
if (!preset) {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (typeof preset === 'function') {
|
|
34
|
+
return preset(progress.value, index, totalItems, animationConfig) as Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const animFn = getPreset(preset) as AnimationPresetFn | undefined;
|
|
38
|
+
if (!animFn) {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return animFn(progress.value, animationConfig) as Record<string, unknown>;
|
|
43
|
+
}, [preset, animationConfig, index, totalItems]);
|
|
44
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file usePagination hook
|
|
3
|
+
* @description Manages pagination state derived from scroll progress
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useDerivedValue, type SharedValue } from 'react-native-reanimated';
|
|
7
|
+
|
|
8
|
+
/** Return type of the pagination hook */
|
|
9
|
+
export interface UsePaginationReturn {
|
|
10
|
+
/** Current page index as a shared value (for UI thread usage) */
|
|
11
|
+
currentPage: SharedValue<number>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Derives the current page index from scroll offset.
|
|
16
|
+
* Runs on the UI thread for smooth pagination indicator updates.
|
|
17
|
+
*
|
|
18
|
+
* @param scrollOffset - Current scroll offset shared value
|
|
19
|
+
* @param itemSize - Width or height of each item
|
|
20
|
+
* @param gap - Gap between items
|
|
21
|
+
* @param totalItems - Total number of items
|
|
22
|
+
* @returns Current page as shared value
|
|
23
|
+
*/
|
|
24
|
+
export const usePagination = (
|
|
25
|
+
scrollOffset: SharedValue<number>,
|
|
26
|
+
itemSize: number,
|
|
27
|
+
gap: number,
|
|
28
|
+
totalItems: number
|
|
29
|
+
): UsePaginationReturn => {
|
|
30
|
+
const stepSize = itemSize + gap;
|
|
31
|
+
|
|
32
|
+
const currentPage = useDerivedValue(() => {
|
|
33
|
+
if (stepSize === 0 || totalItems === 0) return 0;
|
|
34
|
+
const page = Math.round(scrollOffset.value / stepSize);
|
|
35
|
+
return Math.max(0, Math.min(page, totalItems - 1));
|
|
36
|
+
}, [stepSize, totalItems]);
|
|
37
|
+
|
|
38
|
+
return { currentPage };
|
|
39
|
+
};
|