Created
June 24, 2019 18:17
-
-
Save Grohden/5f36de6ea27ff69abb82886ecade9774 to your computer and use it in GitHub Desktop.
Just saving this refactor of the lightbox
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { | |
Children, | |
cloneElement, FC, useRef, useState | |
} from 'react' | |
import { | |
Animated, | |
TouchableHighlight, | |
View, | |
ViewStyle | |
} from 'react-native' | |
import LightboxOverlay from './LightboxOverlay' | |
type TProps = { | |
activeProps?: {} | |
renderHeader?: () => JSX.Element | |
renderContent?: () => JSX.Element | |
underlayColor?: string | |
backgroundColor?: string | |
onOpen?: () => void | |
onClose?: () => void | |
springConfig?: { | |
tension: number | |
friction: number | |
useNativeDriver: boolean | |
}, | |
animateOpening?: boolean | |
animateClosing?: boolean | |
swipeToDismiss?: boolean | |
pinchToZoom?: boolean | |
style?: ViewStyle | |
} | |
const Lightbox: FC<TProps> = props => { | |
const swipeToDismiss = props.swipeToDismiss | |
? props.swipeToDismiss | |
: true | |
const pinchToZoom = props.pinchToZoom | |
? props.pinchToZoom | |
: true | |
const onOpen = props.onOpen | |
? props.onOpen | |
: () => { } | |
const onClose = props.onClose | |
? props.onClose | |
: () => { } | |
const rootRef = useRef<View | null>(null) | |
const [isOpen, setIsOpen] = useState(false) | |
const [layoutOpacity] = useState( | |
new Animated.Value(1) | |
) | |
const [origin, setOrigin] = useState({ | |
x: 0, | |
y: 0, | |
width: 0, | |
height: 0 | |
}) | |
const getContent = () => { | |
if(props.renderContent) { | |
return props.renderContent() | |
} | |
if(props.activeProps && props.children) { | |
return cloneElement( | |
Children.only(props.children), | |
props.activeProps | |
) | |
} | |
return props.children | |
} | |
const onCloseHandler = () => { | |
layoutOpacity.setValue(1) | |
setIsOpen(false) | |
onClose() | |
} | |
const open = () => { | |
if(!rootRef.current) { | |
return | |
} | |
rootRef.current.measureInWindow((x, y, width, height) => { | |
onOpen() | |
setIsOpen(true) | |
setOrigin({ | |
width, | |
height, | |
x, | |
y | |
}) | |
setTimeout(() => { | |
layoutOpacity.setValue(0) | |
}) | |
}) | |
} | |
/* | |
* measure will not return anything useful if | |
* we don't attach an onLayout handler on android | |
*/ | |
return ( | |
<View | |
ref={ component => rootRef.current = component } | |
style={ props.style } | |
onLayout={ () => {} }> | |
<Animated.View style={ { opacity: layoutOpacity } }> | |
<TouchableHighlight | |
underlayColor={ props.underlayColor } | |
onPress={ open }> | |
{ props.children } | |
</TouchableHighlight> | |
</Animated.View> | |
<LightboxOverlay | |
isOpen={ isOpen } | |
origin={ origin } | |
renderHeader={ props.renderHeader } | |
swipeToDismiss={ swipeToDismiss } | |
pinchToZoom={ pinchToZoom } | |
springConfig={ props.springConfig } | |
animateOpening={ props.animateOpening } | |
animateClosing={ props.animateClosing } | |
backgroundColor={ props.backgroundColor } | |
onClose={ onCloseHandler }> | |
{ getContent() } | |
</LightboxOverlay> | |
</View> | |
) | |
} | |
export default Lightbox |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { | |
FC, | |
useEffect, | |
useState | |
} from 'react' | |
import { | |
Animated, | |
Dimensions, | |
Modal, | |
Platform, | |
StatusBar, | |
StyleSheet, | |
Text, | |
TouchableOpacity | |
} from 'react-native' | |
import ViewTransformer from 'react-native-view-transformer' | |
const WINDOW_HEIGHT = Dimensions.get('window').height | |
const WINDOW_WIDTH = Dimensions.get('window').width | |
// Translation threshold for closing the image preview | |
const CLOSING_THRESHOLD = 100 | |
type TProps = { | |
origin: { | |
x: number | |
y: number | |
width: number | |
height: number | |
} | |
springConfig?: { | |
tension: number | |
friction: number | |
useNativeDriver: boolean | |
} | |
animateOpening?: boolean | |
animateClosing?: boolean | |
backgroundColor?: string | |
isOpen: boolean | |
renderHeader?: (onClose: () => void) => JSX.Element | |
onOpen?: () => void | |
onClose?: () => void | |
swipeToDismiss?: boolean | |
pinchToZoom?: boolean | |
} | |
const styles = StyleSheet.create({ | |
background: { | |
position: 'absolute', | |
top: 0, | |
left: 0, | |
width: WINDOW_WIDTH, | |
height: WINDOW_HEIGHT | |
}, | |
// eslint-disable-next-line react-native/no-color-literals | |
open: { | |
position: 'absolute', | |
justifyContent: 'center', | |
// Android pan handlers crash without this declaration: | |
backgroundColor: 'transparent' | |
}, | |
// eslint-disable-next-line react-native/no-color-literals | |
header: { | |
position: 'absolute', | |
top: 0, | |
left: 0, | |
width: WINDOW_WIDTH, | |
backgroundColor: 'transparent' | |
}, | |
// eslint-disable-next-line react-native/no-color-literals | |
closeButton: { | |
fontSize: 35, | |
color: 'white', | |
lineHeight: 40, | |
width: 40, | |
textAlign: 'center', | |
shadowOffset: { | |
width: 0, | |
height: 0 | |
}, | |
shadowRadius: 1.5, | |
shadowColor: 'black', | |
shadowOpacity: 0.8 | |
}, | |
viewTransformer: { | |
flex: 1 | |
} | |
}) | |
const LightboxOverlay: FC<TProps> = props => { | |
const [isClosing, setIsClosing] = useState(false) | |
const [closingDistance] = useState(new Animated.Value(0)) | |
const [visibility] = useState(new Animated.Value(0)) | |
const [target, setTarget] = useState(props.origin) | |
const animateOpening = 'animateOpening' in props | |
? props.animateOpening | |
: true | |
const animateClosing = 'animateClosing' in props | |
? props.animateClosing | |
: false | |
const backgroundColor = props.backgroundColor || 'black' | |
const springConfig = props.springConfig | |
? props.springConfig | |
: { | |
tension: 30, | |
friction: 7, | |
/* | |
* Native animations work better on Android, but | |
* sometimes still have issues on iOS | |
*/ | |
useNativeDriver: Platform.OS === 'android' | |
} | |
const open = function() { | |
if (Platform.OS === 'ios') { | |
StatusBar.setHidden(true, 'fade') | |
} | |
if (animateOpening) { | |
Animated.spring( | |
visibility, | |
{ | |
toValue: 1, | |
...springConfig | |
} | |
).start() | |
} else { | |
visibility.setValue(1) | |
} | |
} | |
useEffect(() => { | |
if((props.isOpen != props.isOpen) && props.isOpen) { | |
open() | |
} | |
}) | |
const startClosing = () => { | |
if (isClosing) { | |
return | |
} | |
setIsClosing(true) | |
} | |
const stopClosing = () => { | |
if (!isClosing) { | |
return | |
} | |
closingDistance.setValue(0) | |
setIsClosing(false) | |
} | |
const onClose = function() { | |
if(props.onClose) { | |
props.onClose() | |
} | |
closingDistance.setValue(0) | |
visibility.setValue(0) | |
setIsClosing(false) | |
setTarget({ | |
x: 0, | |
y: 0, | |
width: WINDOW_WIDTH, | |
height: WINDOW_HEIGHT | |
}) | |
} | |
const close = () => { | |
if (Platform.OS === 'ios') { | |
StatusBar.setHidden(false, 'fade') | |
} | |
if (animateClosing) { | |
Animated.spring( | |
visibility, | |
{ toValue: 0, ...props.springConfig } | |
).start(() => onClose()) | |
} else { | |
onClose() | |
} | |
} | |
const onViewTransformed = ({ translateY, scale }) => { | |
if (scale > 1) { | |
stopClosing() | |
return | |
} | |
closingDistance.setValue(translateY) | |
if (Math.abs(translateY) > 0) { | |
startClosing() | |
} else { | |
stopClosing() | |
} | |
} | |
const onTransformGestureReleased = ({ translateX, translateY, scale }) => { | |
const { swipeToDismiss } = props | |
if(swipeToDismiss && (scale === 1) && | |
((Math.abs(translateY) > CLOSING_THRESHOLD) || | |
(Math.abs(translateX) > CLOSING_THRESHOLD)) | |
) { | |
setIsClosing(false) | |
setTarget({ | |
y: translateY, | |
x: translateX, | |
width: WINDOW_WIDTH, | |
height: WINDOW_HEIGHT | |
}) | |
close() | |
} else { | |
stopClosing() | |
} | |
} | |
const renderHeader = props.renderHeader | |
? props.renderHeader | |
: (onClose: () => void) => ( | |
<TouchableOpacity onPress={ onClose }> | |
<Text style={ styles.closeButton }>×</Text> | |
</TouchableOpacity> | |
) | |
const { | |
isOpen, | |
pinchToZoom, | |
origin | |
} = props | |
const lightboxOpacityStyle = { | |
opacity: visibility.interpolate({ | |
inputRange: [0, 0.8, 1], | |
outputRange: [0, 0.4, 1] | |
}) | |
} | |
if(isClosing) { | |
lightboxOpacityStyle.opacity = closingDistance.interpolate({ | |
inputRange: [-CLOSING_THRESHOLD * 2, 0, CLOSING_THRESHOLD * 2], | |
outputRange: [0, 1, 0] | |
}) | |
} | |
const openStyle = [styles.open, { | |
top: target.y, | |
left: target.x, | |
width: target.width, | |
height: target.height, | |
transform: [{ | |
translateX: visibility.interpolate({ | |
inputRange: [0, 1], | |
outputRange: [origin.x, target.x] | |
}) | |
}, { | |
translateY: visibility.interpolate({ | |
inputRange: [0, 1], | |
outputRange: [origin.y - origin.height, target.y] | |
}) | |
}, { | |
scale: visibility.interpolate({ | |
inputRange: [0, 1], | |
outputRange: [origin.width / WINDOW_WIDTH, 1] | |
}) | |
}] | |
}] | |
const background = ( | |
<Animated.View | |
style={ [ | |
styles.background, | |
{ backgroundColor }, | |
lightboxOpacityStyle | |
] } | |
/> | |
) | |
const header = ( | |
<Animated.View style={ [styles.header, lightboxOpacityStyle] }> | |
{ renderHeader | |
? renderHeader(close) | |
: null | |
} | |
</Animated.View> | |
) | |
const content = !pinchToZoom | |
? props.children | |
: ( | |
<ViewTransformer | |
style={ styles.viewTransformer } | |
enableTransform | |
enableScale | |
enableTranslate | |
enableResistance | |
contentAspectRatio={ origin.width / origin.height } | |
maxScale={ 3 } | |
onTransformGestureReleased={ onTransformGestureReleased } | |
onViewTransformed={ onViewTransformed }> | |
{ props.children } | |
</ViewTransformer> | |
) | |
return ( | |
<Modal | |
transparent | |
hardwareAccelerated | |
visible={ isOpen } | |
onRequestClose={ close }> | |
{ background } | |
<Animated.View style={ openStyle }> | |
{ content } | |
</Animated.View> | |
{ header } | |
</Modal> | |
) | |
} | |
export default LightboxOverlay |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment