Skip to content

Instantly share code, notes, and snippets.

@Grohden
Created June 24, 2019 18:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Grohden/5f36de6ea27ff69abb82886ecade9774 to your computer and use it in GitHub Desktop.
Save Grohden/5f36de6ea27ff69abb82886ecade9774 to your computer and use it in GitHub Desktop.
Just saving this refactor of the lightbox
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
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