My project is a Animated view based on PanResponder.
The Animated.View has three child components.
When I dragged the animated view to top(and child component out of sight of the screen),the child content became shorter and shorter.
But no problem when dragged to bottom.
What is the matter,please?
Thanks!
=======
screen shot videos:
drag child: no problem
drag child: Problem occur when dragging
=====
source:
complete code:
drag panel
child component
main codes shown:
<View style={styles.draggableContainer}>
<Animated.View
onLayout={this.onLayout}
{...this.panResponder.panHandlers}
style={[
this.pan.getLayout(),
styles.aniView,
]}>
{this.renderChildren()}
</Animated.View>
</View>
renderChildren = () => {
const { source } = this.state;
const children = source.map((item, index) => {
return (
<View key={item.toString()}
style={{
width: WIDTH,
height: HEIGHT / 3,
flex: 1,
borderWidth: 1,
}}>
<ChildContent title={item} />
</View>
);
});
return children;
}
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();
this.animatePanel();
}
});
}
Environment:
OS: macOS High Sierra 10.13.6
I got the solution:
update to react-native 0.56.
(using react-native-git-upgrade ,and update babel-preset-react-native to 5.0.2).
then the problem disappeared.
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,
},
]),
There are many examples available from great developers around the world for collapsible header which work based on the scroll value of scrollView. But I'm trying to implement the same by using PanResponder.
Here is what I'm trying to achieve imgur link
in terms of React Native if I explain the screen components we see in the image is a header at the top and a scrollView thereafter (lets skip the directory breadcrumbs)
initially when the screen renders the header stays collapsed, if we slide down the screen the header expands with a springy animation similarly when sliding up the header gets back to initial collapsed state with animation. During the header collapse / expansion the scrollView scroll stays lock until the header animation is complete i.e we have to track gesture via PanResponder.
If we slide up the header to any position that is less than 50% of the header then the header springs back & expands, similarly reverse happens when sliding down from a collapsed condition.
In last few days I went up to the expand & collapse part but can't figure out the springy effect for the header animation now my back is against the wall, any help will be much appreciated
Here is the code
class App extends Component {
constructor(props) {
super(props);
this.state = {
pan: new Animated.ValueXY(),
items: [1,2,3,4,5]
};
this.animY = new Animated.Value(0);
this.lastY = 0;
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
onPanResponderGrant: (evt, gestureState) => {
},
onPanResponderMove: (evt, gestureState) => {
let { dy } = gestureState;
this.animY.setValue(this.lastY + dy);
},
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderRelease: (evt, gestureState) => {
let { dy } = gestureState;
this.lastY += dy;
},
});
}
handleScroll = () => {
console.log('scroll init');
}
render() {
this.state.pan.addListener((value) => {
console.log('animY', this.animY);
});
const AnimateHeaderHeight = Animated.diffClamp(this.animY, -200, 0).interpolate(
{
inputRange: [ -200, 0 ],
outputRange: [ 50, 200 ],
extrapolate: 'clamp'
});
return (
<View style={{flex: 1}}>
<Animated.View style={[{backgroundColor: '#4287f5', height: AnimateHeaderHeight}]}>
<Text>Sample</Text>
</Animated.View>
<ScrollView style={{flex: 1, backgroundColor: '#e6efff'}}
onScroll={this.handleScroll}
{...this._panResponder.panHandlers}
scrollEnabled={false}
>
{this.state.items.map((item, index) => {
return (
<View style={styles.item} key={index}>
<Text>Sample</Text>
</View>
)
})}
</ScrollView>
</View>
)
}
};
here is the output of my above code
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 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>