How can I create a drag and drop action in React native? - ios

Say I have two Views, A and B. I want to have the ability to trigger a 'dragAndDropStart' event on touching View A and then enable a drag and drop from A to B... displaying feedback to the user throughout (i.e. showing a line appearing between View A and the user's finger). On drop (releasing the drag gesture) I want to trigger another 'dragAndDropEnd' event, this time on View B.
The touchStart and touchEnd handlers are too limited as they don't appear to permit handoff of the gesture from one View to another. They also don't seem to enable the in-between 'drag' state.
The React native docs on using the gesture handlers is a little cryptic and I haven't seen any examples that demonstrate their use.
Any ideas?

export default class Viewport extends Component{
constructor(props){
super(props);
this.state = {
showDraggable : true,
dropZoneValues : null,
pan : new Animated.ValueXY()
};
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder : () => true,
onPanResponderMove : Animated.event([null,{
dx : this.state.pan.x,
dy : this.state.pan.y
}]),
onPanResponderRelease : (e, gesture) => {
if(this.isDropZone(gesture)){
this.setState({
showDraggable : false
});
}else{
Animated.spring(
this.state.pan,
{toValue:{x:0,y:0}}
).start();
}
}
});
}
isDropZone(gesture){
var dz = this.state.dropZoneValues;
return gesture.moveY > dz.y && gesture.moveY < dz.y + dz.height;
}
setDropZoneValues(event){
this.setState({
dropZoneValues : event.nativeEvent.layout
});
}
render(){
return (
<View style={styles.mainContainer}>
<View
onLayout={this.setDropZoneValues.bind(this)}
style={styles.dropZone}>
<Text style={styles.text}>Drop me here!</Text>
</View>
{this.renderDraggable()}
</View>
);
}
renderDraggable(){
if(this.state.showDraggable){
return (
<View style={styles.draggableContainer}>
<Animated.View
{...this.panResponder.panHandlers}
style={[this.state.pan.getLayout(), styles.circle]}>
<Text style={styles.text}>Drag me!</Text>
</Animated.View>
</View>
);
}
}
}
source https://moduscreate.com/blog/drag-and-drop-react-native/
https://github.com/crysfel/DragAndDrop

you have to see the current View Rectangle to other view Rectangle if one of your view rectangle intersect each other at some point it will return true then you get notify that the A view was drag to B view here is my example code may be it will help you .
-(void)moveViewWithGestureRecognizer:(UIPanGestureRecognizer *)panGestureRecognizer{
// your current View touch location suppose View A
CGPoint touchLocation = [panGestureRecognizer locationInView:self.contentView];
CGRect movingAViewRect = CGRectMake(touchLocation.x, touchLocation.y, self.aView.width, self.aView.height);
// NSLog(#"Point not Matched first => %# and second => %#",NSStringFromCGPoint(touchLocation),NSStringFromCGPoint(self.bView.frame.origins));
self.aView.center = touchLocation;
if(panGestureRecognizer.state == UIGestureRecognizerStateEnded)
{
//All fingers are lifted.
if(CGRectIntersectsRect(movingAViewRect,self.bView.frame)){
NSLog(#"Point Matched first => %# and second => %#",NSStringFromCGRect(movingAViewRect),NSStringFromCGRect (self.bView.frame ));
// and here you can perform some action for this
}else{
NSLog(#"aView is not drag on bView please drag aView to bView ");
}
}
}

Related

How to make a TouchableOpacity or TouchableHighlight clickable on devices that support 3d touch (force touch)

I've got this code:
<View {...this.panGesture.panHandlers}>
<Animated.View>
<View style={{ position: 'absolute' }}>
<TouchableHighlight onPress={ () => {console.log('hello')}>
</TouchableHighlight>
</View>
</Animated.View>
</View>
and I can't for the love of mine get the onPress to work for iPhones 6s and above, when 3d touch is enabled.
I've tried most solutions suggested here, but with no luck.
I'd appreciate any help at all!
I have been able to workaround this specific issue by making sure the parent PanResponder doesn't grab the responder on move, until the touch has actually moved from the origin:
PanResponder.create({
//... other responder callbacks
onMoveShouldSetPanResponder(e, gestureState) {
if (gestureState.dx === 0 || gestureState.dy === 0) {
return false;
}
return true;
}
});
This was back in around React Native 0.10 days, haven't tried it since. Hope it helps!
Ok after fiddling a lot with this, I realised the solution was partly what jevakallio said BUT I can no longer use Touchables when 3D touch is enabled so I have to reproduce their behaviour manually.
My code now looks like this:
The child code:
let touchable;
if (View.forceTouchAvailable === false) { // if 3d touch is disabled
// just use a Touchable and all should be fine.
touchable = (<TouchableHighlight
onPress={ () => {console.log('hello world');}
style={this.props.style}
underlayColor={this.props.highlightBgColor}
>
{this.props.children}
</TouchableHighlight>);
} else { // Otherwise if 3D touch is enabled
// we'll have to use a child view WITH a PanResponder
touchable = (<View
style={[this.props.style, {
backgroundColor: this.state.panViewIsTapped ?
this.props.highlightBgColor
:
bgColor,
}]}
{...this.panResponderChild.panHandlers}
>
{this.props.children}
</View>);
}
return (<View {...this.panResponderParent.panHandlers}>
<Animated.View>
<View style={{ position: 'absolute' }}>
{touchable}
</View>
</Animated.View>
</View>);
The child pan responder code (this.panResponderChild):
this.panResponderChild = PanResponder.create({
// true because we want tapping on this to set it as the responder
onStartShouldSetPanResponder: () => true,
// true because we want this to capture the responder lock from it's parent on start
onStartShouldSetPanResponderCapture: () => true,
// when the pan responder lock is terminated, set the pan view as NOT tapped
onPanResponderTerminate: () => {
this.setState({ panViewIsTapped: false });
},
// true so that the parent can grab our responder lock if he wan'ts to.
onPanResponderTerminationRequest: () => true,
// false because we DON'T want this btn capturing the resp lock from it's parent on move
onMoveShouldSetPanResponderCapture: () => false,
// false because we DON'T want moving the finger on this to set it as the responder
onMoveShouldSetPanResponder: () => false,
onPanResponderGrant: () => {
this.setState({ panViewIsTapped: true });
},
onPanResponderRelease: () => {
this.setState({ panViewIsTapped: false });
console.log('hello world');
},
})
The parent pan responder code (this.panResponderParent):
this.panResponderParent = PanResponder.create({
// true because we want tapping on the cal, to set it as a responder
onStartShouldSetPanResponder: () => true,
// false because we DON'T want to grab the responder lock from our children on start
onStartShouldSetPanResponderCapture: () => false,
/*
onMoveShouldSetPanResponderCapture:
false because we don't want to accidentally grab the responder lock from
our children on movement.
That's because sometimes even a small tap contains movement,
and thus a big percentage of taps will not work.
Keeping that flag false does not nessecarily mean that our children will
always capture the responder lock on movement, (they can if they want),
we're just not strict enough to grab it from them.
*/
onMoveShouldSetPanResponderCapture: () => false,
/*
onMoveShouldSetPanResponder:
We DO want moving the finter on the cal, to set it as a responder,
BUT, we don't always want moving the finger on an appointment setting this parent
as the responder.
Logic:
So, when the dx AND dy of the pan are 0, we return false, because we don't
want to grab the responder from our appointment children.
For anything other than that we just allow this parent to become the responder.
(dx, dy: accumulated distance of the gesture since the touch started)
*/
onMoveShouldSetPanResponder: (e, gestureState) =>
!(gestureState.dx === 0 || gestureState.dy === 0),
});

react native: How to listen to component position?

I tried to play with different kind of position callbacks but nothing fit my needs.
onLayout doesn't update
measure() with setInterval is too heavy
onPanResponderMove blocks child gestures
I want to retrieve Scene component position during navigator transition.
To be more precise, here's a sample : https://rnplay.org/apps/53SXZg. How to retrieve dragged scene position?
Found it using NavigationTransitionner of NavigatorExperimental. Thanks jered. Listening to transitionProps.position AnimatedValue :
_render(transitionProps: NavigationTransitionProps,): React.Element<any> {
const scenes = transitionProps.scenes.map((scene) => {
const sceneProps = {
...transitionProps,
scene,
};
return this._renderScene(sceneProps);
});
// Add position listener
if(!this.positionListener){
this.positionListener = transitionProps.position.addListener(({value}) => {
console.log('value', value);
});
}
return (
<View style={styles.navigator}>
{scenes}
</View>
);
}

How to get content height of ScrollView component for contentOffset prop in React Native?

For my current project, I'm creating a Diary/Calendar type component that shows the current day (Diary view) or current month (Calendar view) centered when the user clicks to see that view.
I'm using a ScrollView to hold my content:
_getInitialOffset(h) {
const FIX = 75; //TODO: Make it less arbitrary
let percentToScroll = (this.props.viewedDate.month()+1)/12; //12 = number of months per year
let { height } = this.props;
let centerFix = height/2;
let contentHeight = h;
let yPos = contentHeight*percentToScroll - centerFix - FIX;
return (yPos > 0) ? yPos : 0;
}
render() {
var year = this.props.viewedDate.year();
var cal = Calendar.get(year);
var contentHeight = this.contentHeight;
return (
<View>
<View style={styles.daysOfWeek}>
{
Calendar.days().map(function(day, i) {
return <Text key={i} style={styles.dayHeader}>{day}</Text>;
})
}
</View>
<ScrollView
ref={ref => {this._scrollView = ref}}
style={{
height: this.props.height,
paddingTop: 15
}}
onContentSizeChange={(w, h) => {
this._scrollView.scrollTo({y: this._getInitialOffset(h), animated: false});
}}>
<Year year={year} calendar={cal}/>
</ScrollView>
</View>
);
}
I'm trying to have it center upon render on the current month, but because my current method (using OnContentSizeChange) occurs after render, there's a frame where the user sees it uncentered, which is bad user experience.
Is there a way to get the content height of a ScrollView component before/during render, or delay component visibility until after the onContentSizeChange method has fired?
onContentSizeChange is internally using onLayout which is triggered as soon as the layout has been computed.
This event is fired immediately once the layout has been calculated, but the new layout may not yet be reflected on the screen at the time the event is received, especially if a layout animation is in progress.
There's no way to get the size before that.
So what you can do is set the opacity of your scrollview to 0 until the first onContentSizeChange is triggered.
I advise you to use Animated.Value to do the opacity change so it doesn't re-render your whole component.
My solution is as follows:
Create a parent container, with scrollOffset animation that tracks the amount of scroll.
Get the height of the ScrollView container ("containerHeight")
Get the height of the contents within the ScrollView ("contentHeight")
Use the ScrollView's "onScroll" event to get the scroll offset. Use it to update the scrollOffset animation.
Create a ScrollBar component based on the scrollOffset animation value, containerHeight, and contentHeight
The following is extracted from my code. It is incomplete and untested, but should be enough to for one to get started.
class CustomScrollView extends React.Component {
constructor(props) {
super(props);
this.state = {
scrollOffsetAnim: new Animated.Value(0)
};
}
renderScrollBar() {
const {
scrollOffsetAnim,
contentHeight,
containerHeight,
} = this.state;
//...
}
render() {
return (
<View>
<ScrollView
scrollEventThrottle={16}
onScroll={(evt) => {
const {contentOffset} = evt.nativeEvent;
const {y} = contentOffset;
this.state.scrollOffsetAnim.setValue(y);
}}
onLayout={(evt) => {
const {height} = evt.nativeEvent.layout;
const {containerHeight} = this.state;
if(!containerHeight || containerHeight !== height) {
this.setState({containerHeight: height})
}
}}
>
<View
onLayout={(evt) => {
const {height} = evt.nativeEvent.layout;
const {contentHeight} = this.state;
if(!contentHeight || contentHeight !== height) {
this.setState({contentHeight: height})
}
}}
>
{this.props.children}
</View>
</ScrollView>
{this.renderScrollBar()}
</View>
);
}
}
If you are trying to get the total height of the insides of a scrollview, just add a generic View with the onLayout prop inside it:
<ScrollView style={{flex:1}} >
<View style ={{flex:1}} onLayout={e=>console.log('e',e.nativeEvent.layout)}>
{YOUR VIEWS HERE}
</View>
</ScrollView>
Every time the component rerenders, it will print the console log ('e') with the informations you probably wishes

React-native: scrollview inside of panResponder

I am using a ScrollView inside of a PanResponder. On Android it works fine but on iOS the ScrollView will not scroll. I did some investigation and here are some facts:
If I put a break point in PanResponder.onMoveShouldSetPanResponder(), before I step over, the scrollView will scroll as normal but once I release the break point, the scrollView stops working.
If I modify ScrollResponder.js, and return true in scrollResponderHandleStartShouldSetResponderCapture() - it used to return false at runtime; and return false in scrollResponderHandleTerminationRequest(), the scrollView works OK but of course, since it swallows the event the outer PanResponder will not get the event.
So the questions are:
I want to make the scrollview to work, and not to swallow the event. Any one know what's the approach?
How the responding system works on iOS? The react-native responder system doc does not explain that to me.
To enable scrolling in a ScrollView that is a child of a parent with PanResponder, you have to make sure the ScrollView is the responder to any gesture inside of it. By default, gesture events bubble up from the deepest component to the parent component. In order to capture the event by the ScrollView, you can add a View with a PanResponder inside of it. See the (pseudo) example below, where ChildComponent is a child of a parent with PanResponder.
const ChildComponent = ({ theme }) => {
const panResponder = React.useRef(
PanResponder.create({
// Ask to be the responder:
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
})
).current;
return (
<ScrollView style={{ height: 500 }}>
<View {...panResponder.panHandlers}>
...
</View>
</ScrollView>
);
};
Within PanResponder is an event that returns the current touch position. You can use that to compare 2 values to perform a scroll.
I finally solve this by wrap the scrollview inside a view ,and set the style of scrollview a limited height.
import * as React from 'react';
import { Text, View, StyleSheet,PanResponder,Animated,ScrollView,Dimensions} from 'react-native';
import { Constants } from 'expo';
const WINDOW_WIDTH = Dimensions.get("window").width;
const WINDOW_HEIGHT = Dimensions.get("window").height;
// You can import from local files
export default class App extends React.Component {
constructor(props){
super(props);
this.minTop = 100;
this.maxTop = 500;
this.state={
AnimatedTop:new Animated.Value(0),
}
}
componentWillMount(){
let that = this;
this._previousTop = 100;
this._panResponder = PanResponder.create({
onMoveShouldSetPanResponder(){
return true;
},
onPanResponderGrant(){
that._previousTop = that.state.AnimatedTop.__getValue();
return true;
},
onPanResponderMove(evt,gestureState){
let currentTop = that._previousTop + gestureState.dy;
that.state.AnimatedTop.setValue(that._previousTop+gestureState.dy);
},
onPanResponderRelease(){
}
})
}
render() {
return (
<View style={styles.container}>
<Animated.View
style={[styles.overlay,{top:this.state.AnimatedTop}]}
{...this._panResponder.panHandlers}
>
<View style={{height:200,backgroundColor:"black"}}></View>
<View>
<ScrollView
style={{height:500}}
>
<View style={{backgroundColor:"blue",height:200}}></View>
<View style={{backgroundColor:"yellow",height:200}}></View>
<View style={{backgroundColor:"pink",height:200}}></View>
<View style={{backgroundColor:"red",height:200}}></View>
</ScrollView>
</View>
</Animated.View>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
backgroundColor: '#ecf0f1',
padding: 8,
},
overlay:{
position:"absolute",
width:WINDOW_WIDTH,
height:WINDOW_HEIGHT-100,
}
});
enter link description here
I spent plenty of time to solve this.Hope this will help someone confused with the same problem.
I don't know if it's usefull right now but you can add that line,
return !(gestureState.dx === 0 && gestureState.dy === 0)
in 'onMoveShouldSetPanResponder' property of PanResponder with evt and gestureState as parameters.
Your PanResponder should look like this :
this._panResponder = PanResponder.create({
onMoveShouldSetPanResponder(evt, gestureState){
return !(gestureState.dx === 0 && gestureState.dy === 0)
},
onPanResponderGrant(){
that._previousTop = that.state.AnimatedTop.__getValue();
return true;
},
onPanResponderMove(evt,gestureState){
let currentTop = that._previousTop + gestureState.dy;
that.state.AnimatedTop.setValue(that._previousTop+gestureState.dy);
},
onPanResponderRelease(){
}
})
I have solved this by adding onPress handlers to all of the contents of the ScrollView - doesn't matter if the handler is a no-op, the scroll view still works fine.
I'm not sure you still need this or not but I'll put to help others as well
If you put the panResponder on a sibling view of the ScrollView, the ScrollView behaves properly. then you can position that sibling view
here is an example of the workaround.
I solved this issue by doing it
onMoveShouldSetPanResponder: (event, gesture) => {
if (gesture?.moveX > gesture?.moveY) {
return false;
}
return true;
},
On my case, I have a horizontal Flatlist inside a PanResponder...

How do you detect if the right half of the screen has been tapped in react-native?

Originally I was going to create a view component that was the size of half the screen and wrap it in a TouchableHighlight, but that seems messy.
Have a look at the Gesture Responder system, which can let you set a view to react to a touch:
http://facebook.github.io/react-native/docs/gesture-responder-system.html#content
Specifically if you pass your view a onStartShouldSetResponder prop which is a function that returns true. You can then also pass a onResponderGrant function as a prop which will receive an event object with the details you need.
Fixed it.
I added a wrapper <View {...this.panResponder.panHandlers}> to the element and filled in the onPanResponderGrant function:
onPanResponderGrant: ({ nativeEvent: { touches } }, { x0, y0, moveX }) =>{
// if on right side of screen
if (x0 > (Dimensions.get('window').width / 2)){
_this.nextPhoto();
}
}

Resources