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:
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.
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
.
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!