Can't RN do style changes using JS?
...
const PR = PanResponder.create({
onStartShouldSetPanResponder: (e, gestureState) => true,
onPanResponderStart: (e, gestureState) => {
console.log("start");
},
onPanResponderMove: (e, gestureState) => {
const dx = Math.abs(gestureState.dx);
**target.current.style.backgroundColor** = `rgba(${dx},${dx / 2},106,1)`;
},
onPanResponderEnd: (e, gestureState) => {
const dx = Math.abs(gestureState.dx);
if (!dx) {
target.current.style.backgroundColor = "green";
}
console.log("End");
},
});
...
As above, it is difficult to change the style on mobile.
Thank you for your reply.
The useState hook was not what I was expecting.
help me..
In the code above it looks like you were storing the style with useRef, which doesnt trigger component updates when its value changes. Here's a useState example
import * as React from 'react';
import { Text, View, StyleSheet, PanResponder } from 'react-native';
import Animated, {
useSharedValue,
interpolateColor,
} from 'react-native-reanimated';
import Constants from 'expo-constants';
export default function App() {
const [style, setStyle] = React.useState({
borderWidth: 1,
width: '100%',
height: 280,
backgroundColor: 'lightgreen',
});
const PR = PanResponder.create({
onStartShouldSetPanResponder: (e, gestureState) => true,
onPanResponderStart: (e, gestureState) => {
console.log('start');
},
onPanResponderMove: (e, gestureState) => {
const dx = Math.abs(gestureState.dx);
console.log(dx);
setStyle((prev) => ({
...prev,
backgroundColor: `rgba(${dx},${dx / 2},106,1)`,
}));
},
onPanResponderEnd: (e, gestureState) => {
const dx = Math.abs(gestureState.dx);
console.log(dx);
if (!dx) {
setStyle((prev) => ({ ...prev, backgroundColor: 'green' }));
}
console.log('End');
},
});
return (
<View style={styles.container}>
<View style={style} {...PR.panHandlers} />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
backgroundColor: '#ecf0f1',
padding: 8,
},
paragraph: {
margin: 24,
fontSize: 18,
fontWeight: 'bold',
textAlign: 'center',
},
});
While it works, the best results would come from using Animated.View and some Animated values
Look into this panResponder . It has some example of how to use
PanResponderGestureState
in
react-native
Related
Interpolate function from Animated return is not a function
I am following this, but I am currently doing it as function component
(chapter : "Designing the Tinder Cards Movement"):
https://www.instamobile.io/react-native-controls/react-native-swipe-cards-tinder/
I don't understand why interpolate return "is not a function". I spent a lot of time on it and I didn't find something.
There is the card component I have written :
import React, { useState, useEffect } from 'react'
import { Text, View, Animated, Image, StyleSheet, PanResponder } from 'react-native'
function Card(props){
const { screen_height, screen_width, image, Data, currentIndex, Index } = props;
const [pan, setPan] = useState(new Animated.ValueXY());
const [panResponder, setPanResponder] = useState(PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onPanResponderMove: (evt, gestureState) => {
setPan({ x: gestureState.dx, y: gestureState.dy });
},
onPanResponderRelease: (evt, gestureState) => {
}
}));
let value = Math.round(pan.x);
if(value !== NaN || value !== undefined){
console.log(value.interpolate({ inputRange: [0, 1], outputRange: [150, 0]}));
}
if (Index < currentIndex) { return null }
else if(Index == currentIndex){
return(
<Animated.View style={[{ transform:[{ translateX: pan.x }, { translateY: pan.y }] },{ height:screen_height - 120, width:screen_width, padding:10, position:'absolute' }]} {...panResponder.panHandlers}>
<Image style={[{
flex:1,
height:null,
width:null,
resizeMode:"cover",
borderRadius:20,
}]} source={image.uri}/>
</Animated.View>)
} else {
return(
<Animated.View style={[{ height:screen_height - 120, width:screen_width, padding:10, position:'absolute' }]}>
<Image style={[{
flex:1,
height:null,
width:null,
resizeMode:"cover",
borderRadius:20,
}]} source={image.uri}/>
</Animated.View>)
}
}
export default Card;
setPan is wrongly used.pan is now a object not an Animated.Value anymore.You should call setValue or using Animated.event on the animated value Instead.
onPanResponderMove: Animated.event([
null,
{
dx: pan.x, // x,y are Animated.Value
dy: pan.y,
},
]),
Drawing inspiration from this question, I have implemented two draggable components as children in a view. The parent view is as follows:
import React, { Component } from "react";
import { Text, View, StyleSheet, Dimensions } from "react-native";
import Draggable from "./Draggable";
export default class FloorPlan extends Component {
constructor() {
super();
const { width, height } = Dimensions.get("window");
this.separatorPosition = (height * 2) / 3;
}
render() {
return (
<View style={styles.mainContainer}>
<View style={[...styles.dropZone, { height: this.separatorPosition }]}>
<Text style={styles.text}>Floor plan</Text>
</View>
<View style={styles.drawerSeparator} />
<View style={styles.row}>
<Draggable />
<Draggable />
</View>
</View>
);
}
}
const styles = StyleSheet.create({
mainContainer: {
flex: 1
},
drawerSeparator: {
backgroundColor: "grey",
height: 20
},
row: {
flexDirection: "row",
marginTop: 25
},
dropZone: {
height: 700,
backgroundColor: "#f4fffe"
},
text: {
marginTop: 25,
marginLeft: 5,
marginRight: 5,
textAlign: "center",
color: "grey",
fontSize: 20
}
});
And the draggable component is implemented as follows:
import React, { Component } from "react";
import {
StyleSheet,
View,
PanResponder,
Animated,
Text,
Dimensions
} from "react-native";
export default class Draggable extends Component {
constructor() {
super();
const { width, height } = Dimensions.get("window");
this.separatorPosition = (height * 2) / 3;
this.state = {
pan: new Animated.ValueXY(),
circleColor: "skyblue"
};
this.currentPanValue = { x: 0, y: 0 };
this.panListener = this.state.pan.addListener(
value => (this.currentPanValue = value)
);
}
componentWillMount() {
this.state.pan.removeListener(this.panListener);
}
componentWillMount() {
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => false,
onMoveShouldSetPanResponder: (evt, gestureState) => false,
onMoveShouldSetPanResponderCapture: (evt, gesture) => {
return true;
},
onPanResponderGrant: (e, gestureState) => {
this.setState({ circleColor: "red" });
},
onPanResponderMove: (event, gesture) => {
Animated.event([
null,
{
dx: this.state.pan.x,
dy: this.state.pan.y
}
])(event, gesture);
},
onPanResponderRelease: (event, gesture) => {
this.setState({ circleColor: "skyblue" });
if (gesture.moveY < this.separatorPosition) {
this.state.pan.setOffset({
x: this.currentPanValue.x,
y: this.currentPanValue.y
});
this.state.pan.setValue({ x: 0, y: 0 });
// this.state.pan.flattenOffset();
} else {
//Return icon to start position
this.state.pan.flattenOffset();
Animated.timing(this.state.pan, {
toValue: {
x: 0,
y: 0
},
useNativeDriver: true,
duration: 200
}).start();
}
}
});
}
render() {
const panStyle = {
transform: this.state.pan.getTranslateTransform()
};
return (
<Animated.View
{...this.panResponder.panHandlers}
style={[
panStyle,
styles.circle,
{ backgroundColor: this.state.circleColor }
]}
/>
);
}
}
let CIRCLE_RADIUS = 30;
let styles = StyleSheet.create({
circle: {
backgroundColor: "skyblue",
width: CIRCLE_RADIUS * 2,
height: CIRCLE_RADIUS * 2,
borderRadius: CIRCLE_RADIUS,
marginLeft: 25
}
});
A draggable component can be dragged onto the FloorPlan and it's location will be remembered for the next pan action. However, sometimes during dragging, a glitch occurs and the icon jumps at the beginning of the pan or completetely disappears.
What could be the problem? I am developing using React Native 0.55.2 and testing using a device running Android 7.
This question is related to
React Native: Constraining Animated.Value
ReactNative PanResponder limit X position
I am trying to build a horizontal slider with a PanResponder. I can move the element on the x-axis with the following code, but I want to limit the range in which I can move it.
This is an annotated example:
export class MySlider extends React.Component {
constructor(props) {
super(props);
this.state = {
pan: new Animated.ValueXY()
};
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder : () => true,
onPanResponderGrant: (e, gestureState) => {
this.state.pan.setOffset(this.state.pan.__getValue());
this.setState({isAddNewSession:true});
},
///////////////////////////
// CODE OF INTEREST BELOW HERE
///////////////////////////
onPanResponderMove: (evt, gestureState) => {
// I need this space to do some other functions
// This is where I imagine I should implement constraint logic
return Animated.event([null, {
dx: this.state.pan.x
}])(evt, gestureState)
},
onPanResponderRelease: (e, gesture) => {
this.setState({isAddNewSessionModal:true});
this.setState({isAddNewSession:false});
}
});
render() {
let { pan } = this.state;
let translateX = pan.x;
const styles = StyleSheet.create({
pan: {
transform: [{translateX:translateX}]
},
slider: {
height: 44,
width: 60,
backgroundColor: '#b4b4b4'
},
holder: {
height: 60,
width: Dimensions.get('window').width,
flexDirection: 'row',
backgroundColor: 'transparent',
justifyContent: 'space-between',
borderStyle: 'solid',
borderWidth: 8,
borderColor: '#d2d2d2'
}
});
const width = Dimensions.get('window').width - 70
return (
<View style={styles.holder}>
<Animated.View
hitSlop={{ top: 16, left: 16, right: 16, bottom: 16 }}
style={[styles.pan, styles.slider]}
{...this._panResponder.panHandlers}/>
</View>
)
}
}
To Limit the Value so that it can not go below 0,I have tried implementing if else logic like so:
onPanResponderMove: (evt, gestureState) => {
return (gestureState.dx > 0) ? Animated.event([null, {
dx: this.state.pan1.x
}])(evt, gestureState) : null
},
but this is buggy - it seems to work initially, but the minimum x limit appears to effectively increase. The more I scroll back and forward, the minimum x-limit seems to increase.
I also tried this:
onPanResponderMove: (evt, gestureState) => {
return (this.state.pan1.x.__getValue() > 0) ? Animated.event([null, {
dx: this.state.pan1.x
}])(evt, gestureState) : null
},
but it doesn't seem to work at all.
How can interpolate the full breadth of the detected finger movement into a limited range I define?
gestureState.dx is the difference the user moved with the finger from it's original position per swipe. So it resets whenever the user lifts the finger, which causes your problem.
There are several ways to limit the value:
Use interpolation:
let translateX = pan.x.interpolate({inputRange:[0,100],outputRange:[0,100],extrapolateLeft:"clamp"})
While this works, the more the user swipes left the more he has to swipe right to get to "real 0"
reset the value on release
onPanResponderRelease: (e, gestureState)=>{
this.state.pan.setValue({x:realPosition<0?0:realPosition.x,y:realPosition.y})
}
make sure you get the current value using this.state.pan.addListener and put it in realPosition
You can allow some swiping left and animate it back in some kind of spring or just prevent it from going off entirely using the previous interpolation method.
But you should consider using something else since PanResponder doesn't support useNativeDriver. Either use scrollView (two of them if you want 4 direction scrolling) which limits scrolling by virtue of it's content or something like wix's react-native-interactable .
I found this post while looking to linked posts at React Native: Constraining Animated.Value. Your problem seems to be similar to what I experienced and my solution was posted there. Basically, dx can get out of bound b/c it is just the accumulated distance and my solution is cap at the pan.setOffset so dx won't get crazy.
This is a workaround solution for your problem or you can use this as an alternative solution. I am not using pan in this solution. The idea is , restrict slider movement inside the parent view. so it won't move over to parent. Consider below code
export default class MySlider extends Component<Props> {
constructor(props) {
super(props);
this.containerBounds={
width:0
}
this.touchStart=8;
this.sliderWidth= 60;
this.containerBorderWidth=8
this.state = {
frameStart:0
};
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderGrant: (e, gestureState) => {
this.touchStart=this.state.frameStart;
this.setState({ isAddNewSession: true });
},
onPanResponderMove: (evt, gestureState) => {
frameStart = this.touchStart + gestureState.dx;
if(frameStart<0){
frameStart=0
}else if(frameStart+this.sliderWidth>this.containerBounds.width-2*this.containerBorderWidth){
frameStart=this.containerBounds.width-this.sliderWidth-2*this.containerBorderWidth
}
this.setState({
frameStart:frameStart
})
},
onPanResponderRelease: (e, gesture) => {
this.setState({ isAddNewSessionModal: true });
this.setState({ isAddNewSession: false });
}
});
}
render() {
const styles = StyleSheet.create({
slider: {
height: 44,
width: this.sliderWidth,
backgroundColor: '#b4b4b4'
},
holder: {
height: 60,
width: Dimensions.get('window').width,
flexDirection: 'row',
backgroundColor: 'transparent',
justifyContent: 'space-between',
borderStyle: 'solid',
borderWidth: this.containerBorderWidth,
borderColor: '#d2d2d2'
}
});
return (
<View style={styles.holder}
onLayout={event => {
const layout = event.nativeEvent.layout;
this.containerBounds.width=layout.width;
}}>
<Animated.View
hitSlop={{ top: 16, left: 16, right: 16, bottom: 16 }}
style={[{left:this.state.frameStart}, styles.slider]}
{...this._panResponder.panHandlers} />
</View>
)
}
}
Like #AlonBarDavid said above, gestureState.dx is the difference the user moved with the finger from it's original position per swipe. So it resets whenever the user lifts the finger, which causes your problem.
One solution is to create a second variable to hold this offset position from a previous touch, then add it to the gestureState.x value.
const maxVal = 50; // use whatever value you want the max horizontal movement to be
const Toggle = () => {
const [animation, setAnimation] = useState(new Animated.ValueXY());
const [offset, setOffset] = useState(0);
const handlePanResponderMove = (e, gestureState) => {
// use the offset from previous touch to determine current x-pos
const newVal = offset + gestureState.dx > maxVal ? maxVal : offset + gestureState.dx < 0 ? 0 : offset + gestureState.dx;
animation.setValue({x: newVal, y: 0 });
// setOffset(newVal); // <- works in hooks, but better to put in handlePanResponderRelease function below. See comment underneath answer for more.
};
const handlePanResponderRelease = (e, gestureState) => {
console.log("release");
// set the offset value for the next touch event (using previous offset value and current gestureState.dx)
const newVal = offset + gestureState.dx > maxVal ? maxVal : offset + gestureState.dx < 0 ? 0 : offset + gestureState.dx;
setOffset(newVal);
}
const animatedStyle = {
transform: [...animation.getTranslateTransform()]
}
const _panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: () => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
onPanResponderGrant: () => console.log("granted"),
onPanResponderMove: (evt, gestureState) => handlePanResponderMove(evt, gestureState),
onPanResponderRelease: (e, g) => handlePanResponderRelease(e, g)
});
const panHandlers = _panResponder.panHandlers;
return (
<View style={{ width: 80, height: 40 }}>
<Animated.View { ...panHandlers } style={[{ position: "absolute", backgroundColor: "red", height: "100%", width: "55%", borderRadius: 50, opacity: 0.5 }, animatedStyle ]} />
</View>
)
}
I am trying to create a tinder like swipe deck animation. I am using FlatList to render the images. To stack the images one above the other, I am using 'absolute' positioning. The issue with this is the images are not getting rendered and all I am seeing is a blank screen. I am not sure whether there is something wrong with using positioning inside FlatList.
The reason I went with FlatList is my stack will contain around 200 to 300 images. I think I can implement this without using FlatList by just rendering the images as batches (say render 10 images at once and then render the next 10 and so on).
I want to know whether it is possible to implement this using FlatList.
NOTE: The issue is in android and I am not sure about iOS
import React from "react";
import {
StyleSheet,
Text,
View,
FlatList,
Image,
Dimensions,
Animated,
PanResponder
} from "react-native";
const DATA = [
{
id: 1,
text: "Card #1",
uri: "http://www.fluxdigital.co/wp-content/uploads/2015/04/Unsplash.jpg"
},
{
id: 2,
text: "Card #2",
uri: "https://images.pexels.com/photos/247932/pexels-photo-247932.jpeg?h=350"
},
{
id: 3,
text: "Card #3",
uri: "http://www.fluxdigital.co/wp-content/uploads/2015/04/Unsplash.jpg"
}
];
const { width, height } = Dimensions.get("window");
export default class App extends React.Component {
constructor(props) {
super(props);
this.position = new Animated.ValueXY();
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: (event, gestureState) => true,
onPanResponderMove: (event, gestureState) => {},
onPanResponderRelease: (event, gestureState) => {}
});
this.state = {
currentIndex: 0
};
}
extractKey = (item, index) => item.id.toString();
renderCard = ({ item }) => {
return (
<View style={styles.imageContainer}>
<Image
source={{ uri: item.uri }}
resizeMode="cover"
style={styles.image}
/>
</View>
);
};
render() {
return (
<FlatList
contentContainerStle={styles.container}
data={DATA}
keyExtractor={this.extractKey}
renderItem={this.renderCard}
scrollEnabled={true}
/>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1
},
imageContainer: {
width,
height: height - 20,
backgroundColor: "red",
padding: 10
position: 'absolute'
},
image: {
flex: 1,
width: null,
height: null,
borderRadius: 20
}
});
i create tinder swiper using following
might be help you
import React from "react";
import {
StyleSheet,
Text,
View,
FlatList,
Image,
Dimensions,
Animated,
PanResponder
} from "react-native";
const SCREEN_HEIGHT = Dimensions.get('window').height
const SCREEN_WIDTH = Dimensions.get('window').width
const DATA = [
{
id: 1,
text: "Card #1",
uri: "https://images.pexels.com/photos/247932/pexels-photo-247932.jpeg?h=350"
},
{
id: 2,
text: "Card #2",
uri: "http://www.fluxdigital.co/wp-content/uploads/2015/04/Unsplash.jpg"
},
{
id: 3,
text: "Card #3",
uri: "http://www.fluxdigital.co/wp-content/uploads/2015/04/Unsplash.jpg"
}
];
export default class App extends React.Component {
constructor(props) {
super(props);
this.position = new Animated.ValueXY();
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: (event, gestureState) => true,
onPanResponderMove: (event, gestureState) => {},
onPanResponderRelease: (event, gestureState) => {}
});
this.state = {
currentIndex: 0
};
}
extractKey = (item, index) => item.id.toString();
renderUsers = () => {
return DATA.map((item,i)=>{
return(
<View style={ { height: SCREEN_HEIGHT - 120, width: SCREEN_WIDTH, padding: 10, position: 'absolute' }}>
<Image
style={{ flex: 1, height: null, width: null, resizeMode: 'cover', borderRadius: 20 }}
source={{uri:item.uri}} />
</View>
)
})
}
render() {
return (
<View style={{marginTop:24,flex:1,backgroundColor:'#eee'}}>
{
this.renderUsers()
}
</View>
);
}
}
I have implemented PanResponder in my project but it only works when I touch non touchable elements . When I touch touchable elements like TouchableOpacity, PanReponder does not responds.But when I move my finger on TouchableOpacity PanResponder responds.
Same thing happening for Button also
Please tell me what might be the problem.
Expo Link : https://snack.expo.io/SyYrtq87W
import React, { Component } from 'react';
import { Button, PanResponder, View, StyleSheet,TouchableOpacity } from 'react-native';
import { Constants } from 'expo';
export default class App extends Component {
state = {
show : false
};
_panResponder = {};
componentWillMount() {
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => {
alert('clicked')
console.log('clicked')
return true
},
onMoveShouldSetPanResponder: () => {
alert('moved')
console.log('moved')
return true
},
onStartShouldSetPanResponderCapture: () => false,
onMoveShouldSetPanResponderCapture: () => false,
onPanResponderTerminationRequest: () => true,
onShouldBlockNativeResponder: () => false,
});
}
render() {
return (
<View
style={styles.container}
collapsable={false}
{...this._panResponder.panHandlers}>
{/*********************PanResponder does not respond***************************/}
<TouchableOpacity>
<View style={{width:200, height:200,backgroundColor:'red'}}>
</View>
</TouchableOpacity>
<Button
title="Here is a button for some reason"
onPress={() => {}}
/>
{/*****************************************************************************/}
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
backgroundColor: '#ecf0f1',
}
});
I had a similar issue. Basically because you always return true in onStartShouldSetPanResponder and/or onMoveShouldSetPanResponder they will take command over that touch event.
My workaround: Don't consider the touch "yours" (PanResponder's) unless the user moved it a little first by a threshold you set.
const touchThreshold = 20;
const panResponder = PanResponder.create({
onStartShouldSetPanResponder : () => false,
onMoveShouldSetPanResponder : (e, gestureState) => {
const {dx, dy} = gestureState;
return (Math.abs(dx) > touchThreshold) || (Math.abs(dy) > touchThreshold);
},
...
});
Finally It hit me. It was a very silly mistake.
forgot to add alert('clicked') at
onStartShouldSetPanResponderCapture: ()
Now,
onStartShouldSetPanResponderCapture: () => {alert('clicked') ; return false},
Expo Link : https://snack.expo.io/ryWx7lBrb
Now, it takes touch everywhere, including Touchables and Buttons.
import React, { Component } from 'react';
import { Button, PanResponder, View, StyleSheet,TouchableOpacity } from 'react-native';
import { Constants } from 'expo';
export default class App extends Component {
state = {
show : false
};
_panResponder = {};
componentWillMount() {
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => {
alert('clicked')
return true
},
onMoveShouldSetPanResponder: () => true,
onStartShouldSetPanResponderCapture: () => {alert('clicked') ; return false},
onMoveShouldSetPanResponderCapture: () => false,
onPanResponderTerminationRequest: () => true,
onShouldBlockNativeResponder: () => false,
});
}
render() {
return (
<View
style={styles.container}
collapsable={false}
{...this._panResponder.panHandlers}>
{/*********************PanResponder now responds***************************/}
<TouchableOpacity>
<View style={{width:200, height:200,backgroundColor:'red'}}>
</View>
</TouchableOpacity>
<Button
title="Here is a button for some reason"
onPress={() => {}}
/>
{/*****************************************************************************/}
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
backgroundColor: '#ecf0f1',
}
});
Touchables will not work if you set this
onStartShouldSetPanResponder: () => true
This will entirely take control over the touches. It is always a good option to take control only if the user starts dragging the component.
onMoveShouldSetPanResponder: () => true
Here is an example for PanResponder.
this.panResponder = PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onPanResponderMove: (event, gesture) => {
if (gesture.dy > 0 && this.state.size._value < width) {
this.state.size.setValue(40 + gesture.dy)
}
},
onPanResponderRelease: () => {
Animated.spring(this.state.size, {
toValue: 40
}).start()
}
})
This is my JSX code.
<Animated.View {...this.panResponder.panHandlers}>
<TouchableWithoutFeedback onPress={this.openImage}>
<Animated.Image style={profileImage}
source={{uri: 'https://example.com/face.jpg'}}/>
</TouchableWithoutFeedback>
</Animated.View>