mirror of
https://github.com/status-im/react-native-image-viewing.git
synced 2026-08-31 03:41:10 +00:00
compile
This commit is contained in:
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import { ImageURISource, ImageRequireSource } from "react-native";
|
||||
export declare type Dimensions = {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
export declare type Position = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
export declare type ImageSource = ImageURISource | ImageRequireSource;
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
Vendored
+35
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import { ComponentType } from "react";
|
||||
import { ModalProps } from "react-native";
|
||||
import { ImageSource } from "./@types";
|
||||
declare type Props = {
|
||||
images: ImageSource[];
|
||||
keyExtractor?: (imageSrc: ImageSource, index: number) => string;
|
||||
imageIndex: number;
|
||||
visible: boolean;
|
||||
onRequestClose: () => void;
|
||||
onLongPress?: (image: ImageSource) => void;
|
||||
onImageIndexChange?: (imageIndex: number) => void;
|
||||
presentationStyle?: ModalProps["presentationStyle"];
|
||||
animationType?: ModalProps["animationType"];
|
||||
backgroundColor?: string;
|
||||
swipeToCloseEnabled?: boolean;
|
||||
doubleTapToZoomEnabled?: boolean;
|
||||
hideHeaderOnZoom?: boolean;
|
||||
hideFooterOnZoom?: boolean;
|
||||
delayLongPress?: number;
|
||||
HeaderComponent?: ComponentType<{
|
||||
imageIndex: number;
|
||||
}>;
|
||||
FooterComponent?: ComponentType<{
|
||||
imageIndex: number;
|
||||
}>;
|
||||
};
|
||||
declare const EnhancedImageViewing: (props: Props) => JSX.Element;
|
||||
export default EnhancedImageViewing;
|
||||
Vendored
+86
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import { Animated, Dimensions, StyleSheet, View, VirtualizedList, Modal, } from "react-native";
|
||||
import ImageItem from "./components/ImageItem/ImageItem";
|
||||
import ImageDefaultHeader from "./components/ImageDefaultHeader";
|
||||
import StatusBarManager from "./components/StatusBarManager";
|
||||
import useAnimatedComponents from "./hooks/useAnimatedComponents";
|
||||
import useImageIndexChange from "./hooks/useImageIndexChange";
|
||||
import useRequestClose from "./hooks/useRequestClose";
|
||||
const DEFAULT_ANIMATION_TYPE = "fade";
|
||||
const DEFAULT_BG_COLOR = "#000";
|
||||
const DEFAULT_DELAY_LONG_PRESS = 800;
|
||||
const SCREEN = Dimensions.get("screen");
|
||||
const SCREEN_WIDTH = SCREEN.width;
|
||||
const DEFAULT_HIDE_HEADER_ON_ZOOM = true;
|
||||
const DEFAULT_HIDE_FOOTER_ON_ZOOM = true;
|
||||
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, hideHeaderOnZoom = DEFAULT_HIDE_HEADER_ON_ZOOM, hideFooterOnZoom = DEFAULT_HIDE_FOOTER_ON_ZOOM, }) {
|
||||
const imageList = React.createRef();
|
||||
const [opacity, onRequestCloseEnhanced] = useRequestClose(onRequestClose);
|
||||
const [currentImageIndex, onScroll] = useImageIndexChange(imageIndex, SCREEN);
|
||||
const [headerTransform, footerTransform, toggleBarsVisible,] = useAnimatedComponents();
|
||||
useEffect(() => {
|
||||
if (onImageIndexChange) {
|
||||
onImageIndexChange(currentImageIndex);
|
||||
}
|
||||
}, [currentImageIndex]);
|
||||
const onZoom = useCallback((isScaled) => {
|
||||
var _a, _b;
|
||||
// @ts-ignore
|
||||
(_b = (_a = imageList) === null || _a === void 0 ? void 0 : _a.current) === null || _b === void 0 ? void 0 : _b.setNativeProps({ scrollEnabled: !isScaled });
|
||||
toggleBarsVisible(!isScaled, hideHeaderOnZoom, hideFooterOnZoom);
|
||||
}, [imageList, hideHeaderOnZoom, hideFooterOnZoom]);
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
return (<Modal transparent={presentationStyle === "overFullScreen"} visible={visible} presentationStyle={presentationStyle} animationType={animationType} onRequestClose={onRequestCloseEnhanced} supportedOrientations={["portrait"]} hardwareAccelerated>
|
||||
<StatusBarManager presentationStyle={presentationStyle}/>
|
||||
<View style={[styles.container, { opacity, backgroundColor }]}>
|
||||
<Animated.View style={[styles.header, { transform: headerTransform }]}>
|
||||
{typeof HeaderComponent !== "undefined"
|
||||
? (React.createElement(HeaderComponent, {
|
||||
imageIndex: currentImageIndex,
|
||||
}))
|
||||
: (<ImageDefaultHeader onRequestClose={onRequestCloseEnhanced}/>)}
|
||||
</Animated.View>
|
||||
<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) => ({
|
||||
length: SCREEN_WIDTH,
|
||||
offset: SCREEN_WIDTH * index,
|
||||
index,
|
||||
})} renderItem={({ item: imageSrc }) => (<ImageItem onZoom={onZoom} imageSrc={imageSrc} onRequestClose={onRequestCloseEnhanced} onLongPress={onLongPress} delayLongPress={delayLongPress} swipeToCloseEnabled={swipeToCloseEnabled} doubleTapToZoomEnabled={doubleTapToZoomEnabled}/>)} onMomentumScrollEnd={onScroll}
|
||||
//@ts-ignore
|
||||
keyExtractor={(imageSrc, index) => keyExtractor ? keyExtractor(imageSrc, index) : imageSrc.uri || `${imageSrc}`}/>
|
||||
{typeof FooterComponent !== "undefined" && (<Animated.View style={[styles.footer, { transform: footerTransform }]}>
|
||||
{React.createElement(FooterComponent, {
|
||||
imageIndex: currentImageIndex,
|
||||
})}
|
||||
</Animated.View>)}
|
||||
</View>
|
||||
</Modal>);
|
||||
}
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "#000",
|
||||
},
|
||||
header: {
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
zIndex: 1,
|
||||
top: 0,
|
||||
},
|
||||
footer: {
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
zIndex: 1,
|
||||
bottom: 0,
|
||||
},
|
||||
});
|
||||
const EnhancedImageViewing = (props) => (<ImageViewing key={props.imageIndex} {...props}/>);
|
||||
export default EnhancedImageViewing;
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
/// <reference types="react" />
|
||||
declare type Props = {
|
||||
onRequestClose: () => void;
|
||||
};
|
||||
declare const ImageDefaultHeader: ({ onRequestClose }: Props) => JSX.Element;
|
||||
export default ImageDefaultHeader;
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import React from "react";
|
||||
import { SafeAreaView, Text, TouchableOpacity, StyleSheet } from "react-native";
|
||||
const HIT_SLOP = { top: 16, left: 16, bottom: 16, right: 16 };
|
||||
const ImageDefaultHeader = ({ onRequestClose }) => (<SafeAreaView style={styles.root}>
|
||||
<TouchableOpacity style={styles.closeButton} onPress={onRequestClose} hitSlop={HIT_SLOP}>
|
||||
<Text style={styles.closeText}>✕</Text>
|
||||
</TouchableOpacity>
|
||||
</SafeAreaView>);
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
alignItems: "flex-end",
|
||||
},
|
||||
closeButton: {
|
||||
marginRight: 8,
|
||||
marginTop: 8,
|
||||
width: 45,
|
||||
height: 45,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: 22.5,
|
||||
backgroundColor: "#00000077",
|
||||
},
|
||||
closeText: {
|
||||
lineHeight: 25,
|
||||
fontSize: 25,
|
||||
paddingTop: 2,
|
||||
textAlign: "center",
|
||||
color: "#FFF",
|
||||
includeFontPadding: false,
|
||||
},
|
||||
});
|
||||
export default ImageDefaultHeader;
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import React from "react";
|
||||
import { ImageSource } from "../../@types";
|
||||
declare type Props = {
|
||||
imageSrc: ImageSource;
|
||||
onRequestClose: () => void;
|
||||
onZoom: (isZoomed: boolean) => void;
|
||||
onLongPress: (image: ImageSource) => void;
|
||||
delayLongPress: number;
|
||||
swipeToCloseEnabled?: boolean;
|
||||
doubleTapToZoomEnabled?: boolean;
|
||||
};
|
||||
declare const _default: React.MemoExoticComponent<({ imageSrc, onZoom, onRequestClose, onLongPress, delayLongPress, swipeToCloseEnabled, doubleTapToZoomEnabled, }: Props) => JSX.Element>;
|
||||
export default _default;
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import React, { useState, useCallback } from "react";
|
||||
import { Animated, Dimensions, StyleSheet, } from "react-native";
|
||||
import useImageDimensions from "../../hooks/useImageDimensions";
|
||||
import usePanResponder from "../../hooks/usePanResponder";
|
||||
import { getImageStyles, getImageTransform } from "../../utils";
|
||||
import { ImageLoading } from "./ImageLoading";
|
||||
const SWIPE_CLOSE_OFFSET = 75;
|
||||
const SWIPE_CLOSE_VELOCITY = 1.75;
|
||||
const SCREEN = Dimensions.get("window");
|
||||
const SCREEN_WIDTH = SCREEN.width;
|
||||
const SCREEN_HEIGHT = SCREEN.height;
|
||||
const ImageItem = ({ imageSrc, onZoom, onRequestClose, onLongPress, delayLongPress, swipeToCloseEnabled = true, doubleTapToZoomEnabled = true, }) => {
|
||||
const imageContainer = React.createRef();
|
||||
const imageDimensions = useImageDimensions(imageSrc);
|
||||
const [translate, scale] = getImageTransform(imageDimensions, SCREEN);
|
||||
const scrollValueY = new Animated.Value(0);
|
||||
const [isLoaded, setLoadEnd] = useState(false);
|
||||
const onLoaded = useCallback(() => setLoadEnd(true), []);
|
||||
const onZoomPerformed = (isZoomed) => {
|
||||
var _a;
|
||||
onZoom(isZoomed);
|
||||
if ((_a = imageContainer) === null || _a === void 0 ? void 0 : _a.current) {
|
||||
// @ts-ignore
|
||||
imageContainer.current.setNativeProps({
|
||||
scrollEnabled: !isZoomed,
|
||||
});
|
||||
}
|
||||
};
|
||||
const onLongPressHandler = useCallback(() => {
|
||||
onLongPress(imageSrc);
|
||||
}, [imageSrc, onLongPress]);
|
||||
const [panHandlers, scaleValue, translateValue] = usePanResponder({
|
||||
initialScale: scale || 1,
|
||||
initialTranslate: translate || { x: 0, y: 0 },
|
||||
onZoom: onZoomPerformed,
|
||||
doubleTapToZoomEnabled,
|
||||
onLongPress: onLongPressHandler,
|
||||
delayLongPress,
|
||||
});
|
||||
const imagesStyles = getImageStyles(imageDimensions, translateValue, scaleValue);
|
||||
const imageOpacity = scrollValueY.interpolate({
|
||||
inputRange: [-SWIPE_CLOSE_OFFSET, 0, SWIPE_CLOSE_OFFSET],
|
||||
outputRange: [0.7, 1, 0.7],
|
||||
});
|
||||
const imageStylesWithOpacity = { ...imagesStyles, opacity: imageOpacity };
|
||||
const onScrollEndDrag = ({ nativeEvent, }) => {
|
||||
var _a, _b, _c, _d, _e, _f;
|
||||
const velocityY = (_c = (_b = (_a = nativeEvent) === null || _a === void 0 ? void 0 : _a.velocity) === null || _b === void 0 ? void 0 : _b.y, (_c !== null && _c !== void 0 ? _c : 0));
|
||||
const offsetY = (_f = (_e = (_d = nativeEvent) === null || _d === void 0 ? void 0 : _d.contentOffset) === null || _e === void 0 ? void 0 : _e.y, (_f !== null && _f !== void 0 ? _f : 0));
|
||||
if ((Math.abs(velocityY) > SWIPE_CLOSE_VELOCITY &&
|
||||
offsetY > SWIPE_CLOSE_OFFSET) ||
|
||||
offsetY > SCREEN_HEIGHT / 2) {
|
||||
onRequestClose();
|
||||
}
|
||||
};
|
||||
const onScroll = ({ nativeEvent, }) => {
|
||||
var _a, _b, _c;
|
||||
const offsetY = (_c = (_b = (_a = nativeEvent) === null || _a === void 0 ? void 0 : _a.contentOffset) === null || _b === void 0 ? void 0 : _b.y, (_c !== null && _c !== void 0 ? _c : 0));
|
||||
scrollValueY.setValue(offsetY);
|
||||
};
|
||||
return (<Animated.ScrollView ref={imageContainer} style={styles.listItem} pagingEnabled nestedScrollEnabled showsHorizontalScrollIndicator={false} showsVerticalScrollIndicator={false} contentContainerStyle={styles.imageScrollContainer} scrollEnabled={swipeToCloseEnabled} {...(swipeToCloseEnabled && {
|
||||
onScroll,
|
||||
onScrollEndDrag,
|
||||
})}>
|
||||
<Animated.Image {...panHandlers} source={imageSrc} style={imageStylesWithOpacity} onLoad={onLoaded}/>
|
||||
{(!isLoaded || !imageDimensions) && <ImageLoading />}
|
||||
</Animated.ScrollView>);
|
||||
};
|
||||
const styles = StyleSheet.create({
|
||||
listItem: {
|
||||
width: SCREEN_WIDTH,
|
||||
height: SCREEN_HEIGHT,
|
||||
},
|
||||
imageScrollContainer: {
|
||||
height: SCREEN_HEIGHT * 2,
|
||||
},
|
||||
});
|
||||
export default React.memo(ImageItem);
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import React from "react";
|
||||
import { ImageSource } from "../../@types";
|
||||
declare type Props = {
|
||||
imageSrc: ImageSource;
|
||||
onRequestClose: () => void;
|
||||
onZoom: (scaled: boolean) => void;
|
||||
onLongPress: (image: ImageSource) => void;
|
||||
delayLongPress: number;
|
||||
swipeToCloseEnabled?: boolean;
|
||||
doubleTapToZoomEnabled?: boolean;
|
||||
};
|
||||
declare const _default: React.MemoExoticComponent<({ imageSrc, onZoom, onRequestClose, onLongPress, delayLongPress, swipeToCloseEnabled, doubleTapToZoomEnabled, }: Props) => JSX.Element>;
|
||||
export default _default;
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import React, { useCallback, useRef, useState } from "react";
|
||||
import { Animated, Dimensions, ScrollView, StyleSheet, View, TouchableWithoutFeedback, } from "react-native";
|
||||
import useDoubleTapToZoom from "../../hooks/useDoubleTapToZoom";
|
||||
import useImageDimensions from "../../hooks/useImageDimensions";
|
||||
import { getImageStyles, getImageTransform } from "../../utils";
|
||||
import { ImageLoading } from "./ImageLoading";
|
||||
const SWIPE_CLOSE_OFFSET = 75;
|
||||
const SWIPE_CLOSE_VELOCITY = 1.55;
|
||||
const SCREEN = Dimensions.get("screen");
|
||||
const SCREEN_WIDTH = SCREEN.width;
|
||||
const SCREEN_HEIGHT = SCREEN.height;
|
||||
const ImageItem = ({ imageSrc, onZoom, onRequestClose, onLongPress, delayLongPress, swipeToCloseEnabled = true, doubleTapToZoomEnabled = true, }) => {
|
||||
const scrollViewRef = useRef(null);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [scaled, setScaled] = useState(false);
|
||||
const imageDimensions = useImageDimensions(imageSrc);
|
||||
const handleDoubleTap = useDoubleTapToZoom(scrollViewRef, scaled, SCREEN);
|
||||
const [translate, scale] = getImageTransform(imageDimensions, SCREEN);
|
||||
const scrollValueY = new Animated.Value(0);
|
||||
const scaleValue = new Animated.Value(scale || 1);
|
||||
const translateValue = new Animated.ValueXY(translate);
|
||||
const maxScale = scale && scale > 0 ? Math.max(1 / scale, 1) : 1;
|
||||
const imageOpacity = scrollValueY.interpolate({
|
||||
inputRange: [-SWIPE_CLOSE_OFFSET, 0, SWIPE_CLOSE_OFFSET],
|
||||
outputRange: [0.5, 1, 0.5],
|
||||
});
|
||||
const imagesStyles = getImageStyles(imageDimensions, translateValue, scaleValue);
|
||||
const imageStylesWithOpacity = { ...imagesStyles, opacity: imageOpacity };
|
||||
const onScrollEndDrag = useCallback(({ nativeEvent }) => {
|
||||
var _a, _b, _c, _d;
|
||||
const velocityY = (_c = (_b = (_a = nativeEvent) === null || _a === void 0 ? void 0 : _a.velocity) === null || _b === void 0 ? void 0 : _b.y, (_c !== null && _c !== void 0 ? _c : 0));
|
||||
const scaled = ((_d = nativeEvent) === null || _d === void 0 ? void 0 : _d.zoomScale) > 1;
|
||||
onZoom(scaled);
|
||||
setScaled(scaled);
|
||||
if (!scaled &&
|
||||
swipeToCloseEnabled &&
|
||||
Math.abs(velocityY) > SWIPE_CLOSE_VELOCITY) {
|
||||
onRequestClose();
|
||||
}
|
||||
}, [scaled]);
|
||||
const onScroll = ({ nativeEvent, }) => {
|
||||
var _a, _b, _c, _d;
|
||||
const offsetY = (_c = (_b = (_a = nativeEvent) === null || _a === void 0 ? void 0 : _a.contentOffset) === null || _b === void 0 ? void 0 : _b.y, (_c !== null && _c !== void 0 ? _c : 0));
|
||||
if (((_d = nativeEvent) === null || _d === void 0 ? void 0 : _d.zoomScale) > 1) {
|
||||
return;
|
||||
}
|
||||
scrollValueY.setValue(offsetY);
|
||||
};
|
||||
const onLongPressHandler = useCallback((event) => {
|
||||
onLongPress(imageSrc);
|
||||
}, [imageSrc, onLongPress]);
|
||||
return (<View>
|
||||
<ScrollView ref={scrollViewRef} style={styles.listItem} pinchGestureEnabled nestedScrollEnabled={true} showsHorizontalScrollIndicator={false} showsVerticalScrollIndicator={false} maximumZoomScale={maxScale} contentContainerStyle={styles.imageScrollContainer} scrollEnabled={swipeToCloseEnabled} onScrollEndDrag={onScrollEndDrag} scrollEventThrottle={1} {...(swipeToCloseEnabled && {
|
||||
onScroll,
|
||||
})}>
|
||||
{(!loaded || !imageDimensions) && <ImageLoading />}
|
||||
<TouchableWithoutFeedback onPress={doubleTapToZoomEnabled ? handleDoubleTap : undefined} onLongPress={onLongPressHandler} delayLongPress={delayLongPress}>
|
||||
<Animated.Image source={imageSrc} style={imageStylesWithOpacity} onLoad={() => setLoaded(true)}/>
|
||||
</TouchableWithoutFeedback>
|
||||
</ScrollView>
|
||||
</View>);
|
||||
};
|
||||
const styles = StyleSheet.create({
|
||||
listItem: {
|
||||
width: SCREEN_WIDTH,
|
||||
height: SCREEN_HEIGHT,
|
||||
},
|
||||
imageScrollContainer: {
|
||||
height: SCREEN_HEIGHT,
|
||||
},
|
||||
});
|
||||
export default React.memo(ImageItem);
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
/// <reference types="react" />
|
||||
export declare const ImageLoading: () => JSX.Element;
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import React from "react";
|
||||
import { ActivityIndicator, Dimensions, StyleSheet, View } from "react-native";
|
||||
const SCREEN = Dimensions.get("screen");
|
||||
const SCREEN_WIDTH = SCREEN.width;
|
||||
const SCREEN_HEIGHT = SCREEN.height;
|
||||
export const ImageLoading = () => (<View style={styles.loading}>
|
||||
<ActivityIndicator size="small" color="#FFF"/>
|
||||
</View>);
|
||||
const styles = StyleSheet.create({
|
||||
listItem: {
|
||||
width: SCREEN_WIDTH,
|
||||
height: SCREEN_HEIGHT,
|
||||
},
|
||||
loading: {
|
||||
width: SCREEN_WIDTH,
|
||||
height: SCREEN_HEIGHT,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
imageScrollContainer: {
|
||||
height: SCREEN_HEIGHT,
|
||||
},
|
||||
});
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
declare const StatusBarManager: ({ presentationStyle, }: {
|
||||
presentationStyle?: "fullScreen" | "pageSheet" | "formSheet" | "overFullScreen" | undefined;
|
||||
}) => null;
|
||||
export default StatusBarManager;
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
import { useEffect } from "react";
|
||||
import { Platform, StatusBar, } from "react-native";
|
||||
const StatusBarManager = ({ presentationStyle, }) => {
|
||||
if (Platform.OS === "ios" || presentationStyle !== "overFullScreen") {
|
||||
return null;
|
||||
}
|
||||
//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.
|
||||
StatusBar.setHidden(true);
|
||||
useEffect(() => {
|
||||
return () => StatusBar.setHidden(false);
|
||||
}, []);
|
||||
return null;
|
||||
};
|
||||
export default StatusBarManager;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import { Animated } from "react-native";
|
||||
declare const useAnimatedComponents: () => readonly [{
|
||||
[key: string]: Animated.Value;
|
||||
}[], {
|
||||
[key: string]: Animated.Value;
|
||||
}[], (isVisible: boolean, hideHeaderOnZoom: boolean, hideFooterOnZoom: boolean) => void];
|
||||
export default useAnimatedComponents;
|
||||
Vendored
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import { Animated } from "react-native";
|
||||
const INITIAL_POSITION = { x: 0, y: 0 };
|
||||
const ANIMATION_CONFIG = {
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
};
|
||||
const useAnimatedComponents = () => {
|
||||
const headerTranslate = new Animated.ValueXY(INITIAL_POSITION);
|
||||
const footerTranslate = new Animated.ValueXY(INITIAL_POSITION);
|
||||
const toggleVisible = (isVisible, hideHeaderOnZoom, hideFooterOnZoom) => {
|
||||
if (isVisible) {
|
||||
Animated.parallel([
|
||||
Animated.timing(headerTranslate.y, { ...ANIMATION_CONFIG, toValue: 0 }),
|
||||
Animated.timing(footerTranslate.y, { ...ANIMATION_CONFIG, toValue: 0 }),
|
||||
]).start();
|
||||
}
|
||||
else {
|
||||
const hideHeaderAnimation = hideHeaderOnZoom
|
||||
? [Animated.timing(headerTranslate.y, {
|
||||
...ANIMATION_CONFIG,
|
||||
toValue: -300,
|
||||
})]
|
||||
: [];
|
||||
const hideFooterAnimation = hideFooterOnZoom
|
||||
? [Animated.timing(footerTranslate.y, {
|
||||
...ANIMATION_CONFIG,
|
||||
toValue: 300,
|
||||
})]
|
||||
: [];
|
||||
Animated.parallel([...hideHeaderAnimation, ...hideFooterAnimation]).start();
|
||||
}
|
||||
};
|
||||
const headerTransform = headerTranslate.getTranslateTransform();
|
||||
const footerTransform = footerTranslate.getTranslateTransform();
|
||||
return [headerTransform, footerTransform, toggleVisible];
|
||||
};
|
||||
export default useAnimatedComponents;
|
||||
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import React from "react";
|
||||
import { ScrollView, NativeTouchEvent, NativeSyntheticEvent } from "react-native";
|
||||
import { Dimensions } from "../@types";
|
||||
/**
|
||||
* This is iOS only.
|
||||
* Same functionality for Android implemented inside usePanResponder hook.
|
||||
*/
|
||||
declare function useDoubleTapToZoom(scrollViewRef: React.RefObject<ScrollView>, scaled: boolean, screen: Dimensions): (event: NativeSyntheticEvent<NativeTouchEvent>) => void;
|
||||
export default useDoubleTapToZoom;
|
||||
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import { useCallback } from "react";
|
||||
const DOUBLE_TAP_DELAY = 300;
|
||||
let lastTapTS = null;
|
||||
/**
|
||||
* This is iOS only.
|
||||
* Same functionality for Android implemented inside usePanResponder hook.
|
||||
*/
|
||||
function useDoubleTapToZoom(scrollViewRef, scaled, screen) {
|
||||
const handleDoubleTap = useCallback((event) => {
|
||||
var _a, _b, _c;
|
||||
const nowTS = new Date().getTime();
|
||||
const scrollResponderRef = (_b = (_a = scrollViewRef) === null || _a === void 0 ? void 0 : _a.current) === null || _b === void 0 ? void 0 : _b.getScrollResponder();
|
||||
if (lastTapTS && nowTS - lastTapTS < DOUBLE_TAP_DELAY) {
|
||||
const { pageX, pageY } = event.nativeEvent;
|
||||
let targetX = 0;
|
||||
let targetY = 0;
|
||||
let targetWidth = screen.width;
|
||||
let targetHeight = screen.height;
|
||||
// Zooming in
|
||||
// TODO: Add more precise calculation of targetX, targetY based on touch
|
||||
if (!scaled) {
|
||||
targetX = pageX / 2;
|
||||
targetY = pageY / 2;
|
||||
targetWidth = screen.width / 2;
|
||||
targetHeight = screen.height / 2;
|
||||
}
|
||||
// @ts-ignore
|
||||
(_c = scrollResponderRef) === null || _c === void 0 ? void 0 : _c.scrollResponderZoomTo({
|
||||
x: targetX,
|
||||
y: targetY,
|
||||
width: targetWidth,
|
||||
height: targetHeight,
|
||||
animated: true,
|
||||
});
|
||||
}
|
||||
else {
|
||||
lastTapTS = nowTS;
|
||||
}
|
||||
}, [scaled]);
|
||||
return handleDoubleTap;
|
||||
}
|
||||
export default useDoubleTapToZoom;
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import { Dimensions, ImageSource } from "../@types";
|
||||
declare const useImageDimensions: (image: ImageSource) => Dimensions | null;
|
||||
export default useImageDimensions;
|
||||
Vendored
+64
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import { useEffect, useState } from "react";
|
||||
import { Image } from "react-native";
|
||||
import { createCache } from "../utils";
|
||||
const CACHE_SIZE = 50;
|
||||
const imageDimensionsCache = createCache(CACHE_SIZE);
|
||||
const useImageDimensions = (image) => {
|
||||
const [dimensions, setDimensions] = useState(null);
|
||||
const getImageDimensions = (image) => {
|
||||
return new Promise((resolve) => {
|
||||
if (typeof image == "number") {
|
||||
const cacheKey = `${image}`;
|
||||
let imageDimensions = imageDimensionsCache.get(cacheKey);
|
||||
if (!imageDimensions) {
|
||||
const { width, height } = Image.resolveAssetSource(image);
|
||||
imageDimensions = { width, height };
|
||||
imageDimensionsCache.set(cacheKey, imageDimensions);
|
||||
}
|
||||
resolve(imageDimensions);
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
if (image.uri) {
|
||||
const source = image;
|
||||
const cacheKey = source.uri;
|
||||
const imageDimensions = imageDimensionsCache.get(cacheKey);
|
||||
if (imageDimensions) {
|
||||
resolve(imageDimensions);
|
||||
}
|
||||
else {
|
||||
// @ts-ignore
|
||||
Image.getSizeWithHeaders(source.uri, source.headers, (width, height) => {
|
||||
imageDimensionsCache.set(cacheKey, { width, height });
|
||||
resolve({ width, height });
|
||||
}, () => {
|
||||
resolve({ width: 0, height: 0 });
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
resolve({ width: 0, height: 0 });
|
||||
}
|
||||
});
|
||||
};
|
||||
let isImageUnmounted = false;
|
||||
useEffect(() => {
|
||||
getImageDimensions(image).then((dimensions) => {
|
||||
if (!isImageUnmounted) {
|
||||
setDimensions(dimensions);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
isImageUnmounted = true;
|
||||
};
|
||||
}, [image]);
|
||||
return dimensions;
|
||||
};
|
||||
export default useImageDimensions;
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import { NativeSyntheticEvent, NativeScrollEvent } from "react-native";
|
||||
import { Dimensions } from "../@types";
|
||||
declare const useImageIndexChange: (imageIndex: number, screen: Dimensions) => readonly [number, (event: NativeSyntheticEvent<NativeScrollEvent>) => void];
|
||||
export default useImageIndexChange;
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import { useState } from "react";
|
||||
const useImageIndexChange = (imageIndex, screen) => {
|
||||
const [currentImageIndex, setImageIndex] = useState(imageIndex);
|
||||
const onScroll = (event) => {
|
||||
const { nativeEvent: { contentOffset: { x: scrollX }, }, } = event;
|
||||
if (screen.width) {
|
||||
const nextIndex = Math.round(scrollX / screen.width);
|
||||
setImageIndex(nextIndex < 0 ? 0 : nextIndex);
|
||||
}
|
||||
};
|
||||
return [currentImageIndex, onScroll];
|
||||
};
|
||||
export default useImageIndexChange;
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import { ImageSource } from "../@types";
|
||||
declare const useImagePrefetch: (images: ImageSource[]) => void;
|
||||
export default useImagePrefetch;
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import { useEffect } from "react";
|
||||
import { Image } from "react-native";
|
||||
const useImagePrefetch = (images) => {
|
||||
useEffect(() => {
|
||||
images.forEach((image) => {
|
||||
//@ts-ignore
|
||||
if (image.uri) {
|
||||
//@ts-ignore
|
||||
return Image.prefetch(image.uri);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
};
|
||||
export default useImagePrefetch;
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import { Animated, GestureResponderHandlers } from "react-native";
|
||||
import { Position } from "../@types";
|
||||
declare type Props = {
|
||||
initialScale: number;
|
||||
initialTranslate: Position;
|
||||
onZoom: (isZoomed: boolean) => void;
|
||||
doubleTapToZoomEnabled: boolean;
|
||||
onLongPress: () => void;
|
||||
delayLongPress: number;
|
||||
};
|
||||
declare const usePanResponder: ({ initialScale, initialTranslate, onZoom, doubleTapToZoomEnabled, onLongPress, delayLongPress, }: Props) => readonly [GestureResponderHandlers, Animated.Value, Animated.ValueXY];
|
||||
export default usePanResponder;
|
||||
Vendored
+273
@@ -0,0 +1,273 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import { useMemo, useEffect } from "react";
|
||||
import { Animated, Dimensions, } from "react-native";
|
||||
import { createPanResponder, getDistanceBetweenTouches, getImageTranslate, getImageDimensionsByTranslate, } from "../utils";
|
||||
const SCREEN = Dimensions.get("window");
|
||||
const SCREEN_WIDTH = SCREEN.width;
|
||||
const SCREEN_HEIGHT = SCREEN.height;
|
||||
const MIN_DIMENSION = Math.min(SCREEN_WIDTH, SCREEN_HEIGHT);
|
||||
const SCALE_MAX = 2;
|
||||
const DOUBLE_TAP_DELAY = 300;
|
||||
const OUT_BOUND_MULTIPLIER = 0.75;
|
||||
const usePanResponder = ({ initialScale, initialTranslate, onZoom, doubleTapToZoomEnabled, onLongPress, delayLongPress, }) => {
|
||||
let numberInitialTouches = 1;
|
||||
let initialTouches = [];
|
||||
let currentScale = initialScale;
|
||||
let currentTranslate = initialTranslate;
|
||||
let tmpScale = 0;
|
||||
let tmpTranslate = null;
|
||||
let isDoubleTapPerformed = false;
|
||||
let lastTapTS = null;
|
||||
let longPressHandlerRef = null;
|
||||
const meaningfulShift = MIN_DIMENSION * 0.01;
|
||||
const scaleValue = new Animated.Value(initialScale);
|
||||
const translateValue = new Animated.ValueXY(initialTranslate);
|
||||
const imageDimensions = getImageDimensionsByTranslate(initialTranslate, SCREEN);
|
||||
const getBounds = (scale) => {
|
||||
const scaledImageDimensions = {
|
||||
width: imageDimensions.width * scale,
|
||||
height: imageDimensions.height * scale,
|
||||
};
|
||||
const translateDelta = getImageTranslate(scaledImageDimensions, SCREEN);
|
||||
const left = initialTranslate.x - translateDelta.x;
|
||||
const right = left - (scaledImageDimensions.width - SCREEN.width);
|
||||
const top = initialTranslate.y - translateDelta.y;
|
||||
const bottom = top - (scaledImageDimensions.height - SCREEN.height);
|
||||
return [top, left, bottom, right];
|
||||
};
|
||||
const getTranslateInBounds = (translate, scale) => {
|
||||
const inBoundTranslate = { x: translate.x, y: translate.y };
|
||||
const [topBound, leftBound, bottomBound, rightBound] = getBounds(scale);
|
||||
if (translate.x > leftBound) {
|
||||
inBoundTranslate.x = leftBound;
|
||||
}
|
||||
else if (translate.x < rightBound) {
|
||||
inBoundTranslate.x = rightBound;
|
||||
}
|
||||
if (translate.y > topBound) {
|
||||
inBoundTranslate.y = topBound;
|
||||
}
|
||||
else if (translate.y < bottomBound) {
|
||||
inBoundTranslate.y = bottomBound;
|
||||
}
|
||||
return inBoundTranslate;
|
||||
};
|
||||
const fitsScreenByWidth = () => imageDimensions.width * currentScale < SCREEN_WIDTH;
|
||||
const fitsScreenByHeight = () => imageDimensions.height * currentScale < SCREEN_HEIGHT;
|
||||
useEffect(() => {
|
||||
scaleValue.addListener(({ value }) => {
|
||||
if (typeof onZoom === "function") {
|
||||
onZoom(value !== initialScale);
|
||||
}
|
||||
});
|
||||
return () => scaleValue.removeAllListeners();
|
||||
});
|
||||
const cancelLongPressHandle = () => {
|
||||
longPressHandlerRef && clearTimeout(longPressHandlerRef);
|
||||
};
|
||||
const handlers = {
|
||||
onGrant: (_, gestureState) => {
|
||||
numberInitialTouches = gestureState.numberActiveTouches;
|
||||
if (gestureState.numberActiveTouches > 1)
|
||||
return;
|
||||
longPressHandlerRef = setTimeout(onLongPress, delayLongPress);
|
||||
},
|
||||
onStart: (event, gestureState) => {
|
||||
initialTouches = event.nativeEvent.touches;
|
||||
numberInitialTouches = gestureState.numberActiveTouches;
|
||||
if (gestureState.numberActiveTouches > 1)
|
||||
return;
|
||||
const tapTS = Date.now();
|
||||
// Handle double tap event by calculating diff between first and second taps timestamps
|
||||
isDoubleTapPerformed = Boolean(lastTapTS && tapTS - lastTapTS < DOUBLE_TAP_DELAY);
|
||||
if (doubleTapToZoomEnabled && isDoubleTapPerformed) {
|
||||
const isScaled = currentTranslate.x !== initialTranslate.x; // currentScale !== initialScale;
|
||||
const { pageX: touchX, pageY: touchY } = event.nativeEvent.touches[0];
|
||||
const targetScale = SCALE_MAX;
|
||||
const nextScale = isScaled ? initialScale : targetScale;
|
||||
const nextTranslate = isScaled
|
||||
? initialTranslate
|
||||
: getTranslateInBounds({
|
||||
x: initialTranslate.x +
|
||||
(SCREEN_WIDTH / 2 - touchX) * (targetScale / currentScale),
|
||||
y: initialTranslate.y +
|
||||
(SCREEN_HEIGHT / 2 - touchY) * (targetScale / currentScale),
|
||||
}, targetScale);
|
||||
onZoom(!isScaled);
|
||||
Animated.parallel([
|
||||
Animated.timing(translateValue.x, {
|
||||
toValue: nextTranslate.x,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(translateValue.y, {
|
||||
toValue: nextTranslate.y,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(scaleValue, {
|
||||
toValue: nextScale,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
], { stopTogether: false }).start(() => {
|
||||
currentScale = nextScale;
|
||||
currentTranslate = nextTranslate;
|
||||
});
|
||||
lastTapTS = null;
|
||||
}
|
||||
else {
|
||||
lastTapTS = Date.now();
|
||||
}
|
||||
},
|
||||
onMove: (event, gestureState) => {
|
||||
const { dx, dy } = gestureState;
|
||||
if (Math.abs(dx) >= meaningfulShift || Math.abs(dy) >= meaningfulShift) {
|
||||
cancelLongPressHandle();
|
||||
}
|
||||
// Don't need to handle move because double tap in progress (was handled in onStart)
|
||||
if (doubleTapToZoomEnabled && isDoubleTapPerformed) {
|
||||
cancelLongPressHandle();
|
||||
return;
|
||||
}
|
||||
if (numberInitialTouches === 1 &&
|
||||
gestureState.numberActiveTouches === 2) {
|
||||
numberInitialTouches = 2;
|
||||
initialTouches = event.nativeEvent.touches;
|
||||
}
|
||||
const isTapGesture = numberInitialTouches == 1 && gestureState.numberActiveTouches === 1;
|
||||
const isPinchGesture = numberInitialTouches === 2 && gestureState.numberActiveTouches === 2;
|
||||
if (isPinchGesture) {
|
||||
cancelLongPressHandle();
|
||||
const initialDistance = getDistanceBetweenTouches(initialTouches);
|
||||
const currentDistance = getDistanceBetweenTouches(event.nativeEvent.touches);
|
||||
let nextScale = (currentDistance / initialDistance) * currentScale;
|
||||
/**
|
||||
* In case image is scaling smaller than initial size ->
|
||||
* slow down this transition by applying OUT_BOUND_MULTIPLIER
|
||||
*/
|
||||
if (nextScale < initialScale) {
|
||||
nextScale =
|
||||
nextScale + (initialScale - nextScale) * OUT_BOUND_MULTIPLIER;
|
||||
}
|
||||
/**
|
||||
* In case image is scaling down -> move it in direction of initial position
|
||||
*/
|
||||
if (currentScale > initialScale && currentScale > nextScale) {
|
||||
const k = (currentScale - initialScale) / (currentScale - nextScale);
|
||||
const nextTranslateX = nextScale < initialScale
|
||||
? initialTranslate.x
|
||||
: currentTranslate.x -
|
||||
(currentTranslate.x - initialTranslate.x) / k;
|
||||
const nextTranslateY = nextScale < initialScale
|
||||
? initialTranslate.y
|
||||
: currentTranslate.y -
|
||||
(currentTranslate.y - initialTranslate.y) / k;
|
||||
translateValue.x.setValue(nextTranslateX);
|
||||
translateValue.y.setValue(nextTranslateY);
|
||||
tmpTranslate = { x: nextTranslateX, y: nextTranslateY };
|
||||
}
|
||||
scaleValue.setValue(nextScale);
|
||||
tmpScale = nextScale;
|
||||
}
|
||||
if (isTapGesture && currentScale > initialScale) {
|
||||
const { x, y } = currentTranslate;
|
||||
const { dx, dy } = gestureState;
|
||||
const [topBound, leftBound, bottomBound, rightBound] = getBounds(currentScale);
|
||||
let nextTranslateX = x + dx;
|
||||
let nextTranslateY = y + dy;
|
||||
if (nextTranslateX > leftBound) {
|
||||
nextTranslateX =
|
||||
nextTranslateX -
|
||||
(nextTranslateX - leftBound) * OUT_BOUND_MULTIPLIER;
|
||||
}
|
||||
if (nextTranslateX < rightBound) {
|
||||
nextTranslateX =
|
||||
nextTranslateX -
|
||||
(nextTranslateX - rightBound) * OUT_BOUND_MULTIPLIER;
|
||||
}
|
||||
if (nextTranslateY > topBound) {
|
||||
nextTranslateY =
|
||||
nextTranslateY - (nextTranslateY - topBound) * OUT_BOUND_MULTIPLIER;
|
||||
}
|
||||
if (nextTranslateY < bottomBound) {
|
||||
nextTranslateY =
|
||||
nextTranslateY -
|
||||
(nextTranslateY - bottomBound) * OUT_BOUND_MULTIPLIER;
|
||||
}
|
||||
if (fitsScreenByWidth()) {
|
||||
nextTranslateX = x;
|
||||
}
|
||||
if (fitsScreenByHeight()) {
|
||||
nextTranslateY = y;
|
||||
}
|
||||
translateValue.x.setValue(nextTranslateX);
|
||||
translateValue.y.setValue(nextTranslateY);
|
||||
tmpTranslate = { x: nextTranslateX, y: nextTranslateY };
|
||||
}
|
||||
},
|
||||
onRelease: () => {
|
||||
cancelLongPressHandle();
|
||||
if (isDoubleTapPerformed) {
|
||||
isDoubleTapPerformed = false;
|
||||
}
|
||||
if (tmpScale > 0) {
|
||||
if (tmpScale < initialScale || tmpScale > SCALE_MAX) {
|
||||
tmpScale = tmpScale < initialScale ? initialScale : SCALE_MAX;
|
||||
Animated.timing(scaleValue, {
|
||||
toValue: tmpScale,
|
||||
duration: 100,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
}
|
||||
currentScale = tmpScale;
|
||||
tmpScale = 0;
|
||||
}
|
||||
if (tmpTranslate) {
|
||||
const { x, y } = tmpTranslate;
|
||||
const [topBound, leftBound, bottomBound, rightBound] = getBounds(currentScale);
|
||||
let nextTranslateX = x;
|
||||
let nextTranslateY = y;
|
||||
if (!fitsScreenByWidth()) {
|
||||
if (nextTranslateX > leftBound) {
|
||||
nextTranslateX = leftBound;
|
||||
}
|
||||
else if (nextTranslateX < rightBound) {
|
||||
nextTranslateX = rightBound;
|
||||
}
|
||||
}
|
||||
if (!fitsScreenByHeight()) {
|
||||
if (nextTranslateY > topBound) {
|
||||
nextTranslateY = topBound;
|
||||
}
|
||||
else if (nextTranslateY < bottomBound) {
|
||||
nextTranslateY = bottomBound;
|
||||
}
|
||||
}
|
||||
Animated.parallel([
|
||||
Animated.timing(translateValue.x, {
|
||||
toValue: nextTranslateX,
|
||||
duration: 100,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(translateValue.y, {
|
||||
toValue: nextTranslateY,
|
||||
duration: 100,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
currentTranslate = { x: nextTranslateX, y: nextTranslateY };
|
||||
tmpTranslate = null;
|
||||
}
|
||||
},
|
||||
};
|
||||
const panResponder = useMemo(() => createPanResponder(handlers), [handlers]);
|
||||
return [panResponder.panHandlers, scaleValue, translateValue];
|
||||
};
|
||||
export default usePanResponder;
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
declare const useRequestClose: (onRequestClose: () => void) => readonly [number, () => void];
|
||||
export default useRequestClose;
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import { useState } from "react";
|
||||
const useRequestClose = (onRequestClose) => {
|
||||
const [opacity, setOpacity] = useState(1);
|
||||
return [
|
||||
opacity,
|
||||
() => {
|
||||
setOpacity(0);
|
||||
onRequestClose();
|
||||
setTimeout(() => setOpacity(1), 0);
|
||||
},
|
||||
];
|
||||
};
|
||||
export default useRequestClose;
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
export { default } from "./ImageViewing";
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
export { default } from "./ImageViewing";
|
||||
Vendored
+48
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import { Animated, GestureResponderEvent, PanResponderGestureState, PanResponderInstance, NativeTouchEvent } from "react-native";
|
||||
import { Dimensions, Position } from "./@types";
|
||||
declare type CacheStorageItem = {
|
||||
key: string;
|
||||
value: any;
|
||||
};
|
||||
export declare const createCache: (cacheSize: number) => {
|
||||
_storage: CacheStorageItem[];
|
||||
get(key: string): any;
|
||||
set(key: string, value: any): void;
|
||||
};
|
||||
export declare const splitArrayIntoBatches: (arr: any[], batchSize: number) => any[];
|
||||
export declare const getImageTransform: (image: Dimensions | null, screen: Dimensions) => readonly [] | readonly [{
|
||||
readonly x: number;
|
||||
readonly y: number;
|
||||
}, number];
|
||||
export declare const getImageStyles: (image: Dimensions | null, translate: Animated.ValueXY, scale?: Animated.Value | undefined) => {
|
||||
width: number;
|
||||
height: number;
|
||||
transform?: undefined;
|
||||
} | {
|
||||
width: number;
|
||||
height: number;
|
||||
transform: {
|
||||
[key: string]: Animated.Value;
|
||||
}[];
|
||||
};
|
||||
export declare const getImageTranslate: (image: Dimensions, screen: Dimensions) => Position;
|
||||
export declare const getImageDimensionsByTranslate: (translate: Position, screen: Dimensions) => Dimensions;
|
||||
export declare const getImageTranslateForScale: (currentTranslate: Position, targetScale: number, screen: Dimensions) => Position;
|
||||
declare type HandlerType = (event: GestureResponderEvent, state: PanResponderGestureState) => void;
|
||||
declare type PanResponderProps = {
|
||||
onGrant: HandlerType;
|
||||
onStart?: HandlerType;
|
||||
onMove: HandlerType;
|
||||
onRelease?: HandlerType;
|
||||
onTerminate?: HandlerType;
|
||||
};
|
||||
export declare const createPanResponder: ({ onGrant, onStart, onMove, onRelease, onTerminate, }: PanResponderProps) => PanResponderInstance;
|
||||
export declare const getDistanceBetweenTouches: (touches: NativeTouchEvent[]) => number;
|
||||
export {};
|
||||
Vendored
+101
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import { Animated, PanResponder, } from "react-native";
|
||||
export const createCache = (cacheSize) => ({
|
||||
_storage: [],
|
||||
get(key) {
|
||||
const { value } = this._storage.find(({ key: storageKey }) => storageKey === key) || {};
|
||||
return value;
|
||||
},
|
||||
set(key, value) {
|
||||
if (this._storage.length >= cacheSize) {
|
||||
this._storage.shift();
|
||||
}
|
||||
this._storage.push({ key, value });
|
||||
},
|
||||
});
|
||||
export const splitArrayIntoBatches = (arr, batchSize) => arr.reduce((result, item) => {
|
||||
const batch = result.pop() || [];
|
||||
if (batch.length < batchSize) {
|
||||
batch.push(item);
|
||||
result.push(batch);
|
||||
}
|
||||
else {
|
||||
result.push(batch, [item]);
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
export const getImageTransform = (image, screen) => {
|
||||
var _a, _b;
|
||||
if (!((_a = image) === null || _a === void 0 ? void 0 : _a.width) || !((_b = image) === null || _b === void 0 ? void 0 : _b.height)) {
|
||||
return [];
|
||||
}
|
||||
const wScale = screen.width / image.width;
|
||||
const hScale = screen.height / image.height;
|
||||
const scale = Math.min(wScale, hScale);
|
||||
const { x, y } = getImageTranslate(image, screen);
|
||||
return [{ x, y }, scale];
|
||||
};
|
||||
export const getImageStyles = (image, translate, scale) => {
|
||||
var _a, _b;
|
||||
if (!((_a = image) === null || _a === void 0 ? void 0 : _a.width) || !((_b = image) === null || _b === void 0 ? void 0 : _b.height)) {
|
||||
return { width: 0, height: 0 };
|
||||
}
|
||||
const transform = translate.getTranslateTransform();
|
||||
if (scale) {
|
||||
transform.push({ scale }, { perspective: new Animated.Value(1000) });
|
||||
}
|
||||
return {
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
transform,
|
||||
};
|
||||
};
|
||||
export const getImageTranslate = (image, screen) => {
|
||||
const getTranslateForAxis = (axis) => {
|
||||
const imageSize = axis === "x" ? image.width : image.height;
|
||||
const screenSize = axis === "x" ? screen.width : screen.height;
|
||||
return (screenSize - imageSize) / 2;
|
||||
};
|
||||
return {
|
||||
x: getTranslateForAxis("x"),
|
||||
y: getTranslateForAxis("y"),
|
||||
};
|
||||
};
|
||||
export const getImageDimensionsByTranslate = (translate, screen) => ({
|
||||
width: screen.width - translate.x * 2,
|
||||
height: screen.height - translate.y * 2,
|
||||
});
|
||||
export const getImageTranslateForScale = (currentTranslate, targetScale, screen) => {
|
||||
const { width, height } = getImageDimensionsByTranslate(currentTranslate, screen);
|
||||
const targetImageDimensions = {
|
||||
width: width * targetScale,
|
||||
height: height * targetScale,
|
||||
};
|
||||
return getImageTranslate(targetImageDimensions, screen);
|
||||
};
|
||||
export const createPanResponder = ({ onGrant, onStart, onMove, onRelease, onTerminate, }) => PanResponder.create({
|
||||
onStartShouldSetPanResponder: () => true,
|
||||
onStartShouldSetPanResponderCapture: () => true,
|
||||
onMoveShouldSetPanResponder: () => true,
|
||||
onMoveShouldSetPanResponderCapture: () => true,
|
||||
onPanResponderGrant: onGrant,
|
||||
onPanResponderStart: onStart,
|
||||
onPanResponderMove: onMove,
|
||||
onPanResponderRelease: onRelease,
|
||||
onPanResponderTerminate: onTerminate,
|
||||
onPanResponderTerminationRequest: () => false,
|
||||
onShouldBlockNativeResponder: () => false,
|
||||
});
|
||||
export const getDistanceBetweenTouches = (touches) => {
|
||||
const [a, b] = touches;
|
||||
if (a == null || b == null) {
|
||||
return 0;
|
||||
}
|
||||
return Math.sqrt(Math.pow(a.pageX - b.pageX, 2) + Math.pow(a.pageY - b.pageY, 2));
|
||||
};
|
||||
Reference in New Issue
Block a user