react-native change responder dynamically
Asked Answered
H

1

10

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.

Heulandite answered 5/7, 2016 at 8:31 Comment(1)
Related: github.com/facebook/react-native/issues/7941Cele
R
9

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!

Raceway answered 16/7, 2016 at 15:34 Comment(11)
I’ve just tried your code snippet on Android (RN 0.29) and longPress isn’t working.Cele
I just ran it on Android (Nexus 5X emulator), and it worked fine. It just took a really long time for onLongPress to be fired (compared to iOS). So maybe you need to play around with the delayLongPress prop, or something. But that code snippet works fine on both platforms for me.Raceway
Ok thank you for testing @Michael Helvey. Unfortunately it still doesn’t work for me either on my physical device or even in the emulator, but I suspect this isn’t related to this particular snippet.Cele
(are you using RN 0.29?)Cele
Yep. I'm using RN 0.29. I'm sorry if I can't test it better -- I don't have a lot of native Android experience (I come from a native iOS background). But if I run it from my Android studio project it seems to work alright. Theoretically, the behavior of the onLongPress prop shouldn't be platform specific, according to the RN docs.Raceway
@Cele Any luck? It's an interesting question, and I'd like to resolve it, if I can.Raceway
I tested again today with RN 0.31-rc.0 and the long press event is still never fired. I suspect this is a bug with React Native, I guess I'll start working on a minimal reproduction if this isn't getting fixed in the next week or so.Cele
Huh. Good to know.Raceway
@MichaelHelvey, sorry for late reply, I got busy with another work and couldn't check that earlier. I think "When you're handling touches, you don't need the component being moved to actually be the one listening for touch events" is really the key. This is very helpful, thanks a lot!!Heulandite
@mquandalle, regarding issue of longPress not working, I have observed this issue before while chrome debugging is on. But with debugging off longPress works fine for me.Heulandite
@MichaelHelvey, given your awesome answer here, I'm thinking my question about moving the pan-responder is impossible. Any thoughts on a workaround for this one? https://mcmap.net/q/1166266/-in-react-native-how-do-you-make-an-interactive-tap-and-hold-modal-like-instagram/25197Clintclintock

© 2022 - 2024 — McMap. All rights reserved.