Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
node_modules/
dist/
.expo
.vscode
.vscode
.DS_Store
17 changes: 17 additions & 0 deletions dist/@types/index.d.ts
Original file line number Diff line number Diff line change
@@ -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;
7 changes: 0 additions & 7 deletions example/babel.config.js → dist/@types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,3 @@
* LICENSE file in the root directory of this source tree.
*
*/

module.exports = function(api) {
api.cache(true);
return {
presets: ["babel-preset-expo"]
};
};
33 changes: 33 additions & 0 deletions dist/ImageViewing.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* 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;
delayLongPress?: number;
HeaderComponent?: ComponentType<{
imageIndex: number;
}>;
FooterComponent?: ComponentType<{
imageIndex: number;
}>;
};
declare const EnhancedImageViewing: (props: Props) => JSX.Element;
export default EnhancedImageViewing;
89 changes: 89 additions & 0 deletions dist/ImageViewing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* 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, useEffect } from "react";
import { Animated, Dimensions, StyleSheet, View, VirtualizedList, Modal, Platform, } 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;
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, }) {
const isWeb = Platform.OS === "web";
const imageList = useRef(null);
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;
if (Platform.OS !== "web") {
// @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);
}, [imageList]);
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={isWeb ? undefined : onScroll} onScroll={isWeb ? onScroll : undefined}
//@ts-ignore
keyExtractor={(imageSrc, index) => keyExtractor
? keyExtractor(imageSrc, index)
: typeof imageSrc === "number"
? `${imageSrc}`
: imageSrc.uri}/>
{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 changes: 13 additions & 0 deletions dist/components/ImageDefaultHeader.d.ts
Original file line number Diff line number Diff line change
@@ -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;
38 changes: 38 additions & 0 deletions dist/components/ImageDefaultHeader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* 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: 44,
height: 44,
alignItems: "center",
justifyContent: "center",
borderRadius: 22,
backgroundColor: "#00000077",
},
closeText: {
lineHeight: 22,
fontSize: 19,
textAlign: "center",
color: "#FFF",
includeFontPadding: false,
},
});
export default ImageDefaultHeader;
20 changes: 20 additions & 0 deletions dist/components/ImageItem/ImageItem.android.d.ts
Original file line number Diff line number Diff line change
@@ -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;
84 changes: 84 additions & 0 deletions dist/components/ImageItem/ImageItem.android.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* 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, ScrollView, 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 = useRef(null);
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 = useCallback((isZoomed) => {
var _a;
onZoom(isZoomed);
if ((_a = imageContainer) === null || _a === void 0 ? void 0 : _a.current) {
imageContainer.current.setNativeProps({
scrollEnabled: !isZoomed,
});
}
}, [imageContainer]);
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 (<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 />}
</ScrollView>);
};
const styles = StyleSheet.create({
listItem: {
width: SCREEN_WIDTH,
height: SCREEN_HEIGHT,
},
imageScrollContainer: {
height: SCREEN_HEIGHT * 2,
},
});
export default React.memo(ImageItem);
20 changes: 20 additions & 0 deletions dist/components/ImageItem/ImageItem.ios.d.ts
Original file line number Diff line number Diff line change
@@ -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;
Loading