Skip to content

Instantly share code, notes, and snippets.

@aolufisayo
Created August 5, 2019 12:03
Show Gist options
  • Save aolufisayo/0dbabc52912d0bcc05c8704ce38228ab to your computer and use it in GitHub Desktop.
Save aolufisayo/0dbabc52912d0bcc05c8704ce38228ab to your computer and use it in GitHub Desktop.
React Native Gesture Blurview Drawer
// 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