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.
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.
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With