I'm playing around with animation in React Native. Want to build a Screen that allows a user to touch the screen at any point. When they do, a circle should appear at the location of their finger, and follow it around. When they release, the circle should shrink back to nothing.
I started following this tutorial, which is for a spring-like animation, in which you drag a preexisting square (which starts in the center of the screen), and it springs back on release.
In my case, the circle should have no preexisting location, and this is causing the problem. If I initialize my component state, with, say, pan: new Animated.ValueXY()
, and then touch the screen, the circle starts in the upper left corner, not where my finger is. I've tried a variety of other solutions, all replacing the commented line in the onPanResponderGrant method below, but can't seem to figure it out.
How do I have an animation that simply has the component track my finger without starting or returning anywhere?
Here's the code for the component:
class FollowCircle extends React.Component {
constructor(props) {
super(props);
this.state = {
pan: new Animated.ValueXY(),
scale: new Animated.Value(0)
};
}
componentWillMount() {
this._panResponder = PanResponder.create({
onStartShouldSetPanResponderCapture: () => true,
onMoveShouldSetPanResponderCapture: () => true,
onPanResponderGrant: (e, gestureState) => {
// This is the line I'm playing with
this.state.pan.setValue({ x: this.state.pan.x._value, y: this.state.pan.y._value });
Animated.spring(
this.state.scale,
{ toValue: 1, friction: 3 },
).start();
},
onPanResponderMove: Animated.event([
null, { dx: this.state.pan.x, dy: this.state.pan.y }
]),
onPanResponderRelease: (/* e, gestureState */) => {
this.state.pan.flattenOffset();
Animated.spring(
this.state.scale,
{
toValue: 0,
friction: 10,
restDisplacementThreshold: 1
},
).start();
}
});
}
render() {
const { pan, scale } = this.state;
const circleStyles = {
backgroundColor: 'black',
width: 50,
height: 50,
borderRadius: 25,
transform: [
{ translateX: pan.x },
{ translateY: pan.y },
{ scale }
]
};
return (
<AppWrapper navigation={ this.props.navigation }>
<Animated.View
style={ {
flex: 1,
width: Dimensions.get('window').width,
height: Dimensions.get('window').height,
backgroundColor: 'white'
} }
{ ...this._panResponder.panHandlers }
>
<Animated.View
style={ circleStyles }
/>
</Animated.View>
</AppWrapper>
);
}
};
And here are my alternative attempts (replacements for the commented line):
this.state.pan.setValue({ x: gestureState.x0, y: gestureState.y0 });
And this one:
if (this.state.pan) {
this.state.pan.setValue({ x: this.state.pan.x._value, y: this.state.pan.y._value });
} else {
this.setState({ pan: new Animated.ValueXY(gestureState.x0, gestureState.y0) });
}
Both seem to behave similarly, resetting the circle location in the upper left after each movement, and not quite tracking my finger location unless I start my touch at that origin.
Any ideas how to do this correctly?
I ended up figuring this out by replacing the panResponder
stuff with a page-sized TouchableHighlight
, with an onTouch
event.
That event triggered the following function, which gets the nativeEvent.pageX
and pageY
location, and uses that. I had to make some slight alterations for the y
location, to make it work across phones:
triggerSwipe({ nativeEvent }) {
const coordinates = { x: nativeEvent.pageX, y: nativeEvent.pageY };
const self = this;
if (!this.state.animating) {
this.setState({
x: coordinates.x,
y: coordinates.y + (this.windowHeight / 2.5),
animating: true
}, () => {
Animated.timing(
self.state.scale,
{ toValue: 20, duration: 600 },
).start(() => {
this.setState({
scale: new Animated.Value(0.01), // See Notes.md for reasoning
colorIndex: this.state.colorIndex + 1,
animating: false
});
});
});
}
The animating circle is an Animated.View
inside the TouchableHighlight
, which is fed the coordinates of the touch.