First off, I'm new to asking stack overflow questions, so go easy on me guys.
I am trying to figure out how to detect collision of components; namely, a draggable component containing a button-circle component. Sadly, there appears to be zero information online regarding this. I'm thinking the draggable component may be able to handle some sort of radius tracking, which is computed when the user takes their finger off the screen, but hard tracking the radius would seem to be very resource intensive. I really need some ideas here guys. Relevant code is below.
Draggable.js
import React, { Component } from "react";
import {
PanResponder,
Animated,
} from "react-native";
import CircleButton from 'react-native-circle-button';
export default class Draggable extends Component {
constructor(props) {
super(props);
this.state = {
pan: new Animated.ValueXY(),
scale: new Animated.Value(1),
};
}
//Function to handle animations and button pressing of bubbles.
componentWillMount() {
let finalx = 0;
let finaly = 0;
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => false,
onMoveShouldSetPanResponder: (evt, gestureState) => {
return gestureState.dx <= finalx && gestureState.dy <= finaly;
},
onMoveShouldSetPanResponderCapture: (evt, gestureState) => {
return gestureState.dx <= finalx && gestureState.dy <= finaly;
},
onPanResponderGrant: (e, gestureState) => {
this.state.pan.setValue({x: 0, y: 0});
this.finalx = gestureState.dx;
this.finaly = gestureState.dy;
Animated.spring(
this.state.scale,
{ toValue: 1.1, friction: 3 }
).start();
},
onPanResponderMove: Animated.event([
null, {dx: this.state.pan.x, dy: this.state.pan.y},
]),
onPanResponderRelease: (e, {vx, vy}) => {
this.state.pan.flattenOffset();
Animated.spring(
this.state.pan,
{ toValue: {x:0, y:0}},
this.state.scale,
{ toValue: 1, friction: 3 }
).start();
}
});
}
render() {
let { pan } = this.state;
let rotate = '0deg';
let [translateX, translateY] = [pan.x, pan.y];
let moveableStyle = {transform: [{translateX}, {translateY}, {rotate}]};
const panStyle = {
transform: this.state.pan.getTranslateTransform()
}
return (
<Animated.View
{...this._panResponder.panHandlers}
style={[moveableStyle, panStyle]}>
<CircleButton />
</Animated.View>
);
}
}
Homepage.js
<View style={styles.middleContainer}>
{ this.state.bubbles.map((item, key)=>(
<Draggable key={key}> { item }</Draggable>)
)}
</View>
Related
I am using this react code for getting a custom cursor following the main cursor. animation is done using gsap and lerp function. But the animation is not seamless and the chrome performance monitor shows the CPU usage is passing 100%. pls help me figure out this problem. I have referred to this video link for getting the cursor animation: https://www.youtube.com/watch?v=MEO6yQLAgKw&list=PLtSHrBhMos7hXeImRWnnC38mdYp_4c333&index=2&t=630s
import gsap from 'gsap';
import React, {Component} from 'react';
import './cursor.scss';
class Cursor extends Component{
constructor(props){
super(props);
this.state={
x : 0,
y : 0
};
this.cursor = React.createRef();
this.cursorConfigs = {
x: { previous: 0, current: 0, amt: 0.2 },
y: { previous: 0, current: 0, amt: 0.2 },
};
this.lerp = (a, b, n) => (1 - n) * a + n * b;
}
componentDidMount(){
window.addEventListener("mousemove", e=> {
this.setState({
x: e.pageX,
y:e.pageY
})
});
this.cursor.current.style.opacity = 0;
this.onMouseMoveEv = () => {
this.cursorConfigs.x.previous = this.cursorConfigs.x.current = this.state.x;
this.cursorConfigs.y.previous = this.cursorConfigs.y.current = this.state.y;
gsap.to(this.cursor.current,{
duration: 1,
ease: "Power4.easeOut",
opacity: 1,
});
window.removeEventListener("mousemove", this.onMouseMoveEv);
requestAnimationFrame(() =>this.render());
};
window.addEventListener("mousemove", this.onMouseMoveEv);
}
render(){
this.cursorConfigs.x.current = this.state.x;
this.cursorConfigs.y.current = this.state.y;
for (const Key in this.cursorConfigs){
this.cursorConfigs[Key].previous = this.lerp(
this.cursorConfigs[Key].previous,
this.cursorConfigs[Key].current,
this.cursorConfigs[Key].amt
);
}
console.log(this.cursorConfigs.x.previous, this.cursorConfigs.x.current)
var styles = {
transform:`translateX(${this.cursorConfigs.x.previous}px) translateY(${this.cursorConfigs.y.previous}px)`
}
requestAnimationFrame(() =>this.render());
return(
<div className="cursor" ref={this.cursor} style={styles}>
<div className="cursor-media">
</div>
</div>
)
}
}
export default Cursor;```
You actualy don't need to update on every mousemove. Consider debounce setState
example:
// delay in ms
function debounced(delay, fn) {
let timerId;
return function (...args) {
if (timerId) {
clearTimeout(timerId);
}
timerId = setTimeout(() => {
fn(...args);
timerId = null;
}, delay);
};
}
function handler(e) {
this.setState({
x: e.pageX,
y: e.pageY,
});
};
window.addEventListener("mousemove", debounced(200, handler));
React Native documents give an example using PanResponder where a property is set using "._value".
What does "._value" do?
const pan = useRef(new Animated.ValueXY()).current;
const panResponder = useRef(
PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onPanResponderGrant: () => {
pan.setOffset({
x: pan.x._value, // What do you do?
y: pan.y._value // You too!
});
},
onPanResponderMove: Animated.event(
[
null,
{
dx: pan.x,
dy: pan.y
}
]
),
onPanResponderRelease: () => {
pan.flattenOffset();
}
})
).current;
Using react-native, I'm creating sub-Components within the parent App and providing their position to the array this.state.objLocation within the parent App.
I can get the initial location data into the array straight after the render, but because my subcomponents are draggable, each time they re-render on drag, it adds a new position object to the array.
I'd like to avoid this, and I thought that creating this.state = { firstRender: true } in the constructor and then using componentDidMount = () => { this.setState({ firstRender: false }) } after the first render would allow me to create a 'gate' to stop the addition of the extra position objects.
I can see that if I comment out //componentDidMount = () => { this.setState({ firstRender: false }) } then I will get multiple entries to my array but if it's included in the class I get absolutely none.
So possibly my interpretation of the render lifecycle and componentDidMount is incorrect?
Here is my code.
// App
import React, { Component } from 'react';
import { View, Text, } from 'react-native';
import styles from './cust/styles';
import Draggable from './cust/draggable';
const dataArray = [{num: 1,id: 'A',},{num: 2,id: 'B',},{num: 3,id: 'Z',}]
export default class Viewport extends Component {
constructor(props){
super(props);
this.state = {
dID : null,
objLocation: [],
firstRender: true,
};
}
render(){
return (
<View style={styles.mainContainer}>
<View style={styles.draggableContainer}>
<Text>Draggable Container</Text> {dataArray.map( d => { return(
<Draggable
id={d.id}
onLayout={ e=> this.onLayout(e)}
onPanResponderGrant={(dID) =>this.setState({ dID })}
onPanResponderRelease={() => this.setState({dID: null})} /> ) })}
<View style={[styles.findPoint ]} />
</View>
<View style={styles.infoBar}>
<Text>{this.state.dID ? this.state.dID : ''}</Text>{this.compFrame()}
</View>
</View>
);
}
onLayout = (e) => {
if ( e && this.state.firstRender) {
const n = e.nativeEvent.layout;
const position = {
width: n.width,
height: n.height,
x: n.x,
y: n.y
}
console.log(position);
this.setState({
objLocation: this.state.objLocation.concat([position])
});
}
}
componentWillMount = () => {
console.log("START");
}
compFrame = () => {
return(
this.state.objLocation.map( d => {<View style={[styles.findPoint2,{left: d.x, top: d.y, width: d.width, height: d.height} ]} ></View>})
)
}
componentDidMount = () => {
this.setState({firstRender: true })
console.log(this.state.objLocation.length);
}
}
// Draggable
import React, { Component } from 'react';
import { Text, PanResponder, Animated } from 'react-native';
import styles from './styles';
class Draggable extends Component {
constructor(props) {
super(props);
this.state = {
pan: new Animated.ValueXY(),
};
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderGrant: () => {
this.props.onPanResponderGrant(this.props.id);
},
onPanResponderMove: Animated.event([ null, {
dx: this.state.pan.x,
dy: this.state.pan.y,
},
]),
onPanResponderRelease: () => {
Animated.spring(this.state.pan, { toValue: { x: 0, y: 0 } }).start();
this.props.onPanResponderRelease();
},
});
}
render() {
return (
<Animated.View
onLayout={ (e) => this.props.onLayout(e) }
{...this.panResponder.panHandlers}
style={[this.state.pan.getLayout(), styles.circleAlt, styles.position]}>
<Text style={styles.textAlt}>Drag me!</Text>
<Text style={styles.textNum}>{this.props.id}</Text>
</Animated.View>
);
}
componentDidMount = () => {
this.props.onLayout(this.props.dragEvent)
}
}
export default Draggable;
// Output of console.log
START xxx
0
{width:108,height:108,x:133.5,y:376.5}
{width:108,height:108,x:133.5,y:78.5}
{width:108,height:108,x:133.5,y:227.5}
You could set the firstRender state in onLayout function
onLayout = (e) => {
if ( e && this.state.firstRender) {
const n = e.nativeEvent.layout;
const position = {
width: n.width,
height: n.height,
x: n.x,
y: n.y
}
console.log(position);
this.setState({
firstRender: false,
objLocation: this.state.objLocation.concat([position])
});
}
}
According to the information provided by you, your onLayout function is called by the component so its not included in the component lifecycle process, so when the component completes its lifecycle it goes into componentDidMount after mounting (which is not calling onLayout func) & thus changed the firstRender state to false and hence when you drag the component each time it goes from true to false.
I hope this explains
I feel like I've hacked this, to get it to work, so please correct me as to correct procedure.
This is the onLayout method from the App. I've included an if statement that checks if the new positions array length is equal too the dataArray length that the draggable items are based on.
It looks like this.
onLayout = (e) => {
if ( this.state.objLocation.length != dataArray.length ) {
if ( e ) {
const n = e.nativeEvent.layout;
const position = {
width: n.width,
height: n.height,
x: n.x,
y: n.y
}
console.log(position);
this.setState({
objLocation: this.state.objLocation.concat([position])
});
}
}
}
I am trying to enable panResponder to move a component through the screen in a drag and drop mode. However this drag and drop must be initiated through a longPress on such element.
longPress captures the event and so when panResponder is enabled onStartShouldSetPanResponder => this.state.panEnabled we need to press again.
I would like to fire a native event, or, else, activate the drag without pressing again. Is it possible to re-emit a native event? Can we pass it to the panResponder in any way?
Thanks!
Ive got this working without using longTouch and instead just using the panResponder (same effect as the longTouch though), take a look:
import React, { Component } from 'react';
import {
View,
PanResponder
} from 'react-native';
const LONGPRESS_TIMER = 300;
class DraggableComponent extends Component {
constructor() {
super();
this.timer = null;
this.state = {
canMove: false
}
this._panResponder = this.setUpPanResponder();
}
setUpPanResponder() {
const self = this;
return PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
onPanResponderGrant: (evt, gestureState) => {
self.timer = setTimeout(() => {
self.setState({canMove: true});
}, LONGPRESS_TIMER);
},
onPanResponderMove: (evt, gestureState) => {
if(this.state.canMove) {
console.log('moving', gestureState);
// code to move view here
}
},
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderRelease: (evt, gestureState) => {
clearTimeout(self.timer);
self.setState({canMove: false});
}
});
}
render() {
return <View
style={{
width: 100,
height: 100,
backgroundColor: this.state.canMove ? 'green' : 'blue'}}
{...this._panResponder.panHandlers} />
}
}
You can adjust the time it takes to trigger the "canMove" state by changing the LONGPRESS_TIMER const, Hope it helps!
I have a card game where the user can drag cards around a screen.
How can I detect if the cards have been dragged over other View components? My draggable card code is like this:
// A draggable card
// drag drop code example used: https://github.com/brentvatne/react-native-animated-demo-tinder
// panresponder docs: https://facebook.github.io/react-native/docs/panresponder.html
class Card extends React.Component {
componentWillMount() {
this.state = {
pan: new Animated.ValueXY(),
enter: new Animated.Value(1),
}
this._panResponder = PanResponder.create({
onMoveShouldSetResponderCapture: () => true,
onMoveShouldSetPanResponderCapture: () => true,
onPanResponderGrant: (e, gestureState) => {
Animated.spring(this.state.enter, {
toValue: .75,
}).start()
this.state.pan.setOffset({x: this.state.pan.x._value,
y: this.state.pan.y._value});
this.state.pan.setValue({x: 0, y: 0});
},
onPanResponderMove: Animated.event([
null, {dx: this.state.pan.x, dy: this.state.pan.y},
]),
onPanResponderRelease: (e, {vx, vy}) => {
// do stuff when card released
}
Animated.spring(this.state.enter, {
toValue: 1,
}).start()
this.state.pan.flattenOffset();
var velocity;
if (vx >= 0) {
velocity = clamp(vx, 3, 5);
} else if (vx < 0) {
velocity = clamp(vx * -1, 3, 5) * -1;
}
Animated.spring(this.state.pan, {
toValue: {x: 0, y: toValue},
friction: 4
}).start()
}
})
}
I don't know if this would be the best way to go about it, but I would just get the onLayout of the target Views to get the x,y,width, and height...
<View onLayout={({nativeEvent: {layout: {x,y,width,height}}) => { })} />
The coordinates covered by the container would simply be (x, x+width) and (y, y+height)
onPanResponderGrant get the location of the draggable:
onPanResponderGrant: (e) => {
this.originX = e.pageX - e.locationX;
this.originY = e.pageY - e.locationY;
}
onPanResponderMove you can do something like this:
onPanResponderMove: (e, gestureState) => {
const currentX0 = this.originX + gestureState.dx
const currentY0 = this.originY + gestureState.dy
}
The draggable now covers from currentX0 to currentX0 + width and from currentY0 to currentY0 + height...
You can use these values to check to see whether it overlaps with the target view (x, x+width) and (y, y+height)