Created
August 5, 2019 12:03
-
-
Save aolufisayo/0dbabc52912d0bcc05c8704ce38228ab to your computer and use it in GitHub Desktop.
React Native Gesture Blurview Drawer
This file contains 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
// This SNACK is a follow up of this tweet: | |
// https://twitter.com/MengTo/status/1142539362875392001 | |
// I just wanted to recreate it using React Native ❤️ | |
// Take the code as it is, I'll not refactor it -> unnecessary effort | |
import React, { Component } from 'react'; | |
import { | |
TouchableOpacity, | |
StatusBar, | |
Text, | |
ImageBackground, | |
Image, | |
Dimensions, | |
Animated, | |
StyleSheet, | |
View, | |
} from 'react-native'; | |
import { PanGestureHandler, State } from 'react-native-gesture-handler'; | |
const { width, height } = Dimensions.get('screen'); | |
import { BlurView } from 'expo'; | |
import { Ionicons } from '@expo/vector-icons'; | |
const USE_NATIVE_DRIVER = false; | |
const AnimatedBlurView = Animated.createAnimatedComponent(BlurView); | |
// God bless you for giving this resource for free! | |
// Go grab it from here: | |
// https://www.freepik.com/free-vector/abstract-background-with-geometric-style_2318433.htm | |
const IMAGE = | |
'https://i.pinimg.com/564x/c5/c9/e7/c5c9e7e45ef012b3b998fd50cb3bacbd.jpg'; | |
const BLUR_DURATION = 400; | |
const ratio = 370 / 590; | |
const BOX_WIDTH = width * 0.7; | |
const BOX_HEIGHT = BOX_WIDTH * ratio; | |
const START_X = width / 2 - BOX_WIDTH / 2; | |
const START_Y = height / 2 - BOX_HEIGHT / 2; | |
// The following cards are just taken without any rights from | |
// https://dribbble.com/shots/3696146-Pleo-Virtual-Cards-Exploration-2/attachments/827707 | |
// If you're the creator and you don't want to have them here please contact me | |
// mironcatalin@gmail.com or twitter.com/mironcatalin. | |
const first = require('./assets/first.png'); | |
const second = require('./assets/second.png'); | |
const third = require('./assets/third.png'); | |
const fourth = require('./assets/fourth.png'); | |
// Tracking code from below (the dragging stuff) was entirely taken from here: | |
// https://github.com/kmagiera/react-native-gesture-handler/blob/master/Example/chatHeads/index.js | |
// I've made some modifications to it: | |
// - Using parallel instead of .start() for each animation | |
// - I removed the "snap" to points | |
// - Replace "heads" with the cards images. | |
// - Love your work @kzzzf! | |
// PS: Since @kzzzf is working on new things on Reanimated Transition API | |
// https://twitter.com/kzzzf/status/1142051701395992577 I want to distract him | |
// by tweeting "AWESOME" animations where I'm using his libraries 👹 | |
// hahahaha. | |
// If you feel this is a great SNACK | |
// ☕️️️️️️☕️️️️️️☕️️️️️️☕️️️️️️☕️️️️️️☕️️️️️️☕️️️️️️☕️️️️️️☕️️️️️️☕️️️️️️☕️️️️️️ | |
// GO BUY ME A COFFEE | |
// http://buymeacoffee.com/catalinmiron | |
// ☕️️️️️☕️️️️️️☕️️️️️️☕️️️️️️☕️️️️️️☕️️️️️️☕️️️️️️☕️️️️️️☕️️️️️️☕️️️️️️☕️️️️️️☕️️️️️️ | |
// and follow me on Twitter: http://twitter.com/mironcatalin | |
// and Subscribe to my YouTube channel: http://youtube.com/c/catalinmirondev | |
class Tracking extends Component { | |
constructor(props) { | |
super(props); | |
const tension = 0.8; | |
const friction = 3; | |
this._dragX = new Animated.Value(START_X); | |
this._transX = new Animated.Value(START_X); | |
this._follow1x = new Animated.Value(START_X); | |
this._follow2x = new Animated.Value(START_X); | |
this.blurIntensity = new Animated.Value(0); | |
Animated.parallel([ | |
Animated.spring(this._transX, { | |
toValue: this._dragX, | |
tension, | |
friction, | |
}), | |
Animated.spring(this._follow1x, { | |
toValue: this._transX, | |
tension, | |
friction, | |
}), | |
Animated.spring(this._follow2x, { | |
toValue: this._follow1x, | |
tension, | |
friction, | |
}), | |
]).start(); | |
this._dragY = new Animated.Value(START_Y); | |
this._transY = new Animated.Value(START_Y); | |
this._follow1y = new Animated.Value(START_Y); | |
this._follow2y = new Animated.Value(START_Y); | |
Animated.parallel([ | |
Animated.spring(this._transY, { | |
toValue: this._dragY, | |
tension, | |
friction, | |
}), | |
Animated.spring(this._follow1y, { | |
toValue: this._transY, | |
tension, | |
friction, | |
}), | |
Animated.spring(this._follow2y, { | |
toValue: this._follow1y, | |
tension, | |
friction, | |
}), | |
]).start(); | |
this._onGestureEvent = Animated.event( | |
[ | |
{ | |
nativeEvent: { translationX: this._dragX, translationY: this._dragY }, | |
}, | |
], | |
{ useNativeDriver: USE_NATIVE_DRIVER } | |
); | |
this._lastOffset = { x: START_X, y: START_Y }; | |
} | |
_onHandlerStateChange = event => { | |
if (event.nativeEvent.state === State.ACTIVE) { | |
Animated.timing(this.blurIntensity, { | |
duration: BLUR_DURATION, | |
toValue: 100, | |
}).start(); | |
} | |
if (event.nativeEvent.oldState === State.ACTIVE) { | |
Animated.timing(this.blurIntensity, { | |
duration: BLUR_DURATION, | |
toValue: 0, | |
}).start(); | |
const posX = this._lastOffset.x + event.nativeEvent.translationX; | |
const posY = this._lastOffset.y + event.nativeEvent.translationY; | |
this._lastOffset = { x: posX, y: posY }; | |
this._dragX.flattenOffset(); | |
this._dragY.flattenOffset(); | |
this._dragY.setValue(START_Y); | |
this._dragX.setValue(START_X); | |
this._lastOffset.x = START_X; | |
this._lastOffset.y = START_Y; | |
this._dragX.extractOffset(); | |
this._dragY.extractOffset(); | |
} | |
}; | |
render() { | |
return ( | |
<View style={StyleSheet.absoluteFillObject} onLayout={this._onLayout}> | |
<View style={[StyleSheet.absoluteFill, { backgroundColor: 'red' }]} /> | |
<ImageBackground | |
source={{ uri: IMAGE }} | |
style={[ | |
StyleSheet.absoluteFill, | |
{ | |
flex: 1, | |
alignItems: 'center', | |
paddingTop: 60, | |
}, | |
]}> | |
<Text | |
style={{ fontSize: 32, color: '#000000dd', fontFamily: 'Menlo' }}> | |
Awesome? | |
</Text> | |
<AnimatedBlurView | |
tint="default" | |
intensity={this.blurIntensity} | |
style={StyleSheet.absoluteFill} | |
/> | |
</ImageBackground> | |
<Animated.Image | |
style={[ | |
styles.box, | |
{ marginLeft: 40, marginTop: -40 }, | |
{ | |
transform: [ | |
{ translateX: this._follow2x }, | |
{ translateY: this._follow2y }, | |
], | |
}, | |
]} | |
source={first} | |
/> | |
<Animated.Image | |
style={[ | |
styles.box, | |
{ marginLeft: 20, marginTop: -20 }, | |
{ | |
transform: [ | |
{ translateX: this._follow1x }, | |
{ translateY: this._follow1y }, | |
], | |
}, | |
]} | |
source={second} | |
/> | |
<PanGestureHandler | |
onGestureEvent={this._onGestureEvent} | |
onHandlerStateChange={this._onHandlerStateChange}> | |
<Animated.Image | |
style={[ | |
styles.box, | |
{ | |
transform: [ | |
{ translateX: this._transX }, | |
{ translateY: this._transY }, | |
], | |
}, | |
]} | |
source={fourth} | |
/> | |
</PanGestureHandler> | |
</View> | |
); | |
} | |
} | |
export default class Example extends Component { | |
constructor(props) { | |
super(props); | |
this.blurIntensity = new Animated.Value(0); | |
this.drawer = new Animated.Value(0); | |
this.state = { | |
opened: false, | |
showBlur: false | |
}; | |
} | |
componentDidMount() { | |
this.drawer.addListener(({value}) => { | |
if (value === 0) { | |
// Hack to hide BlurView which is an absolute positioned element | |
// Because this has to be on top of Tracking component, we're playing | |
// with the zIndex to declare the view hierarchy. I know, but I don't care :-) | |
this.setState({ | |
showBlur: false | |
}) | |
} | |
}) | |
} | |
animate = () => { | |
this.setState( | |
{ | |
opened: !this.state.opened, | |
showBlur: true | |
}, | |
() => { | |
Animated.timing(this.drawer, { | |
duration: 400, | |
toValue: !this.state.opened ? 0 : 1, | |
useNativeDriver: USE_NATIVE_DRIVER, | |
}).start(); | |
} | |
); | |
}; | |
render() { | |
const blur = this.drawer.interpolate({ | |
inputRange: [0, 1], | |
outputRange: [0, 100], | |
}); | |
const drawerTranslate = this.drawer.interpolate({ | |
inputRange: [0, 1], | |
outputRange: [-width, 0], | |
}); | |
const drawerRotation = this.drawer.interpolate({ | |
inputRange: [0, 1], | |
outputRange: ['65deg', '0deg'], | |
}); | |
const drawerScale = this.drawer.interpolate({ | |
inputRange: [0, 1], | |
outputRange: [0.65, 1], | |
}); | |
const contentTranslate = this.drawer.interpolate({ | |
inputRange: [0, 1], | |
outputRange: [0, 40], | |
}); | |
const contentScale = this.drawer.interpolate({ | |
inputRange: [0, 1], | |
outputRange: [1, 1.2], | |
}); | |
return ( | |
<View style={styles.container}> | |
<StatusBar hidden={true} barStyle="light-content" /> | |
<Animated.View | |
style={{ | |
flex: 1, | |
transform: [ | |
{ | |
translateX: contentTranslate, | |
}, | |
{ | |
scale: contentScale, | |
}, | |
], | |
}}> | |
<Tracking /> | |
<AnimatedBlurView | |
tint="default" | |
intensity={blur} | |
style={[ | |
StyleSheet.absoluteFill, | |
{ zIndex: this.state.showBlur ? 1 : -1 }, | |
]} | |
/> | |
</Animated.View> | |
<Animated.View | |
style={[ | |
StyleSheet.absoluteFill, | |
{ | |
marginRight: width * 0.2, | |
marginVertical: 50, | |
marginLeft: 0, | |
transform: [ | |
{ | |
translateX: drawerTranslate, | |
}, | |
], | |
}, | |
]}> | |
<Animated.View | |
style={[ | |
StyleSheet.absoluteFill, | |
{ | |
transform: [ | |
{ | |
perspective: 250, | |
}, | |
{ | |
rotateY: drawerRotation, | |
}, | |
{ | |
scaleY: drawerScale, | |
}, | |
], | |
backgroundColor: 'white', | |
borderRadius: 40, | |
padding: 40, | |
shadowColor: '#000', | |
shadowOffset: { | |
width: 0, | |
height: 8, | |
}, | |
shadowOpacity: 0.44, | |
shadowRadius: 10.32, | |
elevation: 16, | |
}, | |
]}> | |
{Array.from({ length: 6 }).map((_, i) => { | |
return ( | |
<View | |
key={i} | |
style={{ | |
borderBottomColor: i === 5 ? 'transparent' : '#33333330', | |
borderBottomWidth: 1, | |
paddingVertical: 20, | |
}}> | |
<Text | |
style={{ | |
fontFamily: 'Menlo', | |
fontSize: 18, | |
color: '#333', | |
}}> | |
Menu {i} | |
</Text> | |
</View> | |
); | |
})} | |
</Animated.View> | |
</Animated.View> | |
<TouchableOpacity | |
onPress={() => this.animate(!this._opened)} | |
style={{ | |
position: 'absolute', | |
top: 20, | |
left: 20, | |
width: 42, | |
height: 42, | |
}}> | |
<Ionicons name="ios-menu" size={42} color={'#000'} /> | |
</TouchableOpacity> | |
</View> | |
); | |
} | |
} | |
const styles = StyleSheet.create({ | |
container: { | |
flex: 1, | |
backgroundColor: '#000', | |
}, | |
box: { | |
position: 'absolute', | |
width: BOX_WIDTH, | |
height: BOX_HEIGHT, | |
borderRadius: 20, | |
resizeMode: 'contain', | |
}, | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment