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
Related
I'm trying to find a replacement for ComponentWillMount method with useEffect. But panresponder variable is not created before the initial render.
Can anyone please tell me if any mistakes I'm doing here? without useEffect, the page is getting rendered. gestures are not captured.
const makeup = () => {
useEffect(() => {
console.log('I am about to render!');
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: (e, gestureState) => true,
onPanResponderMove: (e, gestureState) => {....},
onPanResponderRelease: (e, gestaureState) => {....}
}));
},[]);
return ARTICLES.map((item, i) => {
console.log("Makeup i:"+i+" item:"+item);
if (i === index.currentIndex - 1) {
return (
<Animated.View
key={item.id}
style={swipeCardPosition.getLayout()}
{...panResponder.panHandlers}
>
</Animated.View>
);
}
if (i < index.currentIndex) {
return null;
}
return (
<Animated.View
key={item.id}
style={index.currentIndex === i ? position.getLayout() : null}
{...(index.currentIndex === i
? { ...panResponder.panHandlers }
: null)}
>
</Animated.View>
);
}).reverse();
};
[Error on execution][1]
[1]: https://i.stack.imgur.com/sls5E.png
Check the official docs. It describes how to use PanResponder with functional components.
For example:
const ExampleComponent = () => {
const panResponder = React.useRef(
PanResponder.create({
onStartShouldSetPanResponder: (e, gestureState) => true,
onPanResponderMove: (e, gestureState) => {....},
onPanResponderRelease: (e, gestaureState) => {....}
})
).current;
return <View {...panResponder.panHandlers} />;
};
Your example won't work, for a large number of reasons, most notably because you can't call useRef inside useEffect.
im using PanResponder in React Native but i have a simple problem that i can't solve!!!
this is my code
class test extends React.Component {
constructor(props) {
super(props);
const position = new Animated.ValueXY();
const panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderMove: (evt, gs) => {
console.log(gs.dx);
position.setValue({ x: gs.dx , y: gs.dy })
},
onPanResponderRelease: (evt, gs) => {
},
});
this.state = { panResponder, position }
}
getCardStyle() {
const { position } = this.state;
return {
...position.getLayout(),
};
}
render() {
return (
<View>
{
this.props.cart.map((item, index) => {
return (
<Animated.View style={[this.getCardStyle() ,styles.CardContainer]} key={item.id}
{...this.state.panResponder.panHandlers}
>
<View><Image style={styles.image} source={{ uri: item.src}} /></View>
<Text style={styles.title}>{item.location}</Text>
</Animated.View>
)
})
}
</View>
);
}
}
i want to move every item finger
Separately but when i touch every item in list whole items start moving.
can anyone help me please??
SOLVE IT
I made another component named ListItem and in that component render FlatList items and it worked!
I need to read the value of useState in onPanResponderMove. On page load onPanResponderMove correctly logs the initial value of 0.
However after I click on TouchableOpacity to increment foo, the onPanResponderMove stills logs out 0 rather than it's new value.
export default function App() {
const [foo, setFoo] = React.useState(0);
const panResponder = React.useRef(
PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
onPanResponderGrant: (evt, gestureState) => {},
onPanResponderMove: (evt, gestureState) => {
console.log(foo); // This is always 0
},
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderRelease: (evt, gestureState) => {},
onPanResponderTerminate: (evt, gestureState) => {},
onShouldBlockNativeResponder: (evt, gestureState) => {
return true;
},
})
).current;
return (
<View style={{ paddingTop: 200 }}>
<TouchableOpacity onPress={() => setFoo(foo + 1)}>
<Text>Foo = {foo}</Text>
</TouchableOpacity>
<View
{...panResponder.panHandlers}
style={{ marginTop: 200, backgroundColor: "grey", padding: 100 }}
>
<Text>Text for pan responder</Text>
</View>
</View>
);
}
The pan responder depends on the value 'foo'. useRef wouldn't be a good choice here. You should replace it with useMemo
const panResponder = useMemo(
() => PanResponder.create({
[...]
onPanResponderMove: (evt, gestureState) => {
console.log(foo); // This is now updated
},
[...]
}),
[foo] // dependency list
);
Issue
The issue is that you create the PanResponder once with the foo which you have at that time. However with each setFoo call you'll receive a new foo from the useState hook. The PanResponder wont know that new foo. This happens due to how useRef works as it provides you a mutable object which lives through your whole component lifecycle. (This is explained in the react docs here)
(You can play around with the issue and a simple solution in this sandbox.)
Solution
In your case the simplest solution is to update the PanResponder function with the new foo you got from useState. In your example this would look like this:
export default function App() {
const [foo, setFoo] = React.useState(0);
const panResponder = React.useRef(
PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
onPanResponderGrant: (evt, gestureState) => {},
onPanResponderMove: (evt, gestureState) => {
console.log(foo);
},
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderRelease: (evt, gestureState) => {},
onPanResponderTerminate: (evt, gestureState) => {},
onShouldBlockNativeResponder: (evt, gestureState) => {
return true;
},
})
).current;
// update the onPanResponderMove with the new foo
panResponder.onPanResponderMove = (evt, gestureState) => {
console.log(foo);
},
return (
<View style={{ paddingTop: 200 }}>
<TouchableOpacity onPress={() => setFoo(foo + 1)}>
<Text>Foo = {foo}</Text>
</TouchableOpacity>
<View
{...panResponder.panHandlers}
style={{ marginTop: 200, backgroundColor: "grey", padding: 100 }}
>
<Text>Text for pan responder</Text>
</View>
</View>
);
}
Note
Always be extra careful if something mutable depends on your components state. If this is really necessary it is often a good idea to write a proper class or object with getters and setters. For example something like this:
const createPanResponder = (foo) => {
let _foo = foo;
const setFoo = foo => _foo = foo;
const getFoo = () => _foo;
return {
getFoo,
setFoo,
onPanResponderMove: (evt, gestureState) => {
console.log(getFoo());
},
...allYourOtherFunctions
}
}
const App = () => {
const [foo, setFoo] = React.useState(0);
const panResponder = useRef(createPanResponder(foo)).current;
panResponder.setFoo(foo);
return ( ... )
}
With useEffect you can create a new PanResponder when foo changes. However I'm not sure how performant this is.
export default function App() {
const [foo, setFoo] = React.useState(0);
const pan = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
onPanResponderGrant: (evt, gestureState) => {},
onPanResponderMove: (evt, gestureState) => {
console.log(foo); // This is always 0
},
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderRelease: (evt, gestureState) => {},
onPanResponderTerminate: (evt, gestureState) => {},
onShouldBlockNativeResponder: (evt, gestureState) => {
return true;
},
});
const [panResponder, setPanResponder] = React.useState(pan);
useEffect(() => {
setPanResponder(pan);
}, [foo]);
return (
<View style={{ paddingTop: 200 }}>
<TouchableOpacity onPress={() => setFoo(foo + 1)}>
<Text>Foo = {foo}</Text>
</TouchableOpacity>
<View
{...panResponder.panHandlers}
style={{ marginTop: 200, backgroundColor: "grey", padding: 100 }}
>
<Text>Text for pan responder</Text>
</View>
</View>
);
}
It looks like you're passing foo in an attempt to update the existing state. Instead, pass in the previous state and update it accordingly (functional update). Like this:
<TouchableOpacity onPress={() => setFoo(f => f + 1)}>
<Text>Foo = {foo}</Text>
</TouchableOpacity>
To make the current value of foo available inside the onPanResponderMove handler. Create a ref.
According to the docs:
The “ref” object is a generic container whose current property is mutable and can hold any value
So, with that in mind we could write:
const [foo, setFoo] = React.useState(0);
const fooRef = React.useRef()
React.useEffect(() => {
fooRef.current = foo
},[foo])
Then inside the onPanResponderMove handler you can access the ref's current value that we set before:
onPanResponderMove: (evt, gestureState) => {
console.log('fooRef', fooRef.current)
alert(JSON.stringify(fooRef))
},
Working example here
More info on stale state handling here
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.
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>
)
}