react-native-lightbox-v2
Version:
Images etc in Full Screen Lightbox Popovers for React Native
240 lines (239 loc) • 8.59 kB
JavaScript
import React, { useRef, useEffect, useState } from "react";
import { Animated, Dimensions, PanResponder, Platform, StyleSheet, StatusBar, TouchableOpacity, Text, Modal, SafeAreaView, } from "react-native";
import { useGesture, useNextTick } from "./hooks";
const { width: WINDOW_WIDTH, height: WINDOW_HEIGHT } = Dimensions.get("window");
const isIOS = Platform.OS === "ios";
const getDefaultTarget = () => ({ x: 0, y: 0, opacity: 1 });
const styles = StyleSheet.create({
background: {
position: "absolute",
top: 0,
left: 0,
width: WINDOW_WIDTH,
height: WINDOW_HEIGHT,
},
open: {
position: "absolute",
flex: 1,
justifyContent: "center",
alignItems: "center",
// Android pan handlers crash without this declaration:
backgroundColor: "transparent",
},
header: {
position: "absolute",
top: 0,
left: 0,
width: WINDOW_WIDTH,
backgroundColor: "transparent",
},
closeButton: {
fontSize: 35,
color: "white",
lineHeight: 60,
width: 70,
textAlign: "center",
shadowOffset: {
width: 0,
height: 0,
},
shadowRadius: 1.5,
shadowColor: "black",
shadowOpacity: 0.8,
},
});
const LightboxOverlay = ({ useNativeDriver = false, dragDismissThreshold, springConfig, isOpen, onClose, willClose, didOpen, swipeToDismiss, origin, backgroundColor, renderHeader, modalProps, children, doubleTapZoomEnabled, doubleTapGapTimer, doubleTapCallback, doubleTapZoomToCenter, doubleTapMaxZoom, doubleTapZoomStep, doubleTapInitialScale, doubleTapAnimationDuration, longPressGapTimer, longPressCallback, }) => {
const _panResponder = useRef();
const pan = useRef(new Animated.Value(0));
const openVal = useRef(new Animated.Value(0));
const handlers = useRef();
const [gesture, animations] = useGesture({
useNativeDriver,
doubleTapZoomEnabled,
doubleTapGapTimer,
doubleTapCallback,
doubleTapZoomToCenter,
doubleTapMaxZoom,
doubleTapZoomStep,
doubleTapInitialScale,
doubleTapAnimationDuration,
longPressGapTimer,
longPressCallback,
});
const [deviceWidth, setDeviceWidth] = useState(Dimensions.get('window').width);
const [deviceHeight, setDeviceHeight] = useState(Dimensions.get('window').height);
const [{ isAnimating, isPanning, target }, setState] = useState({
isAnimating: false,
isPanning: false,
target: getDefaultTarget(),
});
const handleCloseNextTick = useNextTick(onClose);
const close = () => {
willClose();
if (isIOS) {
StatusBar.setHidden(false, "fade");
}
gesture.reset();
setState((s) => ({
...s,
isAnimating: true,
}));
Animated.spring(openVal.current, {
toValue: 0,
...springConfig,
useNativeDriver,
}).start(({ finished }) => {
if (finished) {
setState((s) => ({ ...s, isAnimating: false }));
handleCloseNextTick();
}
});
};
const open = () => {
if (isIOS) {
StatusBar.setHidden(true, "fade");
}
pan.current.setValue(0);
setState((s) => ({
...s,
isAnimating: true,
target: getDefaultTarget(),
}));
Animated.spring(openVal.current, {
toValue: 1,
...springConfig,
useNativeDriver,
}).start(({ finished }) => {
if (finished) {
setState((s) => ({ ...s, isAnimating: false }));
didOpen();
}
});
};
const initPanResponder = () => {
_panResponder.current = PanResponder.create({
// Ask to be the responder:
onStartShouldSetPanResponder: () => !isAnimating,
onStartShouldSetPanResponderCapture: () => !isAnimating,
onMoveShouldSetPanResponder: () => !isAnimating,
onMoveShouldSetPanResponderCapture: () => !isAnimating,
onPanResponderGrant: (e, gestureState) => {
gesture.init();
pan.current.setValue(0);
setState((s) => ({ ...s, isPanning: true }));
gesture.onLongPress(e, gestureState);
gesture.onDoubleTap(e, gestureState);
},
onPanResponderMove: Animated.event([null, { dy: pan.current }], {
useNativeDriver,
}),
onPanResponderTerminationRequest: () => true,
onPanResponderRelease: (evt, gestureState) => {
gesture.release();
if (gesture.isDoubleTaped)
return;
if (gesture.isLongPressed)
return;
if (Math.abs(gestureState.dy) > dragDismissThreshold) {
setState((s) => ({
...s,
isPanning: false,
target: {
y: gestureState.dy,
x: gestureState.dx,
opacity: 1 - Math.abs(gestureState.dy / WINDOW_HEIGHT),
},
}));
close();
}
else {
Animated.spring(pan.current, {
toValue: 0,
...springConfig,
useNativeDriver,
}).start(({ finished }) => {
finished && setState((s) => ({ ...s, isPanning: false }));
});
}
},
});
};
useEffect(() => {
const onChange = ({ window }) => {
setDeviceWidth(window.width);
setDeviceHeight(window.height);
};
const removeEventListener = () => {
if (Dimensions.removeEventListener) {
Dimensions.removeEventListener('change', onChange);
}
};
Dimensions.addEventListener('change', onChange);
return removeEventListener;
}, []);
useEffect(() => {
initPanResponder();
}, [useNativeDriver, isAnimating]);
useEffect(() => {
isOpen && open();
}, [isOpen]);
useEffect(() => {
if (_panResponder.current && swipeToDismiss) {
handlers.current = _panResponder.current.panHandlers;
}
}, [swipeToDismiss, _panResponder.current]);
const lightboxOpacityStyle = {
opacity: openVal.current.interpolate({
inputRange: [0, 1],
outputRange: [0, target.opacity],
}),
};
let dragStyle;
if (isPanning) {
dragStyle = {
top: pan.current,
};
lightboxOpacityStyle.opacity = pan.current.interpolate({
inputRange: [-WINDOW_HEIGHT, 0, WINDOW_HEIGHT],
outputRange: [0, 1, 0],
});
}
const openStyle = [
styles.open,
{
left: openVal.current.interpolate({
inputRange: [0, 1],
outputRange: [origin.x, target.x],
}),
top: openVal.current.interpolate({
inputRange: [0, 1],
outputRange: [origin.y, target.y],
}),
width: openVal.current.interpolate({
inputRange: [0, 1],
outputRange: [origin.width, deviceWidth],
}),
height: openVal.current.interpolate({
inputRange: [0, 1],
outputRange: [origin.height, deviceHeight],
}),
},
];
const background = (<Animated.View style={[styles.background, { backgroundColor }, lightboxOpacityStyle]}></Animated.View>);
const header = (<Animated.View style={[styles.header, lightboxOpacityStyle]}>
{renderHeader ? (renderHeader(close)) : (<SafeAreaView>
<TouchableOpacity onPress={close}>
<Text style={styles.closeButton}>×</Text>
</TouchableOpacity>
</SafeAreaView>)}
</Animated.View>);
const content = (<Animated.View style={[openStyle, dragStyle, animations]} {...handlers.current}>
{children}
</Animated.View>);
return (<Modal visible={isOpen} transparent={true} onRequestClose={close} {...modalProps}>
{background}
{content}
{header}
</Modal>);
};
export default LightboxOverlay;