Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

react-native change responder dynamically

I am using react-native for Android development. I have a view on which if user does long-press, I want to show an Animated View which can be dragged. I could achieve this using PanResponder, which works fine.

But what I want to do is when user does long-press, user should be able to continue the same touch/press and drag the newly shown Animated.View.

If you are familiar with Google drive app, it has similar functionality. When user long-presses any item in the list it shows draggable item. User can drag the item straight away.

enter image description here

I think if I could change the Responder dynamically to the draggable item after it starts showing then this would work.

The Question is

Does react-native provide a way to change the responder dynamically?

What I have tried so far

  • I tried with changing logic of onStartShouldSetPanResponderCapture, onMoveShouldSetPanResponderCapture, onMoveShouldSetPanResponder, onPanResponderTerminationRequest so that as soon as the draggable item starts showing the container view should not capture the start and move and accept the termination request also returning false to termination request of draggable item and returning true to it's should capture events.

  • One work-around which is working for me is to show the draggable item on the top of container with less opacity and keep the capture of it as false. As soon as user long-presses on it, I am changing the opacity of it so that it's visible clearly. With this approach user can continue the touch to drag the item. But the container is actually a list row. Thus I would need to create many draggables as user can long-press on any row.

But I think this is not a good solution and if I could change the responder, it would be great.

like image 732
Abhay Avatar asked Jul 05 '16 08:07

Abhay


1 Answers

Simple Answer

To the best of my knowledge, no, you can't dynamically change the responder of a view.

The reason why methods like onStartShouldSetPanResponderCapture don't work on a child view that you're trying to drag, is that those methods are fired on touch start, and by definition, the child view that's implementing onStartShouldSetPanResponderCapture in the behavior you describe doesn't exist yet when the touch starts.

But, there's no reason why the pan responder methods should be implemented on the child view:

The Solution

Taking a step back from the implementation, the actual functionality required is that some component in your application needs to be a pan responder. When the pan responder moves you receive the touch event. At this point, you can setNativeProps on child views to reflect the changes in the pan gesture.

So if you want to move a child view, there's no need to actually make that child the responder. You can simply make the parent the responder, and then update the child props from the parent.

I've implemented an example app below, and here's a step by step explanation of what's going on:

  1. You have a component that renders a ListView. That ListView is your pan responder. Each cell in the list view has a TouchableOpacity that responds to long presses.

  2. When the long press event happens (the onLongPress prop is fired by the row), you re-render your parent component with a floating view on top. The absolute position of this view is controlled by two properties owned by your parent component, this._previousLeft and this._previousTop.

  3. Now, this floating child view doesn't care about responding to touches. The parent is already responding. All the child cares about is its two position properties. So to move the floating child component around, all you have to do is update its top and left properties using the setNativeProps of the child's View component, in the _handlePanResponderMove function provided by the ListView.

Summary

When you're handling touches, you don't need the component being moved to actually be the one listening for touch events. The component being moved just needs to have its position property updated by whatever is listening for touch events.

Here's the complete code for the longPress/Pan gesture you've described in the Google Drive app:

import React, { PropTypes } from 'react';
import {
  AppRegistry,
  ListView,
  PanResponder,
  StyleSheet,
  Text,
  TouchableOpacity,
  View,
} from 'react-native';

class LongPressDrag extends React.Component {

  constructor() {
    super();

    this._panResponder = PanResponder.create({
      onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder.bind(this),
      onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder.bind(this),
      onPanResponderMove: this._handlePanResponderMove.bind(this),
      onPanResponderRelease: this._handlePanResponderEnd.bind(this),
      onPanResponderTerminate: this._handlePanResponderEnd.bind(this),
    });
    this._previousLeft = 0;
    this._previousTop = 0;
    this._floatingStyles = {
      style: {
        left: this._previousLeft,
        top: this._previousTop,
        position: 'absolute',
        height: 40,
        width: 100,
        backgroundColor: 'white',
        justifyContent: 'center',
      }
    };

    const rows = Array(11).fill(11).map((a, i) => i);
    this.state = {
      dataSource: new ListView.DataSource({
        rowHasChanged: (row1, row2) => row1 !== row2,
      }).cloneWithRows(rows),
      nativeEvent: undefined,
      //Pan Responder can screw with scrolling.  See https://github.com/facebook/react-native/issues/1046
      scrollEnabled: true,
    }
  }

  getDragElement() {
    if (!this.state.nativeEvent) {
      return null;
    }
    return (
      <View
        style={[this._floatingStyles.style,
          {top: this._previousTop, left: this._previousLeft}
        ]}
        ref={(floating) => {
          this.floating = floating;
        }}
      >
        <Text style={{alignSelf: 'center'}}>Floating Item</Text>
      </View>
    )
  }

  render() {
    return (
      <View>
        <ListView
          dataSource={this.state.dataSource}
          renderRow={this.renderRow.bind(this)}
          style={styles.container}
          scrollEnabled={this.state.scrollEnabled}
          {...this._panResponder.panHandlers}
        />
        {this.getDragElement.bind(this)()}
      </View>
    )
  }

  renderRow(num) {
    return (
      <TouchableOpacity
        style={styles.cell}
        onLongPress={this.handleLongPress.bind(this)}
        onPressIn={this.handlePressIn.bind(this)}
      >
        <Text style={styles.title}>{`Row ${num}`}</Text>
      </TouchableOpacity>
    );
  }

  handleLongPress(event) {
    console.log(event);
    this.setState({
      nativeEvent: event.nativeEvent,
      scrollEnabled: false,
    })
  }

  handlePressIn(event) {
    this._previousLeft = event.nativeEvent.pageX - 50;
    this._previousTop = event.nativeEvent.pageY - 20;
  }

  _updateNativeStyles() {
    this.floating && this.floating.setNativeProps({style: {left: this._previousLeft, top: this._previousTop}});
  }

  _handleStartShouldSetPanResponder(e, gestureState) {
    return true;
  }

  _handleMoveShouldSetPanResponder(e, gestureState) {
    return true;
  }

  _handlePanResponderMove(event, gestureState) {
    this._previousLeft = event.nativeEvent.pageX - 50;
    this._previousTop = event.nativeEvent.pageY - 20;
    this._updateNativeStyles();
  }

  _handlePanResponderEnd(e, gestureState) {
    this._previousLeft += gestureState.dx;
    this._previousTop += gestureState.dy;
    this.setState({ nativeEvent: undefined, scrollEnabled: true})
  }

}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    marginTop: 20,
  },
  cell: {
    flex: 1,
    height: 60,
    backgroundColor: '#d3d3d3',
    borderWidth: 3,
    borderColor: 'white',
    justifyContent: 'center',
  },
  title: {
    paddingLeft: 20,
  },
});

AppRegistry.registerComponent('LongPressDrag', () => LongPressDrag);

This is working for me with RN 0.29. I'm sure there's plenty of optimizations that could be done here, but I was just trying to illustrate the general concept in a quick morning of hacking at it.

I hope this helps!

like image 157
Michael Helvey Avatar answered Oct 05 '22 16:10

Michael Helvey