According to statista, today, the number of smartphone users surpasses the number of three billions. So in the last years, we have seen an increasing number of applications for mobile devices being developed.
This application can be seen in many areas, like: music (Spotify), travel (Tripadvisor, Uber) and social network (Facebook, Instagram). When we are using some of this apps or another one, our experience improves as the application provides smoother transitions and interactions.
Thus in this post we will see an introduction of how to uses the React Native Animation library for improves the user experience in application and we will uses the Nubank interface as example.
Animation library basics
The React Native Animated library provide us two types of values tha we can use to animate our components:
- Animated.Value
- Animated.ValueXY
Since we have one of this values or both, we can trigger them with our component and use some function, provided by React Native, to drive the animation:
- Animated.spring() -> provides a basic spring physics model.
- Animated.timing() -> animates a value over time.
Example:
const value = new Animated.Value(0);
Animated.spring(value, {
toValue: 0,
speed: 5,
}).start();
Animated.timing(value, {
toValue: 300,
duration: 500,
}).start();
In the rest of this post we will use this Animated values and functions to animate our app example.
Horizontal animation
As we can see, when the app initialise, the first animation that occur is a horizontal moving of card and bottom buttons. Bellow we have the code and image app without any animation:
So our first step will be create a horizontal animation for the card and bottom buttons after the app has ben initialized.
React Native provide us some animatable components and only this can be animated. So to animate bottom buttons we need change the View
that wrapper the BottomButtons Component
by an Animated.View
:
<View>
<BottomButtons />
</View>;
to;
import {Animated} from 'react-native';
<Animated.View>
<BottomButtons />
</Animated.View>;
and we need create a Animated Value
that will be used toghether the Animated Component
previously created:
import React, {useRef, useEffect} from 'react';
import {Dimensions, Animated} from 'react-native';
const {width} = Dimensions.get('screen');
const bottomButtonsAnimated = useRef(new Animated.Value(-width)).current;
<Animated.View
style={{
right: bottomButtonsAnimated,
}}>
<BottomButtons />
</Animated.View>;
In the above code we get the screen width to initialy put our bottom buttons component outside the screen and after animate it go back to screen. We also saw the useRef
, whitout it every rerender will reset the bottomButtonsAnimated
to the initial value.
So at this time we have an Animated Value (bottomButtonsAnimated
) to be used with the <Animated.View />
that wrapper the <BottomButtons />
component. However we still need animate this value and it can be do using Animated.spring()
:
useEffect(() => {
Animated.spring(bottomButtonsAnimated, {
toValue: 0,
speed: 5,
}).start();
}, []);
When the Animated.spring
initialise it will increment the bottomButtonsAnimated from -width
to zero
. As the <Card />
component has the same animation, we can use the bottomButtonsAnimated
to animate it also:
<Card horizontalShift={bottomButtonsAnimated} />
and we will get the first animations:
Vertical animation
Now, as the first gif, our aim is create the vertical animations and for it we will use the PanResponder, once the PanResponder works with Animated API to build gestures in the UI.
The PanResponder provide us a create method
that accept as parameter an config object with all responder callbacks. Each one callback receive as parameter a native event:
changedTouches: Array of all touch events that have changed since the last event
identifier: The ID of the touch
locationX: The X position of the touch, relative to the element
locationY: The Y position of the touch, relative to the element
pageX: The X position of the touch, relative to the root element
pageY: The Y position of the touch, relative to the root element
target: The node id of the element receiving the touch event
timestamp: A time identifier for the touch, useful for velocity calculation
touches: Array of all current touches on the screen
and gestureState object:
stateID: ID of the gestureState, persisted as long as there at least one touch on screen
moveX: the latest screen coordinates of the recently-moved touch
moveY: the latest screen coordinates of the recently-moved touch
x0: the screen coordinates of the responder grant
y0: the screen coordinates of the responder grant
dx: accumulated distance of the gesture since the touch started
dy: accumulated distance of the gesture since the touch started
vx: current velocity of the gesture
vy: current velocity of the gesture
numberActiveTouches - Number of touches currently on screen
With the above information, as the first step, we must create a Animated Value to vertical moving:
const cardAnimatedValueY = useRef(new Animated.Value(0)).current;
and pass this value as parameter to Card component:
<Card
horizontalShift={bottomButtonsAnimated}
verticalShift={cardAnimatedValueY}
/>
Once we have the Animated Value to vertical moving, we can use the PanResponder to recognize the gestures:
import {StatusBar, Dimensions, Animated, PanResponder} from 'react-native';
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) => {
cardAnimatedValueY.setValue(gestureState.dy);
},
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderRelease: (evt, gestureState) => {},
onPanResponderTerminate: (evt, gestureState) => {},
onShouldBlockNativeResponder: (evt, gestureState) => true,
}),
).current;
In the above code we use the onPanResponderMove
to set the cardAnimatedValueY
and we need to pass the PanResponder callbacks to Card component:
<Card
horizontalShift={bottomButtonsAnimated}
verticalShift={cardAnimatedValueY}
{...panResponder.panHandlers}
/>
and internally the card component handles the parameters:
<WrapperSwiper
style={{
right: horizontalShift,
transform: [{translateY: verticalShift}],
}}
{...rest}>
{renderCard()}
</WrapperSwiper>
where the WrapperSwiper
is a Animated.View
.
The above gif demonstrates the results for vertical the moving, however we still have two issues with it gif:
1 - The opacity related to another components;
2 - When we release the card and touch it again the card position became to the initial.
Components opacity
When we slide the card, we need change the opacity from another components. So that the menu options under the card need initialise with zero opacity and growth to one as the card slide down. We need also that the <BottomButtons />
opacity initialise with one opacity and go to zero as the card slide down.
At this point we have creating our animations applying changes to animated variables cardAnimatedValueY
straightly. However we can obtain another animated values as our animated variables cardAnimatedValueY
changes, example:
cardAnimatedValueY | Result |
---|---|
0 | 0 |
1 | 0.1 |
2 | 0.2 |
... | ... |
415 | 1 |
So to do that and use the result with the opacity of our components we will use the AnimatedVariable.interpolate
function. It will receive as parameter a configuration object:
{
inputRange: number[];
outputRange: string[] | number[];
extrapolate?: ExtrapolateType;
}
Thus this function will map the inputRange to outputRange and we can use extrapolate
property to inform what action to take if the inputRange extrapolate the range. In our case we alway want a opacity value between 0 and 1, so we will "clamp" the outputRange in case the extrapolete inputRange occurring.
Now we can make our opacity animations:
import {Dimensions} from 'react-native';
const {width, height} = Dimensions.get('screen');
const MAX_SLIDE = Math.floor(height * 0.623);
const MIDDLE_SLIDE = Math.floor(height * 0.33);
const opacity = cardAnimatedValueY.interpolate({
inputRange: [0, MIDDLE_SLIDE, MAX_SLIDE],
outputRange: [0, 0.3, 1],
extrapolate: 'clamp',
});
const opacityBottomButtons = cardAnimatedValueY.interpolate({
inputRange: [0, MIDDLE_SLIDE, MAX_SLIDE],
outputRange: [1, 0.6, 0],
extrapolate: 'clamp',
});
the MAX_SLIDE
value is the distance from the inital card component position to under the button SAIR DA CONTA
.
Now we will be apply the opacity
variable to:
<Menu opacity={opacity} />
<Logout opacity={opacity} />
and the opacityBottomButtons
to:
<BottomButtons opacity={opacityBottomButtons} />
Fix card position after release and touch again
If we put a console.log
statement inside the onPanResponderMove
method and print the gestureState.dy
value, we can see that the variable that contain the vertical displacement became zero always that we release the card and touch again.
So to solve this we need save the last gestureState.dy
value to maintain the card position. To do it, we create a variable that will accumulate vertical displacement on each release inside onPanResponderRelease
method that will be executed after the touch release.
export default function App() {
let offSet = 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) => {
cardAnimatedValueY.setValue(gestureState.dy);
},
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderRelease: (evt, gestureState) => {
offSet += gestureState.dy;
cardAnimatedValueY.setOffset(offSet);
},
onPanResponderTerminate: (evt, gestureState) => {},
onShouldBlockNativeResponder: (evt, gestureState) => true,
}),
).current;
Auto vertical displacement
At the post begin we saw a gif in which when we touch the card, move it move until certain point and release the card, it auto displacement to bottom or top. So our last step will be create this behavior.
As we saw, the method that catch the event related to release the touch is: onPanResponderRelease
. Thus we will change this method to make our feature.
export default function App() {
let offSet = 0;
let down = true;
const panResponder = React.useRef(
PanResponder.create({
.
.
.
onPanResponderRelease: (evt, gestureState) => {
offSet += gestureState.dy;
cardAnimatedValueY.setOffset(offSet);
const restOffSet = 415 - offSet;
if (down && offSet >= 100) {
Animated.timing(cardAnimatedValueY, {
toValue: restOffSet,
duration: 500,
}).start(() => {
cardAnimatedValueY.setOffset(415);
cardAnimatedValueY.setValue(0);
offSet = 415;
down = !down;
});
}
},
.
.
.
}),
).current;
In the above code snippet we add the down
variable that will contain the direction that the card will be take. The restOffSet
represents the units that the card can slide to the bottom. So if the direction is down and the card offset is bigger than or equal to 100 units, the card will slide restOffSet
.
Now what we need is make the reverse path, to the top:
export default function App() {
let offSet = 0;
let down = true;
const panResponder = React.useRef(
PanResponder.create({
.
.
.
onPanResponderRelease: (evt, gestureState) => {
offSet += gestureState.dy;
cardAnimatedValueY.setOffset(offSet);
const restOffSet = 415 - offSet;
if (down && offSet >= 100) {
Animated.timing(cardAnimatedValueY, {
toValue: restOffSet,
duration: 500,
}).start(() => {
cardAnimatedValueY.setOffset(415);
cardAnimatedValueY.setValue(0);
offSet = 415;
down = !down;
});
} else if (!down && offSet <= 355) {
Animated.timing(cardAnimatedValueY, {
toValue: restOffSet - MAX_SLIDE,
duration: 500,
}).start(() => {
cardAnimatedValueY.setOffset(0);
cardAnimatedValueY.setValue(0);
offSet = 0;
down = !down;
});
}
},
.
.
.
}),
).current;
The else
clausule define the reverse path to the top.
This post was a little introduction about React Native Animated and we saw how tp apply some animations in an app. However we can explorer another libraries that improve the animations performance running on UI thread without having to go through the bridge on every frame: React Native Reanimated and React Native Gesture Handler.
Top comments (0)