Animations with Reanimated can be more difficult to deal with than normal Animated. However when complex animations cause performance issues having a full understanding of Reanimated will empower you to create animations without issue.
In normal Animated
fashion you would call Animated.timing()
and pass in an Animated.Value
. You would supply it with a configuration including the easing
, toValue
, and duration
. Based upon the difference between the current value, the toValue
supplied and how long the animation should take the Animated
library will calculate each step to transition.
A similar concept applies to Reanimated but it will leverage a Clock
, it will tick every frame and based upon the configuration you've provided will advanced the position
. Rather than calling .start
or .stop
you call startClock
and stopClock
.
In Animated
the start()
would take a callback and when it was completed it would be called with a finished
boolean. In Reanimated there is a finished
value that will be set to 1
. Additionally to check if an animation is running you use clockRunning
.
These will all be more clear as soon as we dive into code.
The first step is our box. This is basically going to turn into a button but we have to have something to touch. We utilize Animated.View
from Reanimated so it can handle the animated value that we are going to pass to it later.
import Animated from "react-native-reanimated"; render() { return ( <View style={styles.container}> <Animated.View style={[ styles.rect, ]} /> </View> ); }
Here we just set up our styling.
const styles = StyleSheet.create({ container: { flex: 1, alignItems: "center", justifyContent: "center", }, rect: { width: 200, height: 200, backgroundColor: "tomato", }, });
Next we are going to setup our gestureState
value. This is going to hold onto the state of our TapGestureHandler
. We need to use Value
from Reanimated to hold -1 which is going to be a non-existent state of the gesture handler.
We then use event
from Reanimated to setup a traversal of a value that would be called back to the onHandlerStateChange
function. The syntax is an array, which we have placed an object in the first position of the array which corresponds with the first argument the callback would be called with. Then the object tells Reanimated to look at nativeEvent
then state
on the nativeEvent
and whatever is at state
assign that to our Value
called gestureState
.
This syntax here is class property syntax. The value of gestureState
will be on the class instance. So in order to reference it we need to do this.gestureState
.
import Animated from "react-native-reanimated"; import { TapGestureHandler } from "react-native-gesture-handler"; const { Value, event } = Animated; export default class Example extends Component { gestureState = new Value(-1); onStateChange = event([ { nativeEvent: { state: this.gestureState }, }, ]); }
Now we pass our this.onStateChange
event handler from Reanimated into the onHandlerStateChange
callback of the TapGestureHandler
.
render() { return ( <View style={styles.container}> <TapGestureHandler minDist={0} onHandlerStateChange={this.onStateChange}> <Animated.View style={[ styles.rect, ]} /> </TapGestureHandler> </View> );
Now onto the clocks. We need to create a new instance of a Clock. This is going to be an instance that can be started and stopped. When started it will tick on the native side and handle the configurations that you've passed it. This is essentially the requestAnimationFrame
but in Reanimated.
Now the other thing is we're going to need to setup a function. We'll call it runOpacityTimer
and we'll pass it our clock
as well as our gestureState
. The clock is so we can control timing opacity animation and then also the gestureState
so we can observe it and trigger the animation when the button is clicked.
The return of runOpacityTimer
will eventually be the value pass to our button.
const { Value, event, Clock } = Animated; export default class Example extends Component { gestureState = new Value(-1); clock = new Clock(); onStateChange = event([ { nativeEvent: { state: this.gestureState }, }, ]); opacity = runOpacityTimer(this.clock, this.gestureState); }
Running a timing animation we'll need to define an Easing. So we'll first destructure Easing off of Reanimated.
const { Value, event, Clock, Easing } = Animated;
Now lets take a look at the config for the Clock and what will be passed into our timing
call.
The first is state
this is the state of our clock. It has 4 values. The finished
will either be a 0
or 1
. 0 meaning hasn't finished and 1 meaning the clock has completed. The time
and frameTime
are used to determine how much progress is needed to be made on the animation.
The timing
animation is done using the other building blocks provided by Reanimated. You can check it out the timing code here
For our config that will be passed to our timing we setup our duration of 300
, we set our toValue
to -1
which will be explained why later. Then finally we setup our easing for how we want our animation to be processed.
const runOpacityTimer = (clock, gestureState) => { const state = { finished: new Value(0), position: new Value(0), time: new Value(0), frameTime: new Value(0), }; const config = { duration: 300, toValue: new Value(-1), easing: Easing.inOut(Easing.ease), }; };
We'll now setup a bunch of conditions and our animation so we need to grab all of these things off of Reanimated.
const { Value, event, Clock, Easing, timing, block, stopClock } = Animated;
We've already seen our config and state. But now we need to setup our timing
. When we want to combine a bunch of animations, conditionals, and like wise we use block
. This will create a block of actions that are required for the animation.
const runOpacityTimer = (clock, gestureState) => { const state = { finished: new Value(0), position: new Value(0), time: new Value(0), frameTime: new Value(0), }; const config = { duration: 300, toValue: new Value(-1), easing: Easing.inOut(Easing.ease), }; return block([ timing(clock, state, config), cond(state.finished, stopClock(clock)), ]); };
In our case we creating a timing
animation. We pass in our clock
that the timing animation will work off of. Our state
of animated values, and finally our config. We then use our cond
which will execute when a condition is met. In our case when state.finished
that is controlled by the clock is true then we'll stop our clock.
Now we need to bring in State
from gesture handler so we can compare our gestureState
to what is happening to our TapGestureHandler
. Additionally we'll bring a bunch of other necessary reanimated functions.
import { State, TapGestureHandler } from "react-native-gesture-handler"; const { cond, eq, set, neq, and, Value, event, Clock, startClock, stopClock, timing, block, interpolate, Extrapolate, } = Animated;
Lets look at our block
and break it down. There are two phases of the opacity animation. When you press we'll animate the opacity out. Then as soon as you release it will animate the button to full opacity. This may or may not be once the timing animation has completely finished.
So our comparison here starts out with a cond
which just means when a condition is met do something. In our case we want to look at 2 things so we use and
. When our gestureState
(the state of the user tapping) has BEGAN
and also is neq
(not equal) to 1. Then we do some set
calls to reset our animation if it were already running. This resets it to a fresh state. Notice we didn't reset our position though. This will ensure the appropriate progress is made from the existing opacity of the button. If we reset this to either 1 or 0 the opacity would completely reset to either being visible or not visible and we want it to reverse from the current location that the user let up at.
We set our toValue
to 1
. Remember it started at -1. Then run our startClock
to kick off our timing
animation.
cond(and(eq(gestureState, State.BEGAN), neq(config.toValue, 1)), [ set(state.finished, 0), set(state.time, 0), set(state.frameTime, 0), set(config.toValue, 1), startClock(clock), ]),
Next up we do the same thing but for when the tap gesture has entered the END
phase, aka when the user lifts their finger. Again we do a neq
to compare to the toValue
.
cond(and(eq(gestureState, State.END), neq(config.toValue, 0)), [ set(state.finished, 0), set(state.time, 0), set(state.frameTime, 0), set(config.toValue, 0), startClock(clock), ]),
The reason the neq(config.toValue, 0)
or the comparison to the toValue
is necessary is because of how Reanimated works. It will look at all the nodes and subnodes for any changes to determine if the any segment of the our declaration needs to be run.
So the gestureState
and additionally our clock
are nodes that are constantly changing. So what that means if we didn't add in our neq
then the set
calls would always be called and our animation would never trigger.
When dealing with clocks you must be very explicit about the conditions that you want otherwise you may have unintended consequences.
So finally put all together it would look like this.
return block([ cond(and(eq(gestureState, State.BEGAN), neq(config.toValue, 1)), [ set(state.finished, 0), set(state.time, 0), set(state.frameTime, 0), set(config.toValue, 1), startClock(clock), ]), cond(and(eq(gestureState, State.END), neq(config.toValue, 0)), [ set(state.finished, 0), set(state.time, 0), set(state.frameTime, 0), set(config.toValue, 0), startClock(clock), ]), timing(clock, state, config), cond(state.finished, stopClock(clock)), ]);
So finally the last thing we need to do is reverse our position
with interpolate
. What happens is our position
on our clock
will transition from 0
to 1
. If we want our button to appear in the beginning and be visible that means our final return value must be 1
.
So with our interpolate
we look at state.position
which starts at 0
and then flip it around with our outputRange
. So the opacity will be 1
when position
is 0
. Extrapolate isn't totally necessary here but it will clamp our output to only be between 0
and 1
. Never higher or lower.
interpolate(state.position, { inputRange: [0, 1], outputRange: [1, 0], extrapolate: Extrapolate.CLAMP, }),
Finally look at the position at which we have our interpolate
placed. Our this.opacity
is getting assigned this return value. The final value that our Animated.View
and subsequently the opacity
value is going to be whatever is placed at the end. So in our case we need to put the interpolate
at the end. The rest will all just be executed, and the final interpolate
will be our opacity value.
return block([ cond(and(eq(gestureState, State.BEGAN), neq(config.toValue, 1)), [ set(state.finished, 0), set(state.time, 0), set(state.frameTime, 0), set(config.toValue, 1), startClock(clock), ]), cond(and(eq(gestureState, State.END), neq(config.toValue, 0)), [ set(state.finished, 0), set(state.time, 0), set(state.frameTime, 0), set(config.toValue, 0), startClock(clock), ]), timing(clock, state, config), cond(state.finished, stopClock(clock)), interpolate(state.position, { inputRange: [0, 1], outputRange: [1, 0], extrapolate: Extrapolate.CLAMP, }), ]);
Here we place our this.opacity
on a style object for opacity
.
render() { return ( <View style={styles.container}> <TapGestureHandler minDist={0} onHandlerStateChange={this.onStateChange}> <Animated.View style={[ styles.rect, { opacity: this.opacity, }, ]} /> </TapGestureHandler> </View> ); }
We're complete. We've create a button that when tapped and held will animate all the way to 0
opacity and when released will animate back to 1
opacity. This may seem complicated for a opacity fade in fade out but the power of reanimated comes with complex animations that are intensive on the bridge. I
Check out the live demo here