React Native Pan Responder to Animate single dynamic view - react-native

I'm creating a series of cubes within a grid and I want to only move the cube that is pressed upon. Currently in my code it is moving all the cubes as one. I tried finding the individual key of the cube to try and target it for animation, but I'm not having any luck so far in figuring this out.
The myPanel component is just to layout the cubes that are defined in the renderCube function. It may be an issue with laying out all of the cubes with Animated.View, even though I added the key, but I can't access or figure out how to specifically animate only the selected cube.
import React, { Component } from 'react';
import { TouchableWithoutFeedback, View, ImageBackground, Dimensions, Animated, PanResponder } from 'react-native';
import myPanel from './myPanel';
const DATA = [
{ id: 1, color: '#ff8080' },
{ id: 2, color: '#80ff80' },
{ id: 3, color: '#ffff80' }
];
const winWidth = Dimensions.get('window').width;
const widthCalc = winWidth/400;
const cubeWidth = widthCalc*74;
const winHeight = Dimensions.get('window').height;
const heightCalc = ((winHeight-winWidth)/2)-5;
const xPush = 5*widthCalc;
const borRadius = 7.4*widthCalc;
let cnt = 0;
let xMove = 0;
let yMove = 0;
let currTarget = '033';
class Board extends Component {
constructor(props) {
super(props);
this.state = {
pan: new Animated.ValueXY()
};
}
componentWillMount() {
this._panResponder = PanResponder.create({
onMoveShouldSetResponderCapture: () => true,
onMoveShouldSetPanResponderCapture: () => true,
onStartShouldSetPanResponder:(e, gestureState) => true,
onPanResponderGrant: (e, gestureState) => {
console.log(this.currTarget);
},
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();
}
});
}
renderCube(item) {
return (
<Animated.View {...this._panResponder.panHandlers}
style={this.state.pan.getLayout()} key={item.id}>
<TouchableWithoutFeedback
onPressIn={()=>{this.currTarget = item.id; console.log('Set '+item.id);}}>
<View
style={{
backgroundColor:myColor,
width:cubeWidth,
height:cubeWidth,
position: 'absolute',
top: yMove,
left: xMove,
bottom: 0,
right: 0,
borderRadius:borRadius
}}
></View>
</TouchableWithoutFeedback>
</Animated.View>
);
}
render() {
return (
<myPanel
data={DATA}
renderCube={this.renderCube.bind(this)}
/>
);
}
}
export default Board;

Related

Reanimated 2 reusable animation in custom hook

How can I create a reusable React hook with animation style with Reanimated 2? I have an animation that is working on one element, but if I try to use the same animation on multiple elements on same screen only the first one registered is animating. It is too much animation code to duplicate it everywhere I need this animation, so how can I share this between multiple components on the same screen? And tips for making the animation simpler is also much appreciated.
import {useEffect} from 'react';
import {
cancelAnimation,
Easing,
useAnimatedStyle,
useSharedValue,
withRepeat,
withSequence,
withTiming,
} from 'react-native-reanimated';
const usePulseAnimation = ({shouldAnimate}: {shouldAnimate: boolean}) => {
const titleOpacity = useSharedValue(1);
const isAnimating = useSharedValue(false);
useEffect(() => {
if (shouldAnimate && !isAnimating.value) {
isAnimating.value = true;
titleOpacity.value = withRepeat(
withSequence(
withTiming(0.2, {duration: 700, easing: Easing.inOut(Easing.ease)}),
withTiming(
1,
{duration: 700, easing: Easing.inOut(Easing.ease)},
() => {
if (!shouldAnimate) {
cancelAnimation(titleOpacity);
}
},
),
),
-1,
false,
() => {
if (titleOpacity.value < 1) {
titleOpacity.value = withSequence(
withTiming(0.2, {
duration: 700,
easing: Easing.inOut(Easing.ease),
}),
withTiming(
1,
{duration: 700, easing: Easing.inOut(Easing.ease)},
() => {
isAnimating.value = false;
},
),
);
} else {
titleOpacity.value = withTiming(
1,
{
duration: 700,
easing: Easing.inOut(Easing.ease),
},
() => {
isAnimating.value = false;
},
);
}
},
);
} else {
isAnimating.value = false;
cancelAnimation(titleOpacity);
}
}, [shouldAnimate, isAnimating, titleOpacity]);
const pulseAnimationStyle = useAnimatedStyle(() => {
return {
opacity: titleOpacity.value,
};
});
return {pulseAnimationStyle, isAnimating: isAnimating.value};
};
export default usePulseAnimation;
And I am using it like this inside a component:
const {pulseAnimationStyle} = usePulseAnimation({
shouldAnimate: true,
});
return (
<Animated.View
style={[
{backgroundColor: 'white', height: 100, width: 100},
pulseAnimationStyle,
]}
/>
);
The approach that I've taken is to write my Animations as wrapper components.
This way you can build up a library of these animation components and then simply wrap whatever needs to be animated.
e.g.
//Wrapper component type:
export type ShakeProps = {
// Animation:
children: React.ReactNode;
repeat?: boolean;
repeatEvery?: number;
}
// Wrapper component:
const Shake: FC<ShakeProps> = ({
children,
repeat = false,
repeatEvery = 5000,
}) => {
const shiftY = useSharedValue(0);
const animatedStyles = useAnimatedStyle(() => ({
//Animation properties...
}));
const shake = () => {
//Update shared values...
}
// Loop every X seconds:
const repeatAnimation = () => {
shake();
setTimeout(() => {
repeatAnimation();
}, repeatEvery);
}
// Start Animations on component Init:
useEffect(() => {
// Run animation continously:
if(repeat){
repeatAnimation();
}
// OR ~ call once:
else{
shake();
}
}, []);
return (
<Animated.View style={[animatedStyles]}>
{children}
</Animated.View>
)
}
export default Shake;
Wrapper Component Usage:
import Shake from "../../util/animated-components/shake";
const Screen: FC = () => {
return (
<Shake repeat={true} repeatEvery={5000}>
{/* Whatever needs to be animated!...e.g. */}
<Text>Hello World!</Text>
</Shake>
)
}
From their docs:
CAUTION
Animated styles cannot be shared between views.
To work around this you can generate multiple useAnimatedStyle in top-level loop (number of iterations must be static, see React's Rules of Hooks for more information).
https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

how to drag the view in react native

I have an example code I put it on below ,this example show the circle to drag another location
import React, { Component } from "react";
import {
StyleSheet,
View,
PanResponder,
Animated
} from "react-native";
export default class Draggable extends Component {
constructor() {
super();
this.state = {
pan: new Animated.ValueXY()
};
}
componentWillMount() {
// Add a listener for the delta value change
this._val = { x:0, y:0 }
this.state.pan.addListener((value) => this._val = value);
// Initialize PanResponder with move handling
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: (e, gesture) => true,
onPanResponderMove: Animated.event([
null, { dx: this.state.pan.x, dy: this.state.pan.y }
])
// adjusting delta value
this.state.pan.setValue({ x:0, y:0})
});
}
render() {
const panStyle = {
transform: this.state.pan.getTranslateTransform()
}
return (
<Animated.View
{...this.panResponder.panHandlers}
style={[panStyle, styles.circle]}
/>
);
}
}
let CIRCLE_RADIUS = 30;
let styles = StyleSheet.create({
circle: {
backgroundColor: "skyblue",
width: CIRCLE_RADIUS * 2,
height: CIRCLE_RADIUS * 2,
borderRadius: CIRCLE_RADIUS
}
});
this code the circle to the drag but I have drag another location it goes on the starting location so have to set the current location value so please tell me

after onPanResponderRelease,auto animation moving does not work correctly

My sample code is to auto move a distance after onPanResponderRelease of PanResponder(looks like a object moving inertia).
When I drag the content and release finger(or release dragging in simulator), the content works correctly(it moved a distance automaticlly).
But when I touch quickly(or click once in simulator),the content moves a distance and back to other position.It has problem because the content should not move with a quick touching.
What's the problem?
Here is my code(it can directly paste and run in app.js).
import React from 'react';
import { View, StyleSheet, PanResponder, Animated, Text, Button, } from 'react-native';
export default class App extends React.Component {
constructor(props) {
super(props);
this._layout = { x: 0, y: 0, width: 0, height: 0, };
this._value = { x: 100, y: 100, };
}
componentWillMount() {
this._animatedValue = new Animated.ValueXY({ x: 100, y: 100 });
this._animatedValue.addListener((value) => {
this._value = value;
});
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => ((gestureState.dx != 0) && (gestureState.dy != 0)),
onPanResponderGrant: (e, gestureState) => {
this._animatedValue.setOffset({ x: this._value.x, y: this._value.y });
},
onPanResponderMove: Animated.event([
null,
{ dx: this._animatedValue.x, dy: this._animatedValue.y },
]),
onPanResponderRelease: () => {
this._animatedValue.flattenOffset();
Animated.timing(
this._animatedValue,
{
toValue: { x: this._value.x, y: this._value.y + 100, },
duration: 600,
}
).start(() => {
// animation finished
});
}
});
}
render() {
return (
<View style={styles.container}>
<Animated.View
style={[
styles.box,
{
left: this._animatedValue.x,
top: this._animatedValue.y,
},
]}
{...this._panResponder.panHandlers}
>
<Text>{'This is a test'}</Text>
<Button
title={'Click Me'}
onPress={() => console.log('Yes, I clicked.')}
/>
</Animated.View>
</View>
);
}
}
var styles = StyleSheet.create({
container: {
flex: 1,
},
box: {
width: 200,
height: 200,
borderWidth: 1,
},
});
Thanks a lot!
I'm not sure how much this matters but you are not adding the event listener in the suggested way: https://facebook.github.io/react-native/docs/animated#event
Try adding it like this:
componentWillMount() {
this._animatedValue = new Animated.ValueXY({ x: 100, y: 100 });
// REMOVE THIS
//this._animatedValue.addListener((value) => {
// this._value = value;
//});
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => ((gestureState.dx != 0) && (gestureState.dy != 0)),
onPanResponderGrant: (e, gestureState) => {
this._animatedValue.setOffset({ x: this._value.x, y: this._value.y });
},
onPanResponderMove: Animated.event([
null,
{ dx: this._animatedValue.x, dy: this._animatedValue.y },
],
{
SEE HERE--->>> listener: this.onMove
}
),
onPanResponderRelease: () => {
this._animatedValue.flattenOffset();
Animated.timing(
this._animatedValue,
{
toValue: { x: this._value.x, y: this._value.y + 100, },
duration: 600,
}
).start(() => {
// animation finished
});
}
});
}
AND SEE HERE --->>>
onMove() {
var { x, y } = this.animatedValue;
this._value.x = x;
this._value.y = y;
}
Also try making your Animated.View position:absolute
Now, it looks like the code is doing this:
Set 'Animated offset' to be equal to 'value offset' (onPanResponderGrant)
Set the 'Animated offset' to be equal to 'dx' 'dy' (onPanResponderMove)
set 'value' to be equal to 'Animated value' (Event Listener)
set the 'animated value' to be equal to 'value' (onPanResponderRelease)
Between steps one and two you are setting the offset twice without flattening (again, not sure how much that matters).
Between steps three and four you are setting 'value offset' equal to 'Animated Value offset' and then 'Animated value offset' to be equal to 'Value offset' again - seems redundant
I wrote a new version and got the result.
import React, { Component } from 'react';
import {
StyleSheet, View, Text, Animated, PanResponder,
} from 'react-native';
export default class App extends Component {
constructor(props) {
super(props);
this._value = { x: 0, y: 0 };
this.pan = new Animated.ValueXY({ x: 0, y: 0 });
}
componentWillMount() {
this.pan.addListener((value) => {
this._value = value;
});
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => ((gestureState.dx != 0) && (gestureState.dy != 0)),
onPanResponderGrant: (e, gestureState) => {
this.pan.setOffset({ x: this._value.x, y: this._value.y });
this.pan.setValue({ x: 0, y: 0 });
},
onPanResponderMove: Animated.event([null, {
dx: this.pan.x,
dy: this.pan.y
}]),
onPanResponderRelease: (e, gesture) => {
this.pan.flattenOffset();
const { x, y } = this._value;
Animated.timing(
this.pan,
{
toValue: { x, y: y + 50 },
duration: 600,
},
).start(() => {
// animation finished
});
}
});
}
componentWillUnmount() {
this.pan.removeAllListener();
}
render() {
return (
<View style={{ flex: 1 }}>
<View style={styles.draggableContainer}>
<Animated.View
{...this.panResponder.panHandlers}
style={[
this.pan.getLayout(),
styles.square,
]}>
<Text>Drag me!</Text>
</Animated.View>
</View>
</View>
);
}
}
let styles = StyleSheet.create({
draggableContainer: {
position: 'absolute',
top: 165,
left: 76.1,
},
square: {
backgroundColor: 'red',
width: 72,
height: 72,
borderWidth: 1,
}
});
the previous version has some problems because I didn't know the react-native animation.
there are several points about the codes:
1)animation view should be wrapped by a position:absolute view.
2)the animation value/valueXY is a relative position.
3)addListener works right.
4)valueXY is a combine object with two value object.
but the addListener param is an object with x/y, like this: {x:0,y:0}
(So, valueXY and listener param cannot assign directly)
5)animation will add the offset of value.
after flattenOffset, the offset will be added to value.
flattenOffset

Drag a button in React Native and on drop, return to original position

In my React Native app, I have buttons that you can drag into a drop zone. If a certain condition is met by the drop zone, the data in the drop zone will change, but if it's not met, I would like the draggable return to its original position. Maybe I'm missing it, but there doesn't seem to be a property that retains the original position. Is there a simple way of doing this? I'm sure I can find a roundabout way of doing this, but it seems like I'm missing something.
Can anyone help? Below is the component I'm working with.
class AddTile extends Component {
constructor(props) {
super(props);
this.state = {
pan: new Animated.ValueXY(),
scale: new Animated.Value(1),
showDraggable : true,
dropZoneValues : null,
moveValues: null,
showView: true,
};
}
_onPressButton() {
Alert.alert('You tapped the button!')
}
removeButton() {
this.setState({
showView: false,
});
}
componentWillMount() {
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder : () => true,
onMoveShouldSetResponderCapture: () => true,
onMoveShouldSetPanResponderCapture: () => true,
onPanResponderGrant: (e, gestureState) => {
// Set the initial value to the current state
this.state.pan.setOffset({x: this.state.pan.x._value, y: this.state.pan.y._value});
this.state.pan.setValue({x: 0, y: 0});
Animated.spring(
this.state.scale,
{ toValue: 1.1, friction: 3 }
).start();
},
// When we drag/pan the object, set the delate to the states pan position
onPanResponderMove: Animated.event([
null, {dx: this.state.pan.x, dy: this.state.pan.y},
]),
onPanResponderRelease: ({ identifier, target, pageX, pageY }, {moveX, moveY, x0, y0, dx, dy, vx, vy}) => {
// Flatten the offset to avoid erratic behavior
this.state.pan.flattenOffset();
this.props.movedTile( { title: this.props.tileProps, moveX, moveY } );
**//I want to add a check here that will return to original position, but not sure best way to get or retain coordinates for original position**
this.removeButton();
}
});
}
render() {
// Destructure the value of pan from the state
let { pan, scale } = this.state;
// Calculate the x and y transform from the pan value
let [translateX, translateY] = [pan.x, pan.y];
let rotate = '0deg';
// Calculate the transform property and set it as a value for our style which we add below to the Animated.View component
let tileStyle = {
flex: 0,
borderRadius: 4,
flexDirection: 'row',
justifyContent: 'center',
flexWrap: 'wrap',
transform: [ {translateX}, {translateY}, {rotate}, {scale} ]
};
return (
<Animated.View style={ tileStyle } { ...this._panResponder.panHandlers } >
{this.state.showView && (
<Button
id={this.props.key}
onPress={this._onPressButton}
title={this.props.tileProps}
/>
)}
</Animated.View>
);
}
}

react-native drag and drop multiple items

Im trying to make two circles that can drag and drop with react-native.
I could have created one circle that can drag and drop, but dont know how with two circles individually.
here is the code for one circle that can drag and drop,
constructor(props){
super(props);
this.state = {
pan : new Animated.ValueXY() //Step 1
};
this.panResponder = PanResponder.create({ //Step 2
onStartShouldSetPanResponder : () => true,
onPanResponderMove : Animated.event([null,{ //Step 3
dx : this.state.pan.x,
dy : this.state.pan.y
}]),
onPanResponderRelease : (e, gesture) => {} //Step 4
});
}
and this is for image
renderDraggable(){
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>
);
}
import React, { Component } from 'react';
import {
StyleSheet,
Text,
View,
Image, // we want to use an image
PanResponder, // we want to bring in the PanResponder system
Animated // we wil be using animated value
} from 'react-native';
export default class MovingCircle extends React.Component {
constructor(props) {
super(props);
this.state = {
pan: new Animated.ValueXY(),
scale: new Animated.Value(1)
};
}
_handleStartShouldSetPanResponder(e, gestureState) {
return true;
}
_handleMoveShouldSetPanResponder(e, gestureState) {
return true;
}
componentWillMount() {
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder:
this._handleStartShouldSetPanResponder.bind(this),
onMoveShouldSetPanResponder:
this._handleMoveShouldSetPanResponder.bind(this),
onPanResponderGrant: (e, gestureState) => {
// Set the initial value to the current state
this.state.pan.setOffset({x: this.state.pan.x._value, y: this.state.pan.y._value});
this.state.pan.setValue({x: 30*Math.random(), y: 0});
Animated.spring(
this.state.scale,
{ toValue: 1.1, friction: 1 }
).start();
},
// When we drag/pan the object, set the delate to the states pan position
onPanResponderMove: Animated.event([
null, {dx: this.state.pan.x, dy: this.state.pan.y},
]),
onPanResponderRelease: (e, {vx, vy}) => {
// Flatten the offset to avoid erratic behavior
this.state.pan.flattenOffset();
Animated.spring(
this.state.scale,
{ toValue: 1, friction: 1 }
).start();
}
});
}
render() {
// Destructure the value of pan from the state
let { pan, scale } = this.state;
// Calculate the x and y transform from the pan value
let [translateX, translateY] = [pan.x, pan.y];
let rotate = '0deg';
// Calculate the transform property and set it as a value for our style which we add below to the Animated.View component
let imageStyle = {transform: [{translateX}, {translateY}, {rotate}, {scale}]};
return (
<Animated.View style={[imageStyle, styles.container]} {...this._panResponder.panHandlers} >
<View style={styles.rect}>
<Text style={styles.txt} >tgyyHH</Text>
</View>
</Animated.View>
);
}
}
const styles = StyleSheet.create({
container: {
width:50,
height:50,
position: 'absolute'
},
rect: {
borderRadius:4,
borderWidth: 1,
borderColor: '#fff',
width:50,
height:50,
backgroundColor:'#68a0cf',
},
txt: {
color:'#fff',
textAlign:'center'
}
});
Here is how made items independent of each other. This example is in typescript, but should be clear enough to convert to pure javascript. The main idea here is that each animated item needs its own PanResponderInstance and once you update the items, you need to also refresh the PanResponderInstance
interface State {
model: Array<MyAnimatedItem>,
pans: Array<Animated.ValueXY>,
dropZone1: LayoutRectangle,
dropZone2: LayoutRectangle,
}
public render(): JSX.Element {
const myAnimatedItems = new Array<JSX.Element>()
for (let i = 0; i < this.state.model.length; i++) {
const item = this.state.model[i]
const inst = this.createResponder(this.state.pans[i], item)
myAnimatedItems.push(
<Animated.View
key={'item_' + i}
{...inst.panHandlers}
style={this.state.pans[i].getLayout()}>
<Text>{item.description}</Text>
</Animated.View>
)
}
return (
<View>
<View onLayout={this.setDropZone1} style={styles.dropZone}>
<View style={styles.draggableContainer}>
{myAnimatedItems}
</View>
</View>
<View onLayout={this.setDropZone2} style={styles.dropZone}>
<View style={styles.draggableContainer}>
...
</View>
</View>
</View>
)
}
private setDropZone1 = (event: LayoutChangeEvent): void => {
this.setState({
dropZone1: event.nativeEvent.layout
})
}
private setDropZone2 = (event: LayoutChangeEvent): void => {
this.setState({
dropZone2: event.nativeEvent.layout
})
}
private isDropZone(gesture: PanResponderGestureState, dropZone: LayoutRectangle): boolean {
const toolBarHeight = variables.toolbarHeight + 15 // padding
return gesture.moveY > dropZone.y + toolBarHeight
&& gesture.moveY < dropZone.y + dropZone.height + toolBarHeight
&& gesture.moveX > dropZone.x
&& gesture.moveX < dropZone.x + dropZone.width
}
private createResponder(pan: Animated.ValueXY, item: MyAnimatedItem): PanResponderInstance {
return PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderMove: Animated.event([null, {
dx: pan.x,
dy: pan.y
}]),
onPanResponderRelease: (_e, gesture: PanResponderGestureState) => {
const model = this.state.model
const pans = this.state.pans
const idx = model.findIndex(x => x.id === item.id)
if (this.isDropZone(gesture, this.state.dropZone1)) {
... // do something with the item if needed
// reset each PanResponderInstance
for (let i = 0; i < model.length; i++) {
pans[i] = new Animated.ValueXY()
}
this.setState({ model: model, pans: pans })
return
}
} else if (this.isDropZone(gesture, this.state.dropZone2)) {
... // do something with the item if needed
// reset each PanResponderInstance
for (let i = 0; i < model.length; i++) {
pans[i] = new Animated.ValueXY()
}
this.setState({ model: model, pans: pans })
return
}
Animated.spring(pan, { toValue: { x: 0, y: 0 } }).start()
this.setState({ scrollEnabled: true })
}
})
}