@gyjshow/react-native-image-viewing 1.0.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.
Files changed (35) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +101 -0
  3. package/dist/@types/index.d.ts +17 -0
  4. package/dist/@types/index.js +8 -0
  5. package/dist/ImageViewing.d.ts +33 -0
  6. package/dist/ImageViewing.js +86 -0
  7. package/dist/components/ImageDefaultHeader.d.ts +13 -0
  8. package/dist/components/ImageDefaultHeader.js +38 -0
  9. package/dist/components/ImageItem/ImageItem.android.d.ts +20 -0
  10. package/dist/components/ImageItem/ImageItem.android.js +83 -0
  11. package/dist/components/ImageItem/ImageItem.ios.d.ts +20 -0
  12. package/dist/components/ImageItem/ImageItem.ios.js +79 -0
  13. package/dist/components/ImageItem/ImageLoading.d.ts +9 -0
  14. package/dist/components/ImageItem/ImageLoading.js +30 -0
  15. package/dist/components/StatusBarManager.d.ts +5 -0
  16. package/dist/components/StatusBarManager.js +14 -0
  17. package/dist/hooks/useAnimatedComponents.d.ts +18 -0
  18. package/dist/hooks/useAnimatedComponents.js +41 -0
  19. package/dist/hooks/useDoubleTapToZoom.d.ts +16 -0
  20. package/dist/hooks/useDoubleTapToZoom.js +49 -0
  21. package/dist/hooks/useImageDimensions.d.ts +10 -0
  22. package/dist/hooks/useImageDimensions.js +69 -0
  23. package/dist/hooks/useImageIndexChange.d.ts +11 -0
  24. package/dist/hooks/useImageIndexChange.js +20 -0
  25. package/dist/hooks/useImagePrefetch.d.ts +10 -0
  26. package/dist/hooks/useImagePrefetch.js +21 -0
  27. package/dist/hooks/usePanResponder.d.ts +19 -0
  28. package/dist/hooks/usePanResponder.js +273 -0
  29. package/dist/hooks/useRequestClose.d.ts +9 -0
  30. package/dist/hooks/useRequestClose.js +20 -0
  31. package/dist/index.d.ts +8 -0
  32. package/dist/index.js +8 -0
  33. package/dist/utils.d.ts +46 -0
  34. package/dist/utils.js +99 -0
  35. package/package.json +45 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 JOB TODAY S.A.
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/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # react-native-image-viewing
2
+
3
+ > React Native modal component for viewing images as a sliding gallery.
4
+
5
+ [![npm version](https://badge.fury.io/js/react-native-image-viewing.svg)](https://badge.fury.io/js/react-native-image-viewing)
6
+ [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
7
+
8
+ - 🔥Pinch zoom for both iOS and Android
9
+ - 🔥Double tap to zoom for both iOS and Android
10
+ - 🔥Supports swipe-to-close animation
11
+ - 🔥Custom header and footer components
12
+ - 🔥Uses VirtualizedList to optimize image loading and rendering
13
+
14
+ Try with Expo: https://expo.io/@antonkalinin/react-native-image-viewing
15
+
16
+ <p align="center">
17
+ <img src="https://github.com/jobtoday/react-native-image-viewing/blob/master/demo.gif?raw=true" height="480" />
18
+ </p>
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ yarn add react-native-image-viewing
24
+ ```
25
+
26
+ or
27
+
28
+ ```bash
29
+ npm install --save react-native-image-viewing
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ```jsx
35
+ import ImageView from "react-native-image-viewing";
36
+
37
+ const images = [
38
+ {
39
+ uri: "https://images.unsplash.com/photo-1571501679680-de32f1e7aad4",
40
+ },
41
+ {
42
+ uri: "https://images.unsplash.com/photo-1573273787173-0eb81a833b34",
43
+ },
44
+ {
45
+ uri: "https://images.unsplash.com/photo-1569569970363-df7b6160d111",
46
+ },
47
+ ];
48
+
49
+ const [visible, setIsVisible] = useState(false);
50
+
51
+ <ImageView
52
+ images={images}
53
+ imageIndex={0}
54
+ visible={visible}
55
+ onRequestClose={() => setIsVisible(false)}
56
+ />
57
+ ```
58
+
59
+ #### [See Example](https://github.com/jobtoday/react-native-image-viewing/blob/master/example/App.tsx#L62-L80)
60
+
61
+ ## Props
62
+
63
+ | Prop name | Description | Type | Required |
64
+ | ------------------------ | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | -------- |
65
+ | `images` | Array of images to display | ImageSource[] | true |
66
+ | `keyExtractor` | Uniqely identifying each image | (imageSrc: ImageSource, index: number) => string | false |
67
+ | `imageIndex` | Current index of image to display | number | true |
68
+ | `visible` | Is modal shown or not | boolean | true |
69
+ | `onRequestClose` | Function called to close the modal | function | true |
70
+ | `onImageIndexChange` | Function called when image index has been changed | function | false |
71
+ | `onLongPress` | Function called when image long pressed | function (event: GestureResponderEvent, image: ImageSource) | false |
72
+ | `delayLongPress` | Delay in ms, before onLongPress is called: default `800` | number | false |
73
+ | `animationType` | Animation modal presented with: default `fade` | `none`, `fade`, `slide` | false |
74
+ | `presentationStyle` | Modal presentation style: default: `fullScreen` **Android:** Use `overFullScreen` to hide StatusBar | `fullScreen`, `pageSheet`, `formSheet`, `overFullScreen` | false |
75
+ | `backgroundColor` | Background color of the modal in HEX (#000000EE) | string | false |
76
+ | `swipeToCloseEnabled` | Close modal with swipe up or down: default `true` | boolean | false |
77
+ | `doubleTapToZoomEnabled` | Zoom image by double tap on it: default `true` | boolean | false |
78
+ | `HeaderComponent` | Header component, gets current `imageIndex` as a prop | component, function | false |
79
+ | `FooterComponent` | Footer component, gets current `imageIndex` as a prop | component, function | false |
80
+
81
+ - type ImageSource = ImageURISource | ImageRequireSource
82
+
83
+ ## Contributing
84
+
85
+ To start contributing clone this repo and then run inside `react-native-image-viewing` folder:
86
+
87
+ ```bash
88
+ yarn
89
+ ```
90
+
91
+ Then go inside `example` folder and run:
92
+
93
+ ```bash
94
+ yarn & yarn start
95
+ ```
96
+
97
+ This will start packager for expo so you can change `/src/ImageViewing` and see changes in expo example app.
98
+
99
+ ## License
100
+
101
+ [MIT](LICENSE)
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Copyright (c) JOB TODAY S.A. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+ import { ImageURISource, ImageRequireSource } from "react-native";
9
+ export type Dimensions = {
10
+ width: number;
11
+ height: number;
12
+ };
13
+ export type Position = {
14
+ x: number;
15
+ y: number;
16
+ };
17
+ export type ImageSource = ImageURISource | ImageRequireSource;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Copyright (c) JOB TODAY S.A. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+ export {};
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Copyright (c) JOB TODAY S.A. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+ import React, { ComponentType } from "react";
9
+ import { ModalProps } from "react-native";
10
+ import { ImageSource } from "./@types";
11
+ type Props = {
12
+ images: ImageSource[];
13
+ keyExtractor?: (imageSrc: ImageSource, index: number) => string;
14
+ imageIndex: number;
15
+ visible: boolean;
16
+ onRequestClose: () => void;
17
+ onLongPress?: (image: ImageSource) => void;
18
+ onImageIndexChange?: (imageIndex: number) => void;
19
+ presentationStyle?: ModalProps["presentationStyle"];
20
+ animationType?: ModalProps["animationType"];
21
+ backgroundColor?: string;
22
+ swipeToCloseEnabled?: boolean;
23
+ doubleTapToZoomEnabled?: boolean;
24
+ delayLongPress?: number;
25
+ HeaderComponent?: ComponentType<{
26
+ imageIndex: number;
27
+ }>;
28
+ FooterComponent?: ComponentType<{
29
+ imageIndex: number;
30
+ }>;
31
+ };
32
+ declare const EnhancedImageViewing: (props: Props) => React.JSX.Element;
33
+ export default EnhancedImageViewing;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Copyright (c) JOB TODAY S.A. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+ import React, { useCallback, useRef, useEffect } from "react";
9
+ import { Animated, Dimensions, StyleSheet, View, VirtualizedList, Modal, } from "react-native";
10
+ import ImageItem from "./components/ImageItem/ImageItem";
11
+ import ImageDefaultHeader from "./components/ImageDefaultHeader";
12
+ import StatusBarManager from "./components/StatusBarManager";
13
+ import useAnimatedComponents from "./hooks/useAnimatedComponents";
14
+ import useImageIndexChange from "./hooks/useImageIndexChange";
15
+ import useRequestClose from "./hooks/useRequestClose";
16
+ const DEFAULT_ANIMATION_TYPE = "fade";
17
+ const DEFAULT_BG_COLOR = "#000";
18
+ const DEFAULT_DELAY_LONG_PRESS = 800;
19
+ const SCREEN = Dimensions.get("screen");
20
+ const SCREEN_WIDTH = SCREEN.width;
21
+ function ImageViewing({ images, keyExtractor, imageIndex, visible, onRequestClose, onLongPress = () => { }, onImageIndexChange, animationType = DEFAULT_ANIMATION_TYPE, backgroundColor = DEFAULT_BG_COLOR, presentationStyle, swipeToCloseEnabled, doubleTapToZoomEnabled, delayLongPress = DEFAULT_DELAY_LONG_PRESS, HeaderComponent, FooterComponent, }) {
22
+ const imageList = useRef(null);
23
+ const [opacity, onRequestCloseEnhanced] = useRequestClose(onRequestClose);
24
+ const [currentImageIndex, onScroll] = useImageIndexChange(imageIndex, SCREEN);
25
+ const [headerTransform, footerTransform, toggleBarsVisible] = useAnimatedComponents();
26
+ useEffect(() => {
27
+ if (onImageIndexChange) {
28
+ onImageIndexChange(currentImageIndex);
29
+ }
30
+ }, [currentImageIndex]);
31
+ const onZoom = useCallback((isScaled) => {
32
+ var _a;
33
+ // @ts-ignore
34
+ (_a = imageList === null || imageList === void 0 ? void 0 : imageList.current) === null || _a === void 0 ? void 0 : _a.setNativeProps({ scrollEnabled: !isScaled });
35
+ toggleBarsVisible(!isScaled);
36
+ }, [imageList]);
37
+ if (!visible) {
38
+ return null;
39
+ }
40
+ return (<Modal transparent={presentationStyle === "overFullScreen"} visible={visible} presentationStyle={presentationStyle} animationType={animationType} onRequestClose={onRequestCloseEnhanced} supportedOrientations={["portrait"]} hardwareAccelerated>
41
+ <StatusBarManager presentationStyle={presentationStyle}/>
42
+ <View style={[styles.container, { opacity, backgroundColor }]}>
43
+ <Animated.View style={[styles.header, { transform: headerTransform }]}>
44
+ {typeof HeaderComponent !== "undefined" ? (React.createElement(HeaderComponent, {
45
+ imageIndex: currentImageIndex,
46
+ })) : (<ImageDefaultHeader onRequestClose={onRequestCloseEnhanced}/>)}
47
+ </Animated.View>
48
+ <VirtualizedList ref={imageList} data={images} horizontal pagingEnabled windowSize={2} initialNumToRender={1} maxToRenderPerBatch={1} showsHorizontalScrollIndicator={false} showsVerticalScrollIndicator={false} initialScrollIndex={imageIndex} getItem={(_, index) => images[index]} getItemCount={() => images.length} getItemLayout={(_, index) => ({
49
+ length: SCREEN_WIDTH,
50
+ offset: SCREEN_WIDTH * index,
51
+ index,
52
+ })} renderItem={({ item: imageSrc }) => (<ImageItem onZoom={onZoom} imageSrc={imageSrc} onRequestClose={onRequestCloseEnhanced} onLongPress={onLongPress} delayLongPress={delayLongPress} swipeToCloseEnabled={swipeToCloseEnabled} doubleTapToZoomEnabled={doubleTapToZoomEnabled}/>)} onMomentumScrollEnd={onScroll}
53
+ //@ts-ignore
54
+ keyExtractor={(imageSrc, index) => keyExtractor
55
+ ? keyExtractor(imageSrc, index)
56
+ : typeof imageSrc === "number"
57
+ ? `${imageSrc}`
58
+ : imageSrc.uri}/>
59
+ {typeof FooterComponent !== "undefined" && (<Animated.View style={[styles.footer, { transform: footerTransform }]}>
60
+ {React.createElement(FooterComponent, {
61
+ imageIndex: currentImageIndex,
62
+ })}
63
+ </Animated.View>)}
64
+ </View>
65
+ </Modal>);
66
+ }
67
+ const styles = StyleSheet.create({
68
+ container: {
69
+ flex: 1,
70
+ backgroundColor: "#000",
71
+ },
72
+ header: {
73
+ position: "absolute",
74
+ width: "100%",
75
+ zIndex: 1,
76
+ top: 0,
77
+ },
78
+ footer: {
79
+ position: "absolute",
80
+ width: "100%",
81
+ zIndex: 1,
82
+ bottom: 0,
83
+ },
84
+ });
85
+ const EnhancedImageViewing = (props) => (<ImageViewing key={props.imageIndex} {...props}/>);
86
+ export default EnhancedImageViewing;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Copyright (c) JOB TODAY S.A. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+ import React from "react";
9
+ type Props = {
10
+ onRequestClose: () => void;
11
+ };
12
+ declare const ImageDefaultHeader: ({ onRequestClose }: Props) => React.JSX.Element;
13
+ export default ImageDefaultHeader;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Copyright (c) JOB TODAY S.A. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+ import React from "react";
9
+ import { SafeAreaView, Text, TouchableOpacity, StyleSheet } from "react-native";
10
+ const HIT_SLOP = { top: 16, left: 16, bottom: 16, right: 16 };
11
+ const ImageDefaultHeader = ({ onRequestClose }) => (<SafeAreaView style={styles.root}>
12
+ <TouchableOpacity style={styles.closeButton} onPress={onRequestClose} hitSlop={HIT_SLOP}>
13
+ <Text style={styles.closeText}>✕</Text>
14
+ </TouchableOpacity>
15
+ </SafeAreaView>);
16
+ const styles = StyleSheet.create({
17
+ root: {
18
+ alignItems: "flex-end",
19
+ },
20
+ closeButton: {
21
+ marginRight: 8,
22
+ marginTop: 8,
23
+ width: 44,
24
+ height: 44,
25
+ alignItems: "center",
26
+ justifyContent: "center",
27
+ borderRadius: 22,
28
+ backgroundColor: "#00000077",
29
+ },
30
+ closeText: {
31
+ lineHeight: 22,
32
+ fontSize: 19,
33
+ textAlign: "center",
34
+ color: "#FFF",
35
+ includeFontPadding: false,
36
+ },
37
+ });
38
+ export default ImageDefaultHeader;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Copyright (c) JOB TODAY S.A. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+ import React from "react";
9
+ import { ImageSource } from "../../@types";
10
+ type Props = {
11
+ imageSrc: ImageSource;
12
+ onRequestClose: () => void;
13
+ onZoom: (isZoomed: boolean) => void;
14
+ onLongPress: (image: ImageSource) => void;
15
+ delayLongPress: number;
16
+ swipeToCloseEnabled?: boolean;
17
+ doubleTapToZoomEnabled?: boolean;
18
+ };
19
+ declare const _default: React.MemoExoticComponent<({ imageSrc, onZoom, onRequestClose, onLongPress, delayLongPress, swipeToCloseEnabled, doubleTapToZoomEnabled, }: Props) => React.JSX.Element>;
20
+ export default _default;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Copyright (c) JOB TODAY S.A. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+ import React, { useCallback, useRef, useState } from "react";
9
+ import { Animated, ScrollView, Dimensions, StyleSheet, } from "react-native";
10
+ import useImageDimensions from "../../hooks/useImageDimensions";
11
+ import usePanResponder from "../../hooks/usePanResponder";
12
+ import { getImageStyles, getImageTransform } from "../../utils";
13
+ import { ImageLoading } from "./ImageLoading";
14
+ const SWIPE_CLOSE_OFFSET = 75;
15
+ const SWIPE_CLOSE_VELOCITY = 1.75;
16
+ const SCREEN = Dimensions.get("window");
17
+ const SCREEN_WIDTH = SCREEN.width;
18
+ const SCREEN_HEIGHT = SCREEN.height;
19
+ const ImageItem = ({ imageSrc, onZoom, onRequestClose, onLongPress, delayLongPress, swipeToCloseEnabled = true, doubleTapToZoomEnabled = true, }) => {
20
+ const imageContainer = useRef(null);
21
+ const imageDimensions = useImageDimensions(imageSrc);
22
+ const [translate, scale] = getImageTransform(imageDimensions, SCREEN);
23
+ const scrollValueY = new Animated.Value(0);
24
+ const [isLoaded, setLoadEnd] = useState(false);
25
+ const onLoaded = useCallback(() => setLoadEnd(true), []);
26
+ const onZoomPerformed = useCallback((isZoomed) => {
27
+ onZoom(isZoomed);
28
+ if (imageContainer === null || imageContainer === void 0 ? void 0 : imageContainer.current) {
29
+ imageContainer.current.setNativeProps({
30
+ scrollEnabled: !isZoomed,
31
+ });
32
+ }
33
+ }, [imageContainer]);
34
+ const onLongPressHandler = useCallback(() => {
35
+ onLongPress(imageSrc);
36
+ }, [imageSrc, onLongPress]);
37
+ const [panHandlers, scaleValue, translateValue] = usePanResponder({
38
+ initialScale: scale || 1,
39
+ initialTranslate: translate || { x: 0, y: 0 },
40
+ onZoom: onZoomPerformed,
41
+ doubleTapToZoomEnabled,
42
+ onLongPress: onLongPressHandler,
43
+ delayLongPress,
44
+ });
45
+ const imagesStyles = getImageStyles(imageDimensions, translateValue, scaleValue);
46
+ const imageOpacity = scrollValueY.interpolate({
47
+ inputRange: [-SWIPE_CLOSE_OFFSET, 0, SWIPE_CLOSE_OFFSET],
48
+ outputRange: [0.7, 1, 0.7],
49
+ });
50
+ const imageStylesWithOpacity = { ...imagesStyles, opacity: imageOpacity };
51
+ const onScrollEndDrag = ({ nativeEvent, }) => {
52
+ var _a, _b, _c, _d;
53
+ const velocityY = (_b = (_a = nativeEvent === null || nativeEvent === void 0 ? void 0 : nativeEvent.velocity) === null || _a === void 0 ? void 0 : _a.y) !== null && _b !== void 0 ? _b : 0;
54
+ const offsetY = (_d = (_c = nativeEvent === null || nativeEvent === void 0 ? void 0 : nativeEvent.contentOffset) === null || _c === void 0 ? void 0 : _c.y) !== null && _d !== void 0 ? _d : 0;
55
+ if ((Math.abs(velocityY) > SWIPE_CLOSE_VELOCITY &&
56
+ offsetY > SWIPE_CLOSE_OFFSET) ||
57
+ offsetY > SCREEN_HEIGHT / 2) {
58
+ onRequestClose();
59
+ }
60
+ };
61
+ const onScroll = ({ nativeEvent, }) => {
62
+ var _a, _b;
63
+ const offsetY = (_b = (_a = nativeEvent === null || nativeEvent === void 0 ? void 0 : nativeEvent.contentOffset) === null || _a === void 0 ? void 0 : _a.y) !== null && _b !== void 0 ? _b : 0;
64
+ scrollValueY.setValue(offsetY);
65
+ };
66
+ return (<ScrollView ref={imageContainer} style={styles.listItem} pagingEnabled nestedScrollEnabled showsHorizontalScrollIndicator={false} showsVerticalScrollIndicator={false} contentContainerStyle={styles.imageScrollContainer} scrollEnabled={swipeToCloseEnabled} {...(swipeToCloseEnabled && {
67
+ onScroll,
68
+ onScrollEndDrag,
69
+ })}>
70
+ <Animated.Image {...panHandlers} source={imageSrc} style={imageStylesWithOpacity} onLoad={onLoaded}/>
71
+ {(!isLoaded || !imageDimensions) && <ImageLoading />}
72
+ </ScrollView>);
73
+ };
74
+ const styles = StyleSheet.create({
75
+ listItem: {
76
+ width: SCREEN_WIDTH,
77
+ height: SCREEN_HEIGHT,
78
+ },
79
+ imageScrollContainer: {
80
+ height: SCREEN_HEIGHT * 2,
81
+ },
82
+ });
83
+ export default React.memo(ImageItem);
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Copyright (c) JOB TODAY S.A. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+ import React from "react";
9
+ import { ImageSource } from "../../@types";
10
+ type Props = {
11
+ imageSrc: ImageSource;
12
+ onRequestClose: () => void;
13
+ onZoom: (scaled: boolean) => void;
14
+ onLongPress: (image: ImageSource) => void;
15
+ delayLongPress: number;
16
+ swipeToCloseEnabled?: boolean;
17
+ doubleTapToZoomEnabled?: boolean;
18
+ };
19
+ declare const _default: React.MemoExoticComponent<({ imageSrc, onZoom, onRequestClose, onLongPress, delayLongPress, swipeToCloseEnabled, doubleTapToZoomEnabled, }: Props) => React.JSX.Element>;
20
+ export default _default;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Copyright (c) JOB TODAY S.A. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+ import React, { useCallback, useRef, useState } from "react";
9
+ import { Animated, Dimensions, ScrollView, StyleSheet, View, TouchableWithoutFeedback, } from "react-native";
10
+ import useDoubleTapToZoom from "../../hooks/useDoubleTapToZoom";
11
+ import useImageDimensions from "../../hooks/useImageDimensions";
12
+ import { getImageStyles, getImageTransform } from "../../utils";
13
+ import { ImageLoading } from "./ImageLoading";
14
+ const SWIPE_CLOSE_OFFSET = 75;
15
+ const SWIPE_CLOSE_VELOCITY = 1.55;
16
+ const SCREEN = Dimensions.get("screen");
17
+ const SCREEN_WIDTH = SCREEN.width;
18
+ const SCREEN_HEIGHT = SCREEN.height;
19
+ const ImageItem = ({ imageSrc, onZoom, onRequestClose, onLongPress, delayLongPress, swipeToCloseEnabled = true, doubleTapToZoomEnabled = true, }) => {
20
+ const scrollViewRef = useRef(null);
21
+ const [loaded, setLoaded] = useState(false);
22
+ const [scaled, setScaled] = useState(false);
23
+ const imageDimensions = useImageDimensions(imageSrc);
24
+ const handleDoubleTap = useDoubleTapToZoom(scrollViewRef, scaled, SCREEN);
25
+ const [translate, scale] = getImageTransform(imageDimensions, SCREEN);
26
+ const scrollValueY = new Animated.Value(0);
27
+ const scaleValue = new Animated.Value(scale || 1);
28
+ const translateValue = new Animated.ValueXY(translate);
29
+ const maxScale = scale && scale > 0 ? Math.max(1 / scale, 1) : 1;
30
+ const imageOpacity = scrollValueY.interpolate({
31
+ inputRange: [-SWIPE_CLOSE_OFFSET, 0, SWIPE_CLOSE_OFFSET],
32
+ outputRange: [0.5, 1, 0.5],
33
+ });
34
+ const imagesStyles = getImageStyles(imageDimensions, translateValue, scaleValue);
35
+ const imageStylesWithOpacity = { ...imagesStyles, opacity: imageOpacity };
36
+ const onScrollEndDrag = useCallback(({ nativeEvent }) => {
37
+ var _a, _b;
38
+ const velocityY = (_b = (_a = nativeEvent === null || nativeEvent === void 0 ? void 0 : nativeEvent.velocity) === null || _a === void 0 ? void 0 : _a.y) !== null && _b !== void 0 ? _b : 0;
39
+ const scaled = (nativeEvent === null || nativeEvent === void 0 ? void 0 : nativeEvent.zoomScale) > 1;
40
+ onZoom(scaled);
41
+ setScaled(scaled);
42
+ if (!scaled &&
43
+ swipeToCloseEnabled &&
44
+ Math.abs(velocityY) > SWIPE_CLOSE_VELOCITY) {
45
+ onRequestClose();
46
+ }
47
+ }, [scaled]);
48
+ const onScroll = ({ nativeEvent, }) => {
49
+ var _a, _b;
50
+ const offsetY = (_b = (_a = nativeEvent === null || nativeEvent === void 0 ? void 0 : nativeEvent.contentOffset) === null || _a === void 0 ? void 0 : _a.y) !== null && _b !== void 0 ? _b : 0;
51
+ if ((nativeEvent === null || nativeEvent === void 0 ? void 0 : nativeEvent.zoomScale) > 1) {
52
+ return;
53
+ }
54
+ scrollValueY.setValue(offsetY);
55
+ };
56
+ const onLongPressHandler = useCallback((event) => {
57
+ onLongPress(imageSrc);
58
+ }, [imageSrc, onLongPress]);
59
+ return (<View>
60
+ <ScrollView ref={scrollViewRef} style={styles.listItem} pinchGestureEnabled showsHorizontalScrollIndicator={false} showsVerticalScrollIndicator={false} maximumZoomScale={maxScale} contentContainerStyle={styles.imageScrollContainer} scrollEnabled={swipeToCloseEnabled} onScrollEndDrag={onScrollEndDrag} scrollEventThrottle={1} {...(swipeToCloseEnabled && {
61
+ onScroll,
62
+ })}>
63
+ {(!loaded || !imageDimensions) && <ImageLoading />}
64
+ <TouchableWithoutFeedback onPress={doubleTapToZoomEnabled ? handleDoubleTap : undefined} onLongPress={onLongPressHandler} delayLongPress={delayLongPress}>
65
+ <Animated.Image source={imageSrc} style={imageStylesWithOpacity} onLoad={() => setLoaded(true)}/>
66
+ </TouchableWithoutFeedback>
67
+ </ScrollView>
68
+ </View>);
69
+ };
70
+ const styles = StyleSheet.create({
71
+ listItem: {
72
+ width: SCREEN_WIDTH,
73
+ height: SCREEN_HEIGHT,
74
+ },
75
+ imageScrollContainer: {
76
+ height: SCREEN_HEIGHT,
77
+ },
78
+ });
79
+ export default React.memo(ImageItem);
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Copyright (c) JOB TODAY S.A. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+ import React from "react";
9
+ export declare const ImageLoading: () => React.JSX.Element;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Copyright (c) JOB TODAY S.A. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+ import React from "react";
9
+ import { ActivityIndicator, Dimensions, StyleSheet, View } from "react-native";
10
+ const SCREEN = Dimensions.get("screen");
11
+ const SCREEN_WIDTH = SCREEN.width;
12
+ const SCREEN_HEIGHT = SCREEN.height;
13
+ export const ImageLoading = () => (<View style={styles.loading}>
14
+ <ActivityIndicator size="small" color="#FFF"/>
15
+ </View>);
16
+ const styles = StyleSheet.create({
17
+ listItem: {
18
+ width: SCREEN_WIDTH,
19
+ height: SCREEN_HEIGHT,
20
+ },
21
+ loading: {
22
+ width: SCREEN_WIDTH,
23
+ height: SCREEN_HEIGHT,
24
+ alignItems: "center",
25
+ justifyContent: "center",
26
+ },
27
+ imageScrollContainer: {
28
+ height: SCREEN_HEIGHT,
29
+ },
30
+ });
@@ -0,0 +1,5 @@
1
+ import { ModalProps } from "react-native";
2
+ declare const StatusBarManager: ({ presentationStyle, }: {
3
+ presentationStyle?: ModalProps["presentationStyle"];
4
+ }) => null;
5
+ export default StatusBarManager;
@@ -0,0 +1,14 @@
1
+ import { useEffect } from "react";
2
+ import { Platform, StatusBar, } from "react-native";
3
+ const StatusBarManager = ({ presentationStyle, }) => {
4
+ if (Platform.OS === "ios" || presentationStyle !== "overFullScreen") {
5
+ return null;
6
+ }
7
+ //Can't get an actual state of app status bar with default RN. Gonna rely on "presentationStyle === overFullScreen" prop and guess application status bar state to be visible in this case.
8
+ StatusBar.setHidden(true);
9
+ useEffect(() => {
10
+ return () => StatusBar.setHidden(false);
11
+ }, []);
12
+ return null;
13
+ };
14
+ export default StatusBarManager;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Copyright (c) JOB TODAY S.A. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+ import { Animated } from "react-native";
9
+ declare const useAnimatedComponents: () => readonly [[{
10
+ translateX: Animated.AnimatedValue;
11
+ }, {
12
+ translateY: Animated.AnimatedValue;
13
+ }], [{
14
+ translateX: Animated.AnimatedValue;
15
+ }, {
16
+ translateY: Animated.AnimatedValue;
17
+ }], (isVisible: boolean) => void];
18
+ export default useAnimatedComponents;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Copyright (c) JOB TODAY S.A. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+ import { Animated } from "react-native";
9
+ const INITIAL_POSITION = { x: 0, y: 0 };
10
+ const ANIMATION_CONFIG = {
11
+ duration: 200,
12
+ useNativeDriver: true,
13
+ };
14
+ const useAnimatedComponents = () => {
15
+ const headerTranslate = new Animated.ValueXY(INITIAL_POSITION);
16
+ const footerTranslate = new Animated.ValueXY(INITIAL_POSITION);
17
+ const toggleVisible = (isVisible) => {
18
+ if (isVisible) {
19
+ Animated.parallel([
20
+ Animated.timing(headerTranslate.y, { ...ANIMATION_CONFIG, toValue: 0 }),
21
+ Animated.timing(footerTranslate.y, { ...ANIMATION_CONFIG, toValue: 0 }),
22
+ ]).start();
23
+ }
24
+ else {
25
+ Animated.parallel([
26
+ Animated.timing(headerTranslate.y, {
27
+ ...ANIMATION_CONFIG,
28
+ toValue: -300,
29
+ }),
30
+ Animated.timing(footerTranslate.y, {
31
+ ...ANIMATION_CONFIG,
32
+ toValue: 300,
33
+ }),
34
+ ]).start();
35
+ }
36
+ };
37
+ const headerTransform = headerTranslate.getTranslateTransform();
38
+ const footerTransform = footerTranslate.getTranslateTransform();
39
+ return [headerTransform, footerTransform, toggleVisible];
40
+ };
41
+ export default useAnimatedComponents;