@one-am/react-native-simple-image-slider 0.2.2

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.
Files changed (71) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +74 -0
  3. package/lib/commonjs/@types/styled.d.js +4 -0
  4. package/lib/commonjs/@types/styled.d.js.map +1 -0
  5. package/lib/commonjs/BaseSimpleImageSlider.js +194 -0
  6. package/lib/commonjs/BaseSimpleImageSlider.js.map +1 -0
  7. package/lib/commonjs/FullScreenImageSlider.js +126 -0
  8. package/lib/commonjs/FullScreenImageSlider.js.map +1 -0
  9. package/lib/commonjs/PageCounter.js +39 -0
  10. package/lib/commonjs/PageCounter.js.map +1 -0
  11. package/lib/commonjs/PinchToZoom.js +173 -0
  12. package/lib/commonjs/PinchToZoom.js.map +1 -0
  13. package/lib/commonjs/SimpleImageSlider.js +59 -0
  14. package/lib/commonjs/SimpleImageSlider.js.map +1 -0
  15. package/lib/commonjs/StyledComponentsThemeProvider.js +51 -0
  16. package/lib/commonjs/StyledComponentsThemeProvider.js.map +1 -0
  17. package/lib/commonjs/index.js +35 -0
  18. package/lib/commonjs/index.js.map +1 -0
  19. package/lib/commonjs/utils/clamp.js +13 -0
  20. package/lib/commonjs/utils/clamp.js.map +1 -0
  21. package/lib/commonjs/utils/renderProp.js +17 -0
  22. package/lib/commonjs/utils/renderProp.js.map +1 -0
  23. package/lib/module/@types/styled.d.js +2 -0
  24. package/lib/module/@types/styled.d.js.map +1 -0
  25. package/lib/module/BaseSimpleImageSlider.js +185 -0
  26. package/lib/module/BaseSimpleImageSlider.js.map +1 -0
  27. package/lib/module/FullScreenImageSlider.js +117 -0
  28. package/lib/module/FullScreenImageSlider.js.map +1 -0
  29. package/lib/module/PageCounter.js +31 -0
  30. package/lib/module/PageCounter.js.map +1 -0
  31. package/lib/module/PinchToZoom.js +165 -0
  32. package/lib/module/PinchToZoom.js.map +1 -0
  33. package/lib/module/SimpleImageSlider.js +50 -0
  34. package/lib/module/SimpleImageSlider.js.map +1 -0
  35. package/lib/module/StyledComponentsThemeProvider.js +43 -0
  36. package/lib/module/StyledComponentsThemeProvider.js.map +1 -0
  37. package/lib/module/index.js +6 -0
  38. package/lib/module/index.js.map +1 -0
  39. package/lib/module/utils/clamp.js +6 -0
  40. package/lib/module/utils/clamp.js.map +1 -0
  41. package/lib/module/utils/renderProp.js +9 -0
  42. package/lib/module/utils/renderProp.js.map +1 -0
  43. package/lib/typescript/src/BaseSimpleImageSlider.d.ts +33 -0
  44. package/lib/typescript/src/BaseSimpleImageSlider.d.ts.map +1 -0
  45. package/lib/typescript/src/FullScreenImageSlider.d.ts +15 -0
  46. package/lib/typescript/src/FullScreenImageSlider.d.ts.map +1 -0
  47. package/lib/typescript/src/PageCounter.d.ts +10 -0
  48. package/lib/typescript/src/PageCounter.d.ts.map +1 -0
  49. package/lib/typescript/src/PinchToZoom.d.ts +18 -0
  50. package/lib/typescript/src/PinchToZoom.d.ts.map +1 -0
  51. package/lib/typescript/src/SimpleImageSlider.d.ts +11 -0
  52. package/lib/typescript/src/SimpleImageSlider.d.ts.map +1 -0
  53. package/lib/typescript/src/StyledComponentsThemeProvider.d.ts +3 -0
  54. package/lib/typescript/src/StyledComponentsThemeProvider.d.ts.map +1 -0
  55. package/lib/typescript/src/index.d.ts +6 -0
  56. package/lib/typescript/src/index.d.ts.map +1 -0
  57. package/lib/typescript/src/utils/clamp.d.ts +2 -0
  58. package/lib/typescript/src/utils/clamp.d.ts.map +1 -0
  59. package/lib/typescript/src/utils/renderProp.d.ts +4 -0
  60. package/lib/typescript/src/utils/renderProp.d.ts.map +1 -0
  61. package/package.json +170 -0
  62. package/src/@types/styled.d.ts +39 -0
  63. package/src/BaseSimpleImageSlider.tsx +281 -0
  64. package/src/FullScreenImageSlider.tsx +164 -0
  65. package/src/PageCounter.tsx +39 -0
  66. package/src/PinchToZoom.tsx +323 -0
  67. package/src/SimpleImageSlider.tsx +72 -0
  68. package/src/StyledComponentsThemeProvider.tsx +48 -0
  69. package/src/index.tsx +14 -0
  70. package/src/utils/clamp.ts +4 -0
  71. package/src/utils/renderProp.tsx +22 -0
@@ -0,0 +1,39 @@
1
+ import React, { useMemo } from 'react';
2
+ import { type StyleProp, StyleSheet, Text, View, type ViewStyle } from 'react-native';
3
+ import { type DefaultTheme, useTheme } from 'styled-components/native';
4
+
5
+ type PageCounterProps = {
6
+ currentPage: number;
7
+ totalPages: number;
8
+ style?: StyleProp<ViewStyle>;
9
+ };
10
+
11
+ export default function PageCounter({ currentPage, totalPages, style }: PageCounterProps) {
12
+ const theme = useTheme();
13
+ const styles = useMemo(() => makeStyles(theme), [theme]);
14
+
15
+ return (
16
+ <View style={[styles.container, style]}>
17
+ <Text>
18
+ {currentPage} / {totalPages}
19
+ </Text>
20
+ </View>
21
+ );
22
+ }
23
+
24
+ const makeStyles = (theme: DefaultTheme) => {
25
+ return StyleSheet.create({
26
+ container: {
27
+ backgroundColor: theme.colors.pageCounterBackground,
28
+ borderWidth: 1,
29
+ borderColor: theme.colors.pageCounterBorder,
30
+ borderRadius: 8,
31
+ paddingHorizontal: 5,
32
+ paddingVertical: 6,
33
+ width: 75,
34
+ flexDirection: 'row',
35
+ alignItems: 'center',
36
+ justifyContent: 'center',
37
+ },
38
+ });
39
+ };
@@ -0,0 +1,323 @@
1
+ import React, { type PropsWithChildren, useCallback, useMemo } from 'react';
2
+ import {
3
+ type LayoutChangeEvent,
4
+ type StyleProp,
5
+ useWindowDimensions,
6
+ type ViewStyle,
7
+ } from 'react-native';
8
+ import Animated, {
9
+ cancelAnimation,
10
+ runOnJS,
11
+ type SharedValue,
12
+ useAnimatedReaction,
13
+ useAnimatedStyle,
14
+ useSharedValue,
15
+ withTiming,
16
+ } from 'react-native-reanimated';
17
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
18
+ import { clamp } from './utils/clamp';
19
+ import * as Haptics from 'expo-haptics';
20
+
21
+ export type PinchToZoomProps = PropsWithChildren<{
22
+ minimumZoomScale?: number;
23
+ maximumZoomScale?: number;
24
+ style?: StyleProp<ViewStyle>;
25
+ onPinchStart?: () => void;
26
+ onPinchEnd?: () => void;
27
+ disabled?: boolean;
28
+ onLayout?: (e: LayoutChangeEvent) => void;
29
+ onScaleChange?: () => void;
30
+ onScaleReset?: () => void;
31
+ onTranslationChange?: (
32
+ x: SharedValue<number>,
33
+ y: SharedValue<number>,
34
+ scale: SharedValue<number>
35
+ ) => void;
36
+ onRequestClose?: () => void;
37
+ }>;
38
+
39
+ export default function PinchToZoom({
40
+ minimumZoomScale = 1,
41
+ maximumZoomScale = 8,
42
+ style: propStyle,
43
+ onPinchStart,
44
+ onPinchEnd,
45
+ disabled,
46
+ onLayout,
47
+ onTranslationChange,
48
+ onScaleChange,
49
+ onScaleReset,
50
+ children,
51
+ onRequestClose,
52
+ }: PinchToZoomProps) {
53
+ const { height: windowHeight } = useWindowDimensions();
54
+
55
+ const translationX = useSharedValue(0);
56
+ const translationY = useSharedValue(0);
57
+ const originX = useSharedValue(0);
58
+ const originY = useSharedValue(0);
59
+ const scale = useSharedValue(1);
60
+ const isPinching = useSharedValue(false);
61
+ const viewHeight = useSharedValue(0);
62
+ const viewWidth = useSharedValue(0);
63
+
64
+ const prevScale = useSharedValue(0);
65
+ const offsetScale = useSharedValue(0);
66
+ const prevTranslationX = useSharedValue(0);
67
+ const prevTranslationY = useSharedValue(0);
68
+
69
+ const pinchGesture = useMemo(
70
+ () =>
71
+ Gesture.Pinch()
72
+ .enabled(!disabled)
73
+ .onStart(() => {
74
+ cancelAnimation(translationX);
75
+ cancelAnimation(translationY);
76
+ cancelAnimation(scale);
77
+ prevScale.value = scale.value;
78
+ offsetScale.value = scale.value;
79
+ if (onPinchStart) runOnJS(onPinchStart)();
80
+ if (onScaleChange) runOnJS(onScaleChange)();
81
+ })
82
+ .onUpdate((e) => {
83
+ if (e.numberOfPointers === 2) {
84
+ scale.value = Math.min(prevScale.value * e.scale, maximumZoomScale);
85
+
86
+ // reset the origin
87
+ if (!isPinching.value) {
88
+ isPinching.value = true;
89
+ originX.value = e.focalX;
90
+ originY.value = e.focalY;
91
+ prevTranslationX.value = translationX.value;
92
+ prevTranslationY.value = translationY.value;
93
+ offsetScale.value = scale.value;
94
+ }
95
+
96
+ if (isPinching.value) {
97
+ // translate the image to the focal point as we're zooming
98
+ translationX.value = clamp(
99
+ prevTranslationX.value +
100
+ -1 *
101
+ ((scale.value - offsetScale.value) *
102
+ (originX.value - viewWidth.value / 2)),
103
+ (-viewWidth.value * (scale.value - minimumZoomScale)) / 2,
104
+ (viewWidth.value * (scale.value - minimumZoomScale)) / 2
105
+ );
106
+ translationY.value = clamp(
107
+ prevTranslationY.value +
108
+ -1 *
109
+ ((scale.value - offsetScale.value) *
110
+ (originY.value - viewHeight.value / 2)),
111
+ (-viewHeight.value * (scale.value - minimumZoomScale)) / 2,
112
+ (viewHeight.value * (scale.value - minimumZoomScale)) / 2
113
+ );
114
+ }
115
+ }
116
+ })
117
+ .onEnd(() => {
118
+ isPinching.value = false;
119
+
120
+ if (scale.value < minimumZoomScale / 2 && prevScale.value <= minimumZoomScale) {
121
+ if (onRequestClose) {
122
+ runOnJS(onRequestClose)();
123
+ }
124
+ } else if (scale.value < minimumZoomScale) {
125
+ runOnJS(Haptics.impactAsync)(Haptics.ImpactFeedbackStyle.Light);
126
+ translationX.value = withTiming(0);
127
+ translationY.value = withTiming(0);
128
+ scale.value = withTiming(minimumZoomScale);
129
+ if (onScaleReset) {
130
+ runOnJS(onScaleReset)();
131
+ }
132
+ }
133
+
134
+ prevScale.value = 0;
135
+ prevTranslationX.value = translationX.value;
136
+ prevTranslationY.value = translationY.value;
137
+
138
+ if (onPinchEnd) runOnJS(onPinchEnd)();
139
+ }),
140
+
141
+ [
142
+ disabled,
143
+ translationX,
144
+ translationY,
145
+ scale,
146
+ prevScale,
147
+ offsetScale,
148
+ onPinchStart,
149
+ onScaleChange,
150
+ maximumZoomScale,
151
+ isPinching,
152
+ originX,
153
+ originY,
154
+ prevTranslationX,
155
+ prevTranslationY,
156
+ viewWidth.value,
157
+ viewHeight.value,
158
+ minimumZoomScale,
159
+ onPinchEnd,
160
+ onRequestClose,
161
+ onScaleReset,
162
+ ]
163
+ );
164
+
165
+ const panGesture = useMemo(
166
+ () =>
167
+ Gesture.Pan()
168
+ .enabled(!disabled)
169
+ .onStart(() => {
170
+ cancelAnimation(translationX);
171
+ cancelAnimation(translationY);
172
+
173
+ prevTranslationX.value = translationX.value;
174
+ prevTranslationY.value = translationY.value;
175
+ })
176
+ .onUpdate((e) => {
177
+ if (prevScale.value <= minimumZoomScale) {
178
+ translationX.value = prevTranslationX.value + e.translationX;
179
+ translationY.value = prevTranslationY.value + e.translationY;
180
+ } else {
181
+ translationX.value = clamp(
182
+ prevTranslationX.value + e.translationX,
183
+ (-viewWidth.value * (scale.value - minimumZoomScale)) / 2,
184
+ (viewWidth.value * (scale.value - minimumZoomScale)) / 2
185
+ );
186
+ translationY.value = clamp(
187
+ prevTranslationY.value + e.translationY,
188
+ (-viewHeight.value * (scale.value - minimumZoomScale)) / 2,
189
+ (viewHeight.value * (scale.value - minimumZoomScale)) / 2
190
+ );
191
+ }
192
+ })
193
+ .onEnd(() => {
194
+ if (scale.value <= minimumZoomScale && prevScale.value <= minimumZoomScale) {
195
+ if (
196
+ Math.abs(translationX.value) > viewWidth.value / 2 ||
197
+ Math.abs(translationY.value) > viewHeight.value / 2
198
+ ) {
199
+ if (onRequestClose) {
200
+ runOnJS(onRequestClose)();
201
+ }
202
+ } else {
203
+ runOnJS(Haptics.impactAsync)(Haptics.ImpactFeedbackStyle.Light);
204
+ translationX.value = withTiming(0);
205
+ translationY.value = withTiming(0);
206
+ }
207
+ } else if (
208
+ viewHeight.value * (scale.value - minimumZoomScale) <=
209
+ windowHeight
210
+ ) {
211
+ translationX.value = withTiming(
212
+ clamp(
213
+ translationX.value,
214
+ (-viewWidth.value * (scale.value - minimumZoomScale)) / 2,
215
+ (viewWidth.value * (scale.value - minimumZoomScale)) / 2
216
+ )
217
+ );
218
+ translationY.value = withTiming(
219
+ clamp(
220
+ translationY.value,
221
+ (-viewHeight.value * (scale.value - minimumZoomScale)) / 2,
222
+ (viewHeight.value * (scale.value - minimumZoomScale)) / 2
223
+ )
224
+ );
225
+ }
226
+ }),
227
+ [
228
+ disabled,
229
+ minimumZoomScale,
230
+ onRequestClose,
231
+ prevScale.value,
232
+ prevTranslationX,
233
+ prevTranslationY,
234
+ scale.value,
235
+ windowHeight,
236
+ translationX,
237
+ translationY,
238
+ viewHeight.value,
239
+ viewWidth.value,
240
+ ]
241
+ );
242
+
243
+ const tapGesture = useMemo(
244
+ () =>
245
+ Gesture.Tap()
246
+ .enabled(!disabled)
247
+ .numberOfTaps(2)
248
+ .onStart(() => {
249
+ if (scale.value > minimumZoomScale) {
250
+ translationX.value = withTiming(0);
251
+ translationY.value = withTiming(0);
252
+ scale.value = withTiming(minimumZoomScale);
253
+ if (onScaleReset) {
254
+ runOnJS(onScaleReset)();
255
+ }
256
+ } else {
257
+ scale.value = withTiming(maximumZoomScale / 2);
258
+ if (onScaleChange) {
259
+ runOnJS(onScaleChange)();
260
+ }
261
+ }
262
+ }),
263
+ [
264
+ disabled,
265
+ maximumZoomScale,
266
+ minimumZoomScale,
267
+ onScaleChange,
268
+ onScaleReset,
269
+ scale,
270
+ translationX,
271
+ translationY,
272
+ ]
273
+ );
274
+
275
+ const compositeGesture = useMemo(() => {
276
+ return Gesture.Exclusive(Gesture.Simultaneous(pinchGesture, panGesture), tapGesture);
277
+ }, [panGesture, pinchGesture, tapGesture]);
278
+
279
+ useAnimatedReaction(
280
+ () => {
281
+ return {
282
+ scale: scale.value,
283
+ translationX: translationX.value,
284
+ translationY: translationY.value,
285
+ };
286
+ },
287
+ () => {
288
+ if (onTranslationChange) {
289
+ runOnJS(onTranslationChange)(translationX, translationY, scale);
290
+ }
291
+ },
292
+ [onTranslationChange]
293
+ );
294
+
295
+ const style = useAnimatedStyle(() => {
296
+ return {
297
+ transform: [
298
+ { translateX: translationX.value },
299
+ { translateY: translationY.value },
300
+ { scale: scale.value },
301
+ ],
302
+ };
303
+ }, []);
304
+
305
+ const internalOnLayout = useCallback(
306
+ (e: LayoutChangeEvent) => {
307
+ viewHeight.value = e.nativeEvent.layout.height;
308
+ viewWidth.value = e.nativeEvent.layout.width;
309
+ onLayout?.(e);
310
+ },
311
+ [viewHeight, viewWidth, onLayout]
312
+ );
313
+
314
+ const finalStyle = useMemo(() => [style, propStyle], [style, propStyle]);
315
+
316
+ return (
317
+ <GestureDetector gesture={compositeGesture}>
318
+ <Animated.View onLayout={internalOnLayout} style={finalStyle}>
319
+ {children}
320
+ </Animated.View>
321
+ </GestureDetector>
322
+ );
323
+ }
@@ -0,0 +1,72 @@
1
+ import React, { forwardRef, useCallback, useRef, useState } from 'react';
2
+ import { FlashList } from '@shopify/flash-list';
3
+ import mergeRefs from 'merge-refs';
4
+ import FullScreenImageSlider from './FullScreenImageSlider';
5
+ import BaseListImageSlider, {
6
+ type BaseSimpleImageSliderProps,
7
+ type SimpleImageSliderItem,
8
+ } from './BaseSimpleImageSlider';
9
+
10
+ export type SimpleImageSliderProps = BaseSimpleImageSliderProps & {
11
+ fullScreenEnabled?: boolean;
12
+ };
13
+
14
+ const SimpleImageSlider = forwardRef<FlashList<SimpleImageSliderItem>, SimpleImageSliderProps>(
15
+ function ListImageSlider(
16
+ { data, fullScreenEnabled = false, onItemPress, onViewableItemChange, ...props },
17
+ ref
18
+ ) {
19
+ const listRef = useRef<FlashList<SimpleImageSliderItem>>(null);
20
+ const fullScreenListRef = useRef<FlashList<SimpleImageSliderItem>>(null);
21
+
22
+ const [fullScreen, setFullScreen] = useState(false);
23
+ const [currentIndex, setCurrentIndex] = useState(0);
24
+
25
+ const internalOnViewableItemChange = useCallback(
26
+ (index: number) => {
27
+ setCurrentIndex(index);
28
+ onViewableItemChange?.(index);
29
+ },
30
+ [onViewableItemChange]
31
+ );
32
+
33
+ const onFullScreenViewableItemChange = useCallback((index: number) => {
34
+ setCurrentIndex(index);
35
+ }, []);
36
+
37
+ const openFullScreen = useCallback(() => {
38
+ setFullScreen(true);
39
+ }, []);
40
+
41
+ const onRequestClose = useCallback(() => {
42
+ listRef.current?.scrollToIndex({ index: currentIndex });
43
+ setFullScreen(false);
44
+ }, [currentIndex]);
45
+
46
+ return (
47
+ <>
48
+ <BaseListImageSlider
49
+ {...props}
50
+ data={data}
51
+ ref={mergeRefs(ref, listRef)}
52
+ onItemPress={onItemPress ?? openFullScreen}
53
+ onViewableItemChange={internalOnViewableItemChange}
54
+ />
55
+ {fullScreenEnabled ? (
56
+ <FullScreenImageSlider
57
+ {...props}
58
+ ref={fullScreenListRef}
59
+ open={fullScreen}
60
+ onRequestClose={onRequestClose}
61
+ data={data}
62
+ showPageCounter={false}
63
+ indexOverride={currentIndex}
64
+ onViewableItemChange={onFullScreenViewableItemChange}
65
+ />
66
+ ) : null}
67
+ </>
68
+ );
69
+ }
70
+ );
71
+
72
+ export default SimpleImageSlider;
@@ -0,0 +1,48 @@
1
+ import React, { type PropsWithChildren, useMemo } from 'react';
2
+ import { type DefaultTheme, ThemeProvider } from 'styled-components/native';
3
+
4
+ export default function StyledComponentsThemeProvider({ children }: PropsWithChildren) {
5
+ const styles = useMemo(
6
+ () => ({
7
+ spacing: {
8
+ xxs: 2,
9
+ xs: 4,
10
+ s: 8,
11
+ m: 16,
12
+ l: 20,
13
+ xl: 40,
14
+ },
15
+ borderRadius: {
16
+ xs: 2,
17
+ s: 4,
18
+ m: 8,
19
+ l: 16,
20
+ xl: 24,
21
+ },
22
+ borderWidth: {
23
+ xs: 1,
24
+ s: 2,
25
+ m: 4,
26
+ l: 8,
27
+ xl: 16,
28
+ },
29
+ }),
30
+
31
+ []
32
+ );
33
+
34
+ const theme: DefaultTheme = useMemo(
35
+ () => ({
36
+ colors: {
37
+ pageCounterBackground: '#D3D3D3',
38
+ pageCounterBorder: '#000000',
39
+ fullScreenCloseButton: '#FFFFFF',
40
+ descriptionContainerBorder: '#FFFFFF',
41
+ },
42
+ styles,
43
+ }),
44
+ [styles]
45
+ );
46
+
47
+ return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
48
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,14 @@
1
+ import StyledComponentsThemeProvider from './StyledComponentsThemeProvider';
2
+ import BaseListImageSlider, { type BaseSimpleImageSliderProps } from './BaseSimpleImageSlider';
3
+ import SimpleImageSlider, { type SimpleImageSliderProps } from './SimpleImageSlider';
4
+ import FullScreenImageSlider, { type FullScreenImageSliderProps } from './FullScreenImageSlider';
5
+
6
+ export {
7
+ StyledComponentsThemeProvider as SimpleImageSliderThemeProvider,
8
+ BaseListImageSlider,
9
+ type BaseSimpleImageSliderProps,
10
+ SimpleImageSlider,
11
+ type SimpleImageSliderProps,
12
+ FullScreenImageSlider,
13
+ type FullScreenImageSliderProps,
14
+ };
@@ -0,0 +1,4 @@
1
+ export const clamp = (value: number, min: number, max: number) => {
2
+ 'worklet';
3
+ return Math.min(Math.max(value, min), max);
4
+ };
@@ -0,0 +1,22 @@
1
+ import React, { isValidElement } from 'react';
2
+
3
+ export type RenderProp =
4
+ | React.ComponentType<unknown>
5
+ | React.ReactElement
6
+ | string
7
+ | undefined
8
+ | null;
9
+
10
+ export default function renderProp(Component: RenderProp) {
11
+ return Component ? (
12
+ typeof Component === 'string' ? (
13
+ Component
14
+ ) : isValidElement(Component) ? (
15
+ Component
16
+ ) : (
17
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
18
+ // @ts-ignore
19
+ <Component />
20
+ )
21
+ ) : null;
22
+ }