Today we are going to build from scratch, as usual, this simple and powerful animation React Native using mainly the react-native-gesture-handler package and the reanimated package.
So in this article, we're going to introduce a new series called "What about gestures?". The aim is to take an in-depth look at the React Native animations closely related to gestures.
Obviously, the "Animate with Reanimated" series will not be interrupted and will continue to bring content related to animations.
Let me tell you that this animation is heavily inspired by the Chat Heads example already uploaded in the react-native-gesture-handler repository (Learn more).
That said we can finally move to the code part.
YouTube Channel
Are you more of a visual learner? Here you can find my full video tutorial
Code Setup
Here I've created a React Native project with the latest version of the Expo SDK (v44 till now).
At this point, you can already start to see how the new version of the Gesture Handler package works.
Normally, if we wanted to move this circle on the screen, we would have to use the PanGestureHandler component from react-native-gesture-handler (version < 2.0.0).
With the new version, we can simply use the GestureDetector to handle every kind of gesture animation.
So the difference is that, instead of using a PanGestureHandler, TapGestureHandler, PinchGestureHandler (and so on...), we can just use and define every gesture with the GestureDetector component.
Since we're going to animate the circle, we can convert it into an Animated.View.
...
import{
GestureDetector,
GestureHandlerRootView,
}from'react-native-gesture-handler';
...
exportdefaultfunctionApp(){
return(
<GestureHandlerRootViewstyle={{ flex:1}}>
<Viewstyle={styles.container}>
<GestureDetectorgesture={🧐}>
<Animated.Viewstyle={styles.circle}/>
</GestureDetector>
</View>
</GestureHandlerRootView>
);
}
...
To move around it, we definitely need to specify the "gesture" property inside the GestureDetector component.
We can define the "gesture" value as follows, by implementing the onUpdate callback:
...
import{
Gesture,
GestureDetector,
GestureHandlerRootView,
}from'react-native-gesture-handler';
...
exportdefaultfunctionApp(){
const gesture =Gesture.Pan()
.onUpdate((event)=>{
console.log(event.translationX)
})
return(
<GestureHandlerRootViewstyle={{ flex:1}}>
<Viewstyle={styles.container}>
<GestureDetectorgesture={gesture}>
<Animated.Viewstyle={styles.circle}/>
</GestureDetector>
</View>
</GestureHandlerRootView>
);
}
...
Since we need to use the event.translationX value we can just define a SharedValue and store it inside a SharedValue.
const translateX =useSharedValue(0);
const gesture =Gesture.Pan().onUpdate((event)=>{
translateX.value= event.translationX;
});
Finally, we can easily create the Reanimated Style and pass it to the circle.
const rStyle =useAnimatedStyle(()=>{
return{
transform:[{ translateX: translateX.value}],
};
});
Since we're going to animate also on the vertical axis, let's handle in the same way the translateY value.
const translateX =useSharedValue(0);
const translateY =useSharedValue(0);
const gesture =Gesture.Pan().onUpdate((event)=>{
translateX.value= event.translationX;
translateY.value= event.translationY;
});
const rStyle =useAnimatedStyle(()=>{
return{
transform:[
{ translateX: translateX.value},
{ translateY: translateY.value},
],
};
});
By moving the circle a little bit, you can notice a glitchy behavior. That is caused by the fact that we're not handling the previous position. So on the second try, the translateX Shared Value will start again from the initial position.
To handle this use case, we need to store and retrieve the animation context.
So let's reload and you can see that everything is working.
We're able to scroll everywhere the circle, but it isn't the animation that we want. We don't want the circle to follow our finger perfectly but we want it to follow with a spring animation.
In order to do it, let's derive the followX position.
Basically, this followX position will be derived from the translateX value by simply applying a spring animation (the same concept can be of course iterated for the y axis as follows):
Abstract the logic inside a custom hook: useFollowAnimatedPosition
Right now, we definitely need to create and animate the red circle. The purpose of the red circle will be to follow the blue one.
What does it mean? That we're going to replicate basically the same exact logic again.
To avoid the code replication, we can simply abstract the previous logic inside a custom hook called "useFollowAnimatedPosition" (feel free to use whatever name you want).
This hook should make an easy job. It should take the x and y positions to follow and return the springified followX and followY positions with their Reanimated style. Here it is:
...
interfaceAnimatedPosition{
x:Animated.SharedValue<number>
y:Animated.SharedValue<number>
}
constuseFollowAnimatedPosition=({ x, y }:AnimatedPosition)=>{
Right now, everything should work as before, but we can easily start to add the red circle and let him follow the blue one by reusing the same custom hook.
Basically, we want the red circle to follow the blue circle instead of following our finger (the blue circle will follow the finger).
Animating the green circle
The last circle that we want to animate is the green circle and at this stage, everything should be almost easy to do. We're just going to replicate, once again, the same logic used for the blue and the red circles, but this time we're going to use the relation between the red and the green circle.