ELI5: How To Use React Native’s Panresponder
Demystifying React Natives the Panresponder
[WILL UPDATE] I inteneded to add a write up for this but haven’t gotten around to it, but thought I’d give you the before and after with comments explaining what you’re looking at
before
/* @flow */
import React, { Component } from 'react';
import {
View,
TouchableOpacity,
Text,
StyleSheet,
PanResponder,
} from 'react-native';
export default class PanResponderDemo extends Component {
state={
debugActions:"Not Touched",
actions:[" ------------- Start ------------- "]
}
componentWillMount(){
this._panResponder = PanResponder.create({
// Ask to be the responder:
onStartShouldSetPanResponder: (evt, gestureState) => {
this.setState({actions: this.state.actions.concat("1) onStartShouldSetPanResponder")})
// return true
},
onStartShouldSetPanResponderCapture: (evt, gestureState) => {
this.setState({actions: this.state.actions.concat("2) onStartShouldSetPanResponderCapture")})
// return true
},
onMoveShouldSetPanResponder: (evt, gestureState) => {
this.setState({debugActions:"3) onMoveShouldSetPanResponder"})
// return true
},
onMoveShouldSetPanResponderCapture: (evt, gestureState) => {
this.setState({actions: this.state.actions.concat("4) onMoveShouldSetPanResponderCapture")})
// return true
},
onPanResponderGrant: (evt, gestureState) => {
// The gesture has started. Show visual feedback so the user knows
// what is happening!
// gestureState.d{x,y} will be set to zero now
},
onPanResponderMove: (evt, gestureState) => {
this.setState({actions: this.state.actions.concat("6) onPanResponderMove")})
// The most recent move distance is gestureState.move{X,Y}
// The accumulated gesture distance since becoming responder is
// gestureState.d{x,y}
},
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderRelease: (evt, gestureState) => {
this.setState({actions: this.state.actions.concat("7) onPanResponderRelease")})
// The user has released all touches while this view is the
// responder. This typically means a gesture has succeeded
},
onPanResponderTerminate: (evt, gestureState) => {
this.setState({actions: this.state.actions.concat("8) onPanResponderTerminate")})
// Another component has become the responder, so this gesture
// should be cancelled
},
onShouldBlockNativeResponder: (evt, gestureState) => {
this.setState({actions: this.state.actions.concat("9) onShouldBlockNativeResponder")})
// Returns whether this component should block native components from becoming the JS
// responder. Returns true by default. Is currently only supported on android.
return true;
},
});
}
render() {
let {actions} = this.state
if(this.state.actions.length>20){
this.state.actions.shift()
this.setState({actions: this.state.actions});
}
return (
<View style={s.container}>
{/* Pan Responder Item */}
<View style={[s.panItem,]}{...this._panResponder.panHandlers}>
</View>
{/* DEBUG FLOATING CONTAINER */}
<TouchableOpacity onPress={()=>this.setState({actions:[" ------------- Restarted ------------- "]})} style={[{},s.debugContainer]}>
{this.state.actions.map((o,i)=>{
return <Text key={i} style={s.debugActions}>{o}</Text>
})}
</TouchableOpacity>
</View>
);
}
}
const s = StyleSheet.create({
container: {
flex: 1,
justifyContent:"center",alignItems:"center",
},
panItem: {
backgroundColor:"red",
height:50,
width:50,
},
debugContainer:{
position:"absolute",
bottom:25,
right:10,
flex:1,
zIndex:3,
width:null,
height:null,
alignItems:"center",
justifyContent:"center",
backgroundColor:"rgba(0,0,0,.5)"
},
debugActions:{
fontSize: 10,
color:"white",
textAlign: "center",
margin: 0,
},
});
Im referring to each PanResponder Function by Numbers as they are long function names:
1) onStartShouldSetPanResponder 2) onStartShouldSetPanResponderCapture 4) onMoveShouldSetPanResponderCapture 6) onPanResponderMove 7) onPanResponderRelease 8) onPanResponderTerminate 9) onShouldBlockNativeResponder
(URL to Imgur Album - http://imgur.com/a/F6abq) Lets first make some observations. Here I have 1,2,3 & 4 returning false
Open in Imgur (New Tab) - https://i.imgur.com/escV7Ol.gif
Result: only calls 1 & 4 when touched
Now Lets make them (1,2,3 & 4) return true
Open in Imgur (New Tab) - http://i.imgur.com/t3aAGRK.gif
Result: calls 1,5,6,8,7 when touched
Final Product
import React, { Component } from 'react';
import { StyleSheet, View,Text, Image, Animated, Dimensions } from 'react-native';
import Interactable from 'react-native-interactable';
const widthFactor = Dimensions.get('window').width / 375;
const heightFactor = (Dimensions.get('window').height - 75) / 667;
const showSecondFace = true;
const showThirdFace = true;
const showFourthFace = true;
export default class FloatingBubbles extends Component {
constructor(props) {
super(props);
this._deltaX = new Animated.Value(0);
this._deltaY = new Animated.Value(0);
this._face1Scale = new Animated.Value(1);
this._face2Scale = new Animated.Value(1);
this._face3Scale = new Animated.Value(1);
this._face4Scale = new Animated.Value(1);
this.state={showTimer:false,secondsRemaining:3}
this.tick=this.tick.bind(this)
}
tick(){
const {secondsRemaining} = this.state
this.setState({secondsRemaining: secondsRemaining - 1});
if (this.state.secondsRemaining <= 0) {
clearInterval(this.interval);
}
}
componentDidMount() {
// this.setState({ secondsRemaining: this.props.secondsRemaining });
// this.interval = setInterval(this.tick, 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
const{showTimer}=this.state
return (
<View style={styles.container}>
{!showTimer ? false :
<Text style={{fontSize: 20,textAlign: "center",margin: 10,}}> {this.state.secondsRemaining}</Text>
}
<View style={[styles.frame,{borderWidth:2}]}>
<Animated.Image
source={{uri:"https://rawgit.com/wix/react-native-interactable/master/real-life-example/img/chatheads-delete.png"}}
style={[styles.marker, {top: 200*heightFactor}, {
opacity: this._deltaY.interpolate({
inputRange: [-10*heightFactor, 50*heightFactor],
outputRange: [0, 1],
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp'
}),
transform: [{
translateX: this._deltaX.interpolate({
inputRange: [-140*widthFactor, 140*widthFactor],
outputRange: [-10, 10]
})
},
{
translateY: this._deltaY.interpolate({
inputRange: [-30*heightFactor, 50*heightFactor, 270*heightFactor],
outputRange: [50*heightFactor, -10, 10],
extrapolateLeft: 'clamp'
})
}]
}
]} />
</View>
<View style={styles.frame} pointerEvents='box-none'>
<Interactable.View
snapPoints={[
{x: -140*widthFactor, y: 0}, {x: -140*widthFactor, y: -140*heightFactor}, {x: -140*widthFactor, y: 140*heightFactor}, {x: -140*widthFactor, y: -270*heightFactor}, {x: -140*widthFactor, y: 270*heightFactor},
{x: 140*widthFactor, y: 0}, {x: 140*widthFactor, y: 140*heightFactor}, {x: 140*widthFactor, y: -140*heightFactor}, {x: 140*widthFactor, y: -270*heightFactor}, {x: 140*widthFactor, y: 270*heightFactor}]}
dragWithSpring={{tension: 2000, damping: 0.5}}
gravityPoints={[{x: 0, y: 200*heightFactor, strength: 8000, falloff: 40, damping: 0.5, haptics: true}]}
onStop={(event) => this.onStopInteraction(event, this._face1Scale)}
animatedValueX={this._deltaX}
animatedValueY={this._deltaY}
initialPosition={{x: -140*widthFactor, y: -270*heightFactor}}>
<Animated.View style={[styles.head, {
transform: [{
scale: this._face1Scale
}]
}]}>
<Image resizeMode="center" style={styles.image} source={{uri:"https://rawgit.com/unitedstates/images/gh-pages/congress/225x275/S000033.jpg"}} />
</Animated.View>
</Interactable.View>
</View>
{!showSecondFace ? false :
<View style={styles.frame} pointerEvents='box-none'>
<Interactable.View
snapPoints={[
{x: -140*widthFactor, y: 20*heightFactor}, {x: -140*widthFactor, y: -120*heightFactor}, {x: -140*widthFactor, y: 160*heightFactor}, {x: -140*widthFactor, y: -250*heightFactor}, {x: -140*widthFactor, y: 290*heightFactor},
{x: 140*widthFactor, y: 20*heightFactor}, {x: 140*widthFactor, y: 160*heightFactor}, {x: 140*widthFactor, y: -120*heightFactor}, {x: 140*widthFactor, y: -250*heightFactor}, {x: 140*widthFactor, y: 290*heightFactor}]}
dragWithSpring={{tension: 2000, damping: 0.5}}
gravityPoints={[{x: 0, y: 200*heightFactor, strength: 8000, falloff: 40, damping: 0.5, haptics: true}]}
onStop={(event) => this.onStopInteraction(event, this._face2Scale)}
animatedValueX={this._deltaX}
animatedValueY={this._deltaY}
initialPosition={{x: 150*widthFactor, y: -250*heightFactor}}>
<Animated.View style={[styles.head, {
transform: [{
scale: this._face2Scale
}]
}]}>
<Image resizeMode="center" style={styles.image} source={{uri:"https://rawgit.com/unitedstates/images/gh-pages/congress/225x275/W000817.jpg"}} />
</Animated.View>
</Interactable.View>
</View>
}
{!showThirdFace ? false :
<View style={styles.frame} pointerEvents='box-none'>
<Interactable.View
snapPoints={[
{x: -140*widthFactor, y: 20*heightFactor}, {x: -140*widthFactor, y: -120*heightFactor}, {x: -140*widthFactor, y: 160*heightFactor}, {x: -140*widthFactor, y: -250*heightFactor}, {x: -140*widthFactor, y: 290*heightFactor},
{x: 140*widthFactor, y: 20*heightFactor}, {x: 140*widthFactor, y: 160*heightFactor}, {x: 140*widthFactor, y: -120*heightFactor}, {x: 140*widthFactor, y: -250*heightFactor}, {x: 140*widthFactor, y: 290*heightFactor}]}
dragWithSpring={{tension: 2000, damping: 0.5}}
gravityPoints={[{x: 0, y: 200*heightFactor, strength: 8000, falloff: 40, damping: 0.5, haptics: true}]}
onStop={(event) => this.onStopInteraction(event, this._face3Scale)}
animatedValueX={this._deltaX}
animatedValueY={this._deltaY}
initialPosition={{x: 140*widthFactor, y: -50*heightFactor}}>
<Animated.View style={[styles.head, {
transform: [{
scale: this._face3Scale
}]
}]}>
<Image resizeMode="center" style={styles.image} source={{uri:"https://rawgit.com/unitedstates/images/gh-pages/congress/225x275/F000457.jpg"}} />
</Animated.View>
</Interactable.View>
</View>
}
{!showFourthFace ? false :
<View style={styles.frame} pointerEvents='box-none'>
<Interactable.View
snapPoints={[
{x: -140*widthFactor, y: 20*heightFactor}, {x: -140*widthFactor, y: -120*heightFactor}, {x: -140*widthFactor, y: 160*heightFactor}, {x: -140*widthFactor, y: -250*heightFactor}, {x: -140*widthFactor, y: 290*heightFactor},
{x: 140*widthFactor, y: 20*heightFactor}, {x: 140*widthFactor, y: 160*heightFactor}, {x: 140*widthFactor, y: -120*heightFactor}, {x: 140*widthFactor, y: -250*heightFactor}, {x: 140*widthFactor, y: 290*heightFactor}]}
dragWithSpring={{tension: 2000, damping: 0.5}}
gravityPoints={[{x: 0, y: 200*heightFactor, strength: 8000, falloff: 40, damping: 0.5, haptics: true}]}
onStop={(event) => this.onStopInteraction(event, this._face4Scale)}
animatedValueX={this._deltaX}
animatedValueY={this._deltaY}
initialPosition={{x: 140*widthFactor, y: -250*heightFactor}}>
<Animated.View style={[styles.head, {
transform: [{
scale: this._face4Scale
}]
}]}>
<Image resizeMode="center" style={styles.image} source={{uri:"https://rawgit.com/unitedstates/images/gh-pages/congress/225x275/B001288.jpg"}} />
</Animated.View>
</Interactable.View>
</View>
}
</View>
);
}
onStopInteraction(event, scaleValue) {
const x = event.nativeEvent.x;
const y = event.nativeEvent.y;
if (x > -10 && x < 10 && y < 210*heightFactor && y > 190*heightFactor) {
this.interval = setInterval(this.tick, 1000);
this.setState({showTimer:true})
if(this.state.secondsRemaining===0){
clearInterval(this.interval);
this.setState({showTimer:false,secondsRemaining:3})
Animated.timing(scaleValue, {toValue: 0, duration: 300}).start();
}
}
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#eff7ff',
},
head: {
width: 80,
height: 80,
borderRadius: 40,
borderWidth: 1,
borderColor: '#dddddd',
shadowColor: '#000000',
shadowOffset: {
width: 0,
height: 0
},
shadowRadius: 3,
shadowOpacity: 0.8
},
image: {
width: 78,
height: 78,
borderRadius: 40,
},
frame: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0
},
marker: {
width: 60,
height: 60,
margin: 10,
position: 'relative'
},
});
Snapshots