@momo-kits/carousel 0.163.1-beta.4 → 0.164.1-test.10
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/core/LICENSE +21 -0
- package/core/README.md +16 -0
- package/core/components/Carousel.tsx +28 -0
- package/core/components/CarouselLayout.tsx +231 -0
- package/core/components/ItemLayout.tsx +215 -0
- package/core/components/ItemRenderer.tsx +126 -0
- package/core/components/LazyView.tsx +14 -0
- package/core/components/ScrollViewGesture.tsx +443 -0
- package/core/constants/index.ts +11 -0
- package/core/hooks/useAutoPlay.ts +63 -0
- package/core/hooks/useCarouselController.tsx +295 -0
- package/core/hooks/useCheckMounted.ts +14 -0
- package/core/hooks/useCommonVariables.ts +261 -0
- package/core/hooks/useInitProps.ts +177 -0
- package/core/hooks/useLayoutConfig.ts +35 -0
- package/core/hooks/useOffsetX.ts +81 -0
- package/core/hooks/useOnProgressChange.ts +33 -0
- package/core/hooks/usePanGestureProxy.ts +144 -0
- package/core/hooks/usePropsErrorBoundary.ts +41 -0
- package/core/hooks/useUpdateGestureConfig.ts +17 -0
- package/core/hooks/useVisibleRanges.tsx +141 -0
- package/core/layouts/index.tsx +10 -0
- package/core/layouts/normal.ts +32 -0
- package/core/layouts/parallax.ts +50 -0
- package/core/layouts/stack.ts +266 -0
- package/core/public-types.ts +209 -0
- package/core/store/index.tsx +63 -0
- package/core/types.ts +32 -0
- package/core/utils/carousel-direction.ts +39 -0
- package/core/utils/carousel-math.ts +156 -0
- package/core/utils/compute-gesture-translation.ts +29 -0
- package/core/utils/compute-offset-if-size-changed.ts +14 -0
- package/core/utils/computed-with-auto-fill-data.ts +92 -0
- package/core/utils/deal-with-animation.ts +19 -0
- package/core/utils/log.ts +12 -0
- package/core/utils/sanitize-animation-style.ts +20 -0
- package/index.tsx +390 -455
- package/package.json +7 -6
- package/types.ts +123 -48
- package/animation.ts +0 -62
package/core/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 Doho
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/core/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Vendored carousel engine
|
|
2
|
+
|
|
3
|
+
Source: [react-native-reanimated-carousel](https://github.com/dohooo/react-native-reanimated-carousel) v5.1.1, MIT (see `LICENSE`).
|
|
4
|
+
|
|
5
|
+
Copied from the upstream `src/` directory, minus two things we do not ship:
|
|
6
|
+
|
|
7
|
+
- `index.tsx` — the package root is `../index.tsx` instead.
|
|
8
|
+
- `components/Pagination/` — MoMo screens use `Pagination` from
|
|
9
|
+
`@momo-kits/foundation`.
|
|
10
|
+
|
|
11
|
+
Do not edit these files for MoMo-specific behaviour. `../index.tsx` decides what
|
|
12
|
+
the package exposes.
|
|
13
|
+
|
|
14
|
+
To update: re-copy upstream `src/` over `core/`, drop the two paths above again,
|
|
15
|
+
keep `LICENSE` and this file, then re-check `../index.tsx` and `../types.ts`
|
|
16
|
+
against the upstream public types.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useCommonVariables } from "../hooks/useCommonVariables";
|
|
3
|
+
import { useInitProps } from "../hooks/useInitProps";
|
|
4
|
+
import { usePropsErrorBoundary } from "../hooks/usePropsErrorBoundary";
|
|
5
|
+
import { GlobalStateProvider } from "../store";
|
|
6
|
+
import type { CarouselProps, CarouselRef } from "../types";
|
|
7
|
+
import { CarouselLayout } from "./CarouselLayout";
|
|
8
|
+
|
|
9
|
+
const CarouselImpl = React.forwardRef<CarouselRef, CarouselProps<unknown>>((_props, ref) => {
|
|
10
|
+
const props = useInitProps(_props);
|
|
11
|
+
const { dataLength } = props;
|
|
12
|
+
const commonVariables = useCommonVariables(props);
|
|
13
|
+
usePropsErrorBoundary({ ...props, dataLength });
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<GlobalStateProvider value={{ props, common: commonVariables }}>
|
|
17
|
+
<CarouselLayout ref={ref} />
|
|
18
|
+
</GlobalStateProvider>
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
type CarouselComponent = <T>(
|
|
23
|
+
props: CarouselProps<T> & {
|
|
24
|
+
ref?: React.Ref<CarouselRef>;
|
|
25
|
+
}
|
|
26
|
+
) => React.ReactElement;
|
|
27
|
+
|
|
28
|
+
export const Carousel = CarouselImpl as CarouselComponent;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type StyleProp, StyleSheet, View, type ViewStyle } from "react-native";
|
|
3
|
+
import { useAnimatedReaction, useAnimatedStyle, useDerivedValue } from "react-native-reanimated";
|
|
4
|
+
import { scheduleOnRN } from "react-native-worklets";
|
|
5
|
+
import { useAutoPlay } from "../hooks/useAutoPlay";
|
|
6
|
+
import { useCarouselController } from "../hooks/useCarouselController";
|
|
7
|
+
import { useLayoutConfig } from "../hooks/useLayoutConfig";
|
|
8
|
+
import { useOnProgressChange } from "../hooks/useOnProgressChange";
|
|
9
|
+
import { useGlobalState } from "../store";
|
|
10
|
+
import type { CarouselRef } from "../types";
|
|
11
|
+
import { ItemRenderer } from "./ItemRenderer";
|
|
12
|
+
import { ScrollViewGesture } from "./ScrollViewGesture";
|
|
13
|
+
|
|
14
|
+
export function resolveCarouselLayoutStyle(params: {
|
|
15
|
+
flattenedStyle: Partial<ViewStyle>;
|
|
16
|
+
isVertical: boolean;
|
|
17
|
+
measuredSize: number;
|
|
18
|
+
sizeExplicit: boolean;
|
|
19
|
+
}) {
|
|
20
|
+
"worklet";
|
|
21
|
+
|
|
22
|
+
const { flattenedStyle, isVertical, measuredSize, sizeExplicit } = params;
|
|
23
|
+
const { width, height } = flattenedStyle;
|
|
24
|
+
const resolvedMainAxisSize = measuredSize || "100%";
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
width: width ?? (isVertical ? "100%" : sizeExplicit ? resolvedMainAxisSize : "100%"),
|
|
28
|
+
height: height ?? (isVertical ? (sizeExplicit ? resolvedMainAxisSize : "100%") : "100%"),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const CarouselLayout = React.forwardRef<CarouselRef>((_props, ref) => {
|
|
33
|
+
const { props, common } = useGlobalState();
|
|
34
|
+
|
|
35
|
+
const {
|
|
36
|
+
testID,
|
|
37
|
+
loop,
|
|
38
|
+
autoFillData,
|
|
39
|
+
// Fill data with autoFillData
|
|
40
|
+
data,
|
|
41
|
+
rawData,
|
|
42
|
+
// Length of fill data
|
|
43
|
+
dataLength,
|
|
44
|
+
// Length of raw data
|
|
45
|
+
rawDataLength,
|
|
46
|
+
layout,
|
|
47
|
+
style,
|
|
48
|
+
contentContainerStyle,
|
|
49
|
+
orientation,
|
|
50
|
+
autoplay,
|
|
51
|
+
renderWindowSize,
|
|
52
|
+
autoplayDirection,
|
|
53
|
+
autoplayInterval,
|
|
54
|
+
animation,
|
|
55
|
+
renderItem,
|
|
56
|
+
onSnapToItem,
|
|
57
|
+
onScrollStart,
|
|
58
|
+
onProgressChange,
|
|
59
|
+
progress,
|
|
60
|
+
itemAnimation,
|
|
61
|
+
defaultIndex,
|
|
62
|
+
keyExtractor,
|
|
63
|
+
} = props;
|
|
64
|
+
const isVertical = orientation === "vertical";
|
|
65
|
+
|
|
66
|
+
const { size, handlerOffset, resolvedSize, sizePhase, sizeExplicit } = common;
|
|
67
|
+
const layoutConfig = useLayoutConfig({ ...props, size });
|
|
68
|
+
|
|
69
|
+
const isSizeReady = useDerivedValue(() => {
|
|
70
|
+
const currentSize = resolvedSize.value ?? 0;
|
|
71
|
+
return sizePhase.value === "ready" && currentSize > 0;
|
|
72
|
+
}, [resolvedSize, sizePhase]);
|
|
73
|
+
|
|
74
|
+
const offsetX = useDerivedValue(() => {
|
|
75
|
+
const currentSize = resolvedSize.value ?? 0;
|
|
76
|
+
if (currentSize <= 0) return 0;
|
|
77
|
+
|
|
78
|
+
const totalSize = currentSize * dataLength;
|
|
79
|
+
const value = handlerOffset.value;
|
|
80
|
+
if (!loop || totalSize === 0) {
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const x = value % totalSize;
|
|
85
|
+
return Number.isNaN(x) ? 0 : x;
|
|
86
|
+
}, [loop, dataLength, handlerOffset, resolvedSize]);
|
|
87
|
+
|
|
88
|
+
useOnProgressChange({
|
|
89
|
+
loop,
|
|
90
|
+
offset: handlerOffset,
|
|
91
|
+
progress,
|
|
92
|
+
size,
|
|
93
|
+
sizeReady: isSizeReady,
|
|
94
|
+
rawDataLength,
|
|
95
|
+
onProgressChange,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const _onScrollEnd = React.useCallback(
|
|
99
|
+
(realIndex: number) => {
|
|
100
|
+
if (onSnapToItem) onSnapToItem(realIndex);
|
|
101
|
+
},
|
|
102
|
+
[onSnapToItem]
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const carouselController = useCarouselController({
|
|
106
|
+
ref,
|
|
107
|
+
loop,
|
|
108
|
+
size,
|
|
109
|
+
dataLength,
|
|
110
|
+
rawDataLength,
|
|
111
|
+
handlerOffset,
|
|
112
|
+
animation,
|
|
113
|
+
defaultIndex,
|
|
114
|
+
onMovementEnd: _onScrollEnd,
|
|
115
|
+
onScrollStart,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const {
|
|
119
|
+
start: startAutoPlay,
|
|
120
|
+
pause: pauseAutoPlay,
|
|
121
|
+
trigger: triggerAutoPlay,
|
|
122
|
+
} = useAutoPlay({
|
|
123
|
+
autoplay,
|
|
124
|
+
autoplayDirection,
|
|
125
|
+
autoplayInterval,
|
|
126
|
+
carouselController,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
useAnimatedReaction(
|
|
130
|
+
() => ({ ready: isSizeReady.value }),
|
|
131
|
+
(state, previous) => {
|
|
132
|
+
if (!autoplay) return;
|
|
133
|
+
if (state.ready === previous?.ready) return;
|
|
134
|
+
|
|
135
|
+
if (state.ready) scheduleOnRN(triggerAutoPlay);
|
|
136
|
+
else scheduleOnRN(pauseAutoPlay);
|
|
137
|
+
},
|
|
138
|
+
[autoplay]
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const scrollViewGestureOnScrollStart = React.useCallback(() => {
|
|
142
|
+
carouselController.startMovement();
|
|
143
|
+
pauseAutoPlay();
|
|
144
|
+
onScrollStart?.();
|
|
145
|
+
}, [carouselController, onScrollStart, pauseAutoPlay]);
|
|
146
|
+
|
|
147
|
+
const scrollViewGestureOnScrollEnd = React.useCallback(() => {
|
|
148
|
+
const settledIndex = carouselController.settle();
|
|
149
|
+
startAutoPlay();
|
|
150
|
+
_onScrollEnd(settledIndex);
|
|
151
|
+
}, [_onScrollEnd, carouselController, startAutoPlay]);
|
|
152
|
+
|
|
153
|
+
const scrollViewGestureOnTouchBegin = React.useCallback(pauseAutoPlay, [pauseAutoPlay]);
|
|
154
|
+
|
|
155
|
+
const scrollViewGestureOnTouchEnd = React.useCallback(startAutoPlay, [startAutoPlay]);
|
|
156
|
+
|
|
157
|
+
const { opacity, transform, ...restContentContainerStyle } =
|
|
158
|
+
StyleSheet.flatten(contentContainerStyle as StyleProp<ViewStyle>) || {};
|
|
159
|
+
const flattenedStyle = StyleSheet.flatten(style) || {};
|
|
160
|
+
|
|
161
|
+
const layoutStyle = useAnimatedStyle(() => {
|
|
162
|
+
const measuredSize = resolvedSize.value ?? 0;
|
|
163
|
+
const { width, height } = resolveCarouselLayoutStyle({
|
|
164
|
+
flattenedStyle,
|
|
165
|
+
isVertical,
|
|
166
|
+
measuredSize,
|
|
167
|
+
sizeExplicit,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
width,
|
|
172
|
+
height,
|
|
173
|
+
opacity: isSizeReady.value ? 1 : 0,
|
|
174
|
+
};
|
|
175
|
+
}, [flattenedStyle, isSizeReady, isVertical, resolvedSize, sizePhase, sizeExplicit]);
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<View testID={testID} style={[styles.layoutContainer, style]} onLayout={props.onLayout}>
|
|
179
|
+
<ScrollViewGesture
|
|
180
|
+
size={size}
|
|
181
|
+
key={layout?.type}
|
|
182
|
+
translation={handlerOffset}
|
|
183
|
+
style={[
|
|
184
|
+
styles.contentContainer,
|
|
185
|
+
layoutStyle,
|
|
186
|
+
restContentContainerStyle,
|
|
187
|
+
isVertical ? styles.itemsVertical : styles.itemsHorizontal,
|
|
188
|
+
]}
|
|
189
|
+
testID="carousel-content-container"
|
|
190
|
+
onScrollStart={scrollViewGestureOnScrollStart}
|
|
191
|
+
onScrollEnd={scrollViewGestureOnScrollEnd}
|
|
192
|
+
onTouchBegin={scrollViewGestureOnTouchBegin}
|
|
193
|
+
onTouchEnd={scrollViewGestureOnTouchEnd}
|
|
194
|
+
>
|
|
195
|
+
<ItemRenderer
|
|
196
|
+
data={data}
|
|
197
|
+
rawData={rawData}
|
|
198
|
+
dataLength={dataLength}
|
|
199
|
+
rawDataLength={rawDataLength}
|
|
200
|
+
loop={loop}
|
|
201
|
+
size={size}
|
|
202
|
+
renderWindowSize={renderWindowSize}
|
|
203
|
+
defaultIndex={defaultIndex}
|
|
204
|
+
autoFillData={autoFillData}
|
|
205
|
+
offsetX={offsetX}
|
|
206
|
+
handlerOffset={handlerOffset}
|
|
207
|
+
layoutConfig={layoutConfig}
|
|
208
|
+
renderItem={renderItem}
|
|
209
|
+
itemAnimation={itemAnimation}
|
|
210
|
+
keyExtractor={keyExtractor}
|
|
211
|
+
/>
|
|
212
|
+
</ScrollViewGesture>
|
|
213
|
+
</View>
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const styles = StyleSheet.create({
|
|
218
|
+
layoutContainer: {
|
|
219
|
+
display: "flex",
|
|
220
|
+
overflow: "hidden",
|
|
221
|
+
},
|
|
222
|
+
contentContainer: {
|
|
223
|
+
overflow: "hidden",
|
|
224
|
+
},
|
|
225
|
+
itemsHorizontal: {
|
|
226
|
+
flexDirection: "row",
|
|
227
|
+
},
|
|
228
|
+
itemsVertical: {
|
|
229
|
+
flexDirection: "column",
|
|
230
|
+
},
|
|
231
|
+
});
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { LayoutChangeEvent, ViewStyle } from "react-native";
|
|
3
|
+
import { StyleSheet } from "react-native";
|
|
4
|
+
import type { SharedValue } from "react-native-reanimated";
|
|
5
|
+
import Animated, {
|
|
6
|
+
useAnimatedReaction,
|
|
7
|
+
useAnimatedStyle,
|
|
8
|
+
useDerivedValue,
|
|
9
|
+
useSharedValue,
|
|
10
|
+
} from "react-native-reanimated";
|
|
11
|
+
import { scheduleOnRN, scheduleOnUI } from "react-native-worklets";
|
|
12
|
+
|
|
13
|
+
import type { OffsetOptions } from "../hooks/useOffsetX";
|
|
14
|
+
import { useOffsetX } from "../hooks/useOffsetX";
|
|
15
|
+
import type { VisibleRangesValue } from "../hooks/useVisibleRanges";
|
|
16
|
+
import { useGlobalState } from "../store";
|
|
17
|
+
import type { CarouselItemAnimation } from "../types";
|
|
18
|
+
import { getHorizontalStackOffsetType } from "../utils/carousel-direction";
|
|
19
|
+
import { positiveModulo } from "../utils/carousel-math";
|
|
20
|
+
import { sanitizeAnimationStyle } from "../utils/sanitize-animation-style";
|
|
21
|
+
|
|
22
|
+
export type ItemAnimationStyle = CarouselItemAnimation;
|
|
23
|
+
|
|
24
|
+
export function resolveItemMainAxisSize(params: {
|
|
25
|
+
explicitSize?: number | undefined;
|
|
26
|
+
effectivePageSize?: number | undefined;
|
|
27
|
+
measuredSize: number | null;
|
|
28
|
+
}) {
|
|
29
|
+
"worklet";
|
|
30
|
+
|
|
31
|
+
const { explicitSize, effectivePageSize, measuredSize } = params;
|
|
32
|
+
|
|
33
|
+
return typeof explicitSize === "number"
|
|
34
|
+
? explicitSize
|
|
35
|
+
: (effectivePageSize ?? measuredSize ?? "100%");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const ItemLayout: React.FC<{
|
|
39
|
+
index: number;
|
|
40
|
+
rawIndex: number;
|
|
41
|
+
handlerOffset: SharedValue<number>;
|
|
42
|
+
visibleRanges: VisibleRangesValue;
|
|
43
|
+
animationStyle: ItemAnimationStyle;
|
|
44
|
+
children: (ctx: {
|
|
45
|
+
relativeProgress: SharedValue<number>;
|
|
46
|
+
}) => React.ReactElement;
|
|
47
|
+
}> = (props) => {
|
|
48
|
+
const { handlerOffset, index, rawIndex, children, visibleRanges, animationStyle } = props;
|
|
49
|
+
|
|
50
|
+
const {
|
|
51
|
+
props: { loop, dataLength, directionSign, orientation, layout, style, itemSize },
|
|
52
|
+
common,
|
|
53
|
+
layout: { updateItemDimensions },
|
|
54
|
+
} = useGlobalState();
|
|
55
|
+
|
|
56
|
+
const measuredSize = useSharedValue<{ width: number | null; height: number | null }>({
|
|
57
|
+
width: null,
|
|
58
|
+
height: null,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const fallbackSize = common.size;
|
|
62
|
+
const isVertical = orientation === "vertical";
|
|
63
|
+
const { width: styleWidth, height: styleHeight } = StyleSheet.flatten(style) || {};
|
|
64
|
+
const styleWidthNumber = typeof styleWidth === "number" ? styleWidth : undefined;
|
|
65
|
+
const styleHeightNumber = typeof styleHeight === "number" ? styleHeight : undefined;
|
|
66
|
+
|
|
67
|
+
const size = (itemSize ?? fallbackSize) || 0;
|
|
68
|
+
const effectivePageSize = size > 0 ? size : undefined;
|
|
69
|
+
|
|
70
|
+
const dimensionsStyle = useAnimatedStyle<ViewStyle>(() => {
|
|
71
|
+
const widthCandidate = isVertical ? styleWidthNumber : itemSize;
|
|
72
|
+
const heightCandidate = isVertical ? itemSize : styleHeightNumber;
|
|
73
|
+
|
|
74
|
+
const computedWidth = isVertical
|
|
75
|
+
? typeof widthCandidate === "number"
|
|
76
|
+
? widthCandidate
|
|
77
|
+
: (measuredSize.value.width ?? "100%")
|
|
78
|
+
: resolveItemMainAxisSize({
|
|
79
|
+
explicitSize: widthCandidate,
|
|
80
|
+
effectivePageSize,
|
|
81
|
+
measuredSize: measuredSize.value.width,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const computedHeight = isVertical
|
|
85
|
+
? resolveItemMainAxisSize({
|
|
86
|
+
explicitSize: heightCandidate,
|
|
87
|
+
effectivePageSize,
|
|
88
|
+
measuredSize: measuredSize.value.height,
|
|
89
|
+
})
|
|
90
|
+
: typeof heightCandidate === "number"
|
|
91
|
+
? heightCandidate
|
|
92
|
+
: (measuredSize.value.height ?? "100%");
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
width: computedWidth,
|
|
96
|
+
height: computedHeight,
|
|
97
|
+
};
|
|
98
|
+
}, [effectivePageSize, isVertical, itemSize, styleHeightNumber, styleWidthNumber]);
|
|
99
|
+
|
|
100
|
+
let offsetXConfig: OffsetOptions = {
|
|
101
|
+
handlerOffset,
|
|
102
|
+
index,
|
|
103
|
+
size,
|
|
104
|
+
dataLength,
|
|
105
|
+
loop,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if (layout?.type === "horizontal-stack") {
|
|
109
|
+
offsetXConfig = {
|
|
110
|
+
handlerOffset,
|
|
111
|
+
index,
|
|
112
|
+
size,
|
|
113
|
+
dataLength,
|
|
114
|
+
loop,
|
|
115
|
+
type: getHorizontalStackOffsetType(layout.exitDirection ?? "left", directionSign),
|
|
116
|
+
viewCount: layout.visibleCount,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const x = useOffsetX(offsetXConfig, visibleRanges);
|
|
121
|
+
const isCurrentForAccessibility = React.useCallback(() => {
|
|
122
|
+
"worklet";
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
size > 0 &&
|
|
126
|
+
dataLength > 0 &&
|
|
127
|
+
positiveModulo(Math.round(-handlerOffset.value / size), dataLength) === index
|
|
128
|
+
);
|
|
129
|
+
}, [dataLength, handlerOffset, index, size]);
|
|
130
|
+
const [isAccessibilityCurrent, setIsAccessibilityCurrent] =
|
|
131
|
+
React.useState(isCurrentForAccessibility);
|
|
132
|
+
|
|
133
|
+
useAnimatedReaction(
|
|
134
|
+
isCurrentForAccessibility,
|
|
135
|
+
(isCurrent, wasCurrent) => {
|
|
136
|
+
if (isCurrent !== wasCurrent) {
|
|
137
|
+
scheduleOnRN(setIsAccessibilityCurrent, isCurrent);
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
[isCurrentForAccessibility]
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const relativeProgress = useDerivedValue(() => {
|
|
144
|
+
if (!size) return 0;
|
|
145
|
+
return x.value / size;
|
|
146
|
+
}, [x, size]);
|
|
147
|
+
const animatedStyle = useAnimatedStyle<ViewStyle>(() => {
|
|
148
|
+
const safeSize = size || 1;
|
|
149
|
+
return sanitizeAnimationStyle(animationStyle(x.value / safeSize, rawIndex));
|
|
150
|
+
}, [animationStyle, rawIndex, x, size]);
|
|
151
|
+
|
|
152
|
+
// TODO: For dynamic dimension in the future
|
|
153
|
+
// function handleLayout(e: LayoutChangeEvent) {
|
|
154
|
+
// const { width, height } = e.nativeEvent.layout;
|
|
155
|
+
// updateItemDimensions(index, { width, height });
|
|
156
|
+
// }
|
|
157
|
+
|
|
158
|
+
const child = children({ relativeProgress });
|
|
159
|
+
|
|
160
|
+
type LayoutableProps = {
|
|
161
|
+
collapsable?: boolean;
|
|
162
|
+
onLayout?: (event: LayoutChangeEvent) => void;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const enhancedChild = React.isValidElement<LayoutableProps>(child)
|
|
166
|
+
? React.cloneElement(child, {
|
|
167
|
+
collapsable: false,
|
|
168
|
+
onLayout: (event: LayoutChangeEvent) => {
|
|
169
|
+
const { width: layoutWidth, height: layoutHeight } = event.nativeEvent.layout;
|
|
170
|
+
if (layoutWidth > 0 && layoutHeight > 0) {
|
|
171
|
+
scheduleOnUI(() => {
|
|
172
|
+
const { width: prevWidth, height: prevHeight } = measuredSize.value;
|
|
173
|
+
if (prevWidth === layoutWidth && prevHeight === layoutHeight) return;
|
|
174
|
+
|
|
175
|
+
measuredSize.value = {
|
|
176
|
+
width: layoutWidth,
|
|
177
|
+
height: layoutHeight,
|
|
178
|
+
};
|
|
179
|
+
updateItemDimensions(index, {
|
|
180
|
+
width: layoutWidth,
|
|
181
|
+
height: layoutHeight,
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
child.props?.onLayout?.(event);
|
|
187
|
+
},
|
|
188
|
+
})
|
|
189
|
+
: child;
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<Animated.View
|
|
193
|
+
accessibilityElementsHidden={!isAccessibilityCurrent}
|
|
194
|
+
accessible={isAccessibilityCurrent ? undefined : false}
|
|
195
|
+
aria-hidden={!isAccessibilityCurrent}
|
|
196
|
+
importantForAccessibility={isAccessibilityCurrent ? "auto" : "no-hide-descendants"}
|
|
197
|
+
style={[
|
|
198
|
+
{
|
|
199
|
+
position: "absolute",
|
|
200
|
+
pointerEvents: "box-none",
|
|
201
|
+
},
|
|
202
|
+
dimensionsStyle,
|
|
203
|
+
animatedStyle,
|
|
204
|
+
]}
|
|
205
|
+
/**
|
|
206
|
+
* We use this testID to know when the carousel item is ready to be tested in test.
|
|
207
|
+
* e.g.
|
|
208
|
+
* The testID of first item will be changed to __CAROUSEL_ITEM_0_READY__ from __CAROUSEL_ITEM_0_NOT_READY__ when the item is ready.
|
|
209
|
+
* */
|
|
210
|
+
testID={`__CAROUSEL_ITEM_${index}__`}
|
|
211
|
+
>
|
|
212
|
+
{enhancedChild}
|
|
213
|
+
</Animated.View>
|
|
214
|
+
);
|
|
215
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { FC } from "react";
|
|
3
|
+
import type { SharedValue } from "react-native-reanimated";
|
|
4
|
+
import { useAnimatedReaction } from "react-native-reanimated";
|
|
5
|
+
import { scheduleOnRN } from "react-native-worklets";
|
|
6
|
+
|
|
7
|
+
import type { ItemAnimationStyle } from "./ItemLayout";
|
|
8
|
+
import { ItemLayout } from "./ItemLayout";
|
|
9
|
+
|
|
10
|
+
import type { VisibleRanges } from "../hooks/useVisibleRanges";
|
|
11
|
+
import { computeVisibleRanges, useVisibleRanges } from "../hooks/useVisibleRanges";
|
|
12
|
+
import type { CarouselItemAnimation, CarouselRenderItem } from "../types";
|
|
13
|
+
import { computedRealIndexWithAutoFillData } from "../utils/computed-with-auto-fill-data";
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
data: unknown[];
|
|
17
|
+
rawData: unknown[];
|
|
18
|
+
dataLength: number;
|
|
19
|
+
rawDataLength: number;
|
|
20
|
+
loop: boolean;
|
|
21
|
+
size: number;
|
|
22
|
+
renderWindowSize?: number | undefined;
|
|
23
|
+
defaultIndex: number;
|
|
24
|
+
autoFillData: boolean;
|
|
25
|
+
offsetX: SharedValue<number>;
|
|
26
|
+
handlerOffset: SharedValue<number>;
|
|
27
|
+
layoutConfig: ItemAnimationStyle;
|
|
28
|
+
renderItem: CarouselRenderItem<unknown>;
|
|
29
|
+
itemAnimation?: CarouselItemAnimation | undefined;
|
|
30
|
+
keyExtractor?: ((item: unknown, index: number) => string) | undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const ItemRenderer: FC<Props> = (props) => {
|
|
34
|
+
const {
|
|
35
|
+
data,
|
|
36
|
+
rawData,
|
|
37
|
+
size,
|
|
38
|
+
renderWindowSize,
|
|
39
|
+
defaultIndex,
|
|
40
|
+
handlerOffset,
|
|
41
|
+
offsetX,
|
|
42
|
+
dataLength,
|
|
43
|
+
rawDataLength,
|
|
44
|
+
loop,
|
|
45
|
+
autoFillData,
|
|
46
|
+
layoutConfig,
|
|
47
|
+
renderItem,
|
|
48
|
+
itemAnimation,
|
|
49
|
+
keyExtractor,
|
|
50
|
+
} = props;
|
|
51
|
+
|
|
52
|
+
const visibleRanges = useVisibleRanges({
|
|
53
|
+
total: dataLength,
|
|
54
|
+
viewSize: size,
|
|
55
|
+
translation: handlerOffset,
|
|
56
|
+
windowSize: renderWindowSize,
|
|
57
|
+
loop,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Initialize with a sensible default to avoid blank render on first frame
|
|
61
|
+
const initialRanges: VisibleRanges = React.useMemo(
|
|
62
|
+
() =>
|
|
63
|
+
computeVisibleRanges({
|
|
64
|
+
total: dataLength,
|
|
65
|
+
windowSize: renderWindowSize,
|
|
66
|
+
currentIndex: defaultIndex,
|
|
67
|
+
loop,
|
|
68
|
+
}),
|
|
69
|
+
[dataLength, defaultIndex, loop, renderWindowSize]
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const [displayedItems, setDisplayedItems] = React.useState<VisibleRanges>(initialRanges);
|
|
73
|
+
|
|
74
|
+
useAnimatedReaction(
|
|
75
|
+
() => visibleRanges.value,
|
|
76
|
+
(ranges) => scheduleOnRN(setDisplayedItems, ranges),
|
|
77
|
+
[visibleRanges]
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<>
|
|
82
|
+
{data.map((item, index) => {
|
|
83
|
+
const realIndex = computedRealIndexWithAutoFillData({
|
|
84
|
+
index,
|
|
85
|
+
dataLength: rawDataLength,
|
|
86
|
+
loop,
|
|
87
|
+
autoFillData,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const { negativeRange, positiveRange } = displayedItems;
|
|
91
|
+
|
|
92
|
+
const shouldRender =
|
|
93
|
+
(index >= negativeRange[0] && index <= negativeRange[1]) ||
|
|
94
|
+
(index >= positiveRange[0] && index <= positiveRange[1]);
|
|
95
|
+
|
|
96
|
+
if (!shouldRender) return null;
|
|
97
|
+
|
|
98
|
+
const rawItem = rawData[realIndex] ?? item;
|
|
99
|
+
const key = keyExtractor
|
|
100
|
+
? `${keyExtractor(rawItem, realIndex)}::rnrc-copy-${Math.floor(
|
|
101
|
+
index / Math.max(rawDataLength, 1)
|
|
102
|
+
)}`
|
|
103
|
+
: String(index);
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<ItemLayout
|
|
107
|
+
key={key}
|
|
108
|
+
index={index}
|
|
109
|
+
rawIndex={realIndex}
|
|
110
|
+
handlerOffset={offsetX}
|
|
111
|
+
visibleRanges={visibleRanges}
|
|
112
|
+
animationStyle={itemAnimation || layoutConfig}
|
|
113
|
+
>
|
|
114
|
+
{({ relativeProgress }) =>
|
|
115
|
+
renderItem({
|
|
116
|
+
item: rawItem,
|
|
117
|
+
index: realIndex,
|
|
118
|
+
relativeProgress,
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
</ItemLayout>
|
|
122
|
+
);
|
|
123
|
+
})}
|
|
124
|
+
</>
|
|
125
|
+
);
|
|
126
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PropsWithChildren } from "react";
|
|
2
|
+
import React from "react";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
shouldUpdate: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const LazyView: React.FC<PropsWithChildren<Props>> = (props) => {
|
|
9
|
+
const { shouldUpdate, children } = props;
|
|
10
|
+
|
|
11
|
+
if (!shouldUpdate) return <></>;
|
|
12
|
+
|
|
13
|
+
return <>{children}</>;
|
|
14
|
+
};
|