ELI5: How To Use React Native's Panresponder

Demystifying React Natives the Panresponder

Posted by Garrett Mac on 07/07/7990
In Development, React Native
Tags blog, react native, panresponder, animation

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

screen shot 2017-06-09 at 1 50 14 pm screen shot 2017-06-09 at 1 50 08 pm screen shot 2017-06-09 at 1 50 02 pm

comments powered by Disqus