React Native Reanimated
Declarative Animation in React-Native
References:
- the Reanimated docs
- the Release Intro Blog Post
- this Intro to React Native Reanimated 2
- this Webinar on React Native Reanimated 2
Initialize an App
To create a new app, do the following using the Expo CLI (I've covered the basics for this here)
expo init native-animation
And then select the option for Blank (Typescript)
Install Reanimated
yarn add react-native-reanimated
Optionally, to add support for Web update your babel.config.js
file add the reanimated
plugin: react-native-reanimated/plugin
, so your file should now look like this:
module.exports = function (api) {
api.cache(true);
return {
presets: ["babel-preset-expo"],
plugins: ["react-native-reanimated/plugin"],
};
};
Updating Views
Usually when creating components you can mix static and dynamic styles, this allows us to mix styles from our StyleSheet
with styles from a useAnimatedStyle
hook, so something like this:
const animatedStyles = useAnimatedStyle(() => {
return {
transform: [{translateX: 100}]
}
})
return <Animated.View style={[styles.box, animatedStyles]}/>
Managing Animated State
Shared Values are values that can be read from both the JS and UI Threads and help to:
- Carry data
- Drive Animations
- Provide Relativeness
Can be created using the useSharedValue
hook, similar to Animated.value
but can carry any type of data that we want
These values can also be directly assigned to in order to update them
const progress = useSharedValue(0)
const animatedStyles = useAnimatedStyle(() => {
return {
transform: [{translateX: progress.value * 100}]
}
})
// can be modified directly - they are reactive
const onPress = () => progress.value += 1
return <Animated.View style={[styles.box, animatedStyles]}/>
Another hook we can use to calculate a value based on the value of sharedValue
is the useDerivedValue
hook, which can be used in association with the above example like so:
const progress = useSharedValue(0)
const translateX = useDerivedValue(() => progress.value * 100)
const animatedStyles = useAnimatedStyle(() => {
return {
transform: [{translateX}]
}
})
const onPress = () => progress.value += 1
return <Animated.View style={[styles.box, animatedStyles]}/>
Animation
Whe working with animations there's a concept of an animation assigner, this is basically a function that creates an animated value that can then be assigned to a shared value. Some functions available for this are:
Easing
functions likeEasing.bezier
orEasing.bounce
withTiming
to set an animation to happen over a set amount of timewithSpring
to make an animation springy
These can be used like so:
const style = useAnimatedStyle(() => {
const duration = 500;
const w = withSpring(width.value, {duration, easing: Easing.bounce});
return {
width: w,
};
}, []);
Additionally there are modifiers that can be used with an animation, something like delay
:
const w = delay(100, withSpring(width.value))
Gestures
Gesture handling in React Native is usually done using the react-native-gesture-handler
library. At the top-level the library exposes gestureHandlerRootHOC
and GestureHandlerRootView
. Before your application will be able to register gestures you have to wrap your application with either the HOC
or RootView
mentioned above
This would be done at the top-level of your app like so
Using the gestureHandlerRootHOC
:
App.tsx
import { gestureHandlerRootHOC } from 'react-native-gesture-handler'
// ...
function App() {
// app component stuff
}
export default gestureHandlerRootHOC(App)
Or using GestureHandlerRootView
:
App.tsx
import { GestureHandlerRootView } from 'react-native-gesture-handler'
// ...
export default function App() {
// app component stuff
return (
<GestureHandlerRootView>
{/* rest of app */}
</GestureHandlerRootView>
)
}
When handling gestures we have a useAnimatedGestureHandler
which can take an object of method handlers, for example setting an onActive
handler like below
import { PanGestureHandler } from "react-native-gesture-handler";
// ...
const translateX = useSharedValue(0.0);
const translateY = useSharedValue(0.0);
const gestureHandler = useAnimatedGestureHandler({
onActive: (e) => {
translateX.value = e.translationX;
translateY.value = e.translationY;
},
});
const style = useAnimatedStyle(() => {
return {
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
],
};
}, []);
return (
<PanGestureHandler onGestureEvent={gestureHandler}>
<Animated.View style={[styles.box, style]} />
</PanGestureHandler>
);
We can also add an animation to the value we calculate, so something like withSpring
const gestureHandler = useAnimatedGestureHandler({
onActive: (e) => {
translateX.value = withSpring(e.translationX);
translateY.value = withSpring(e.translationY);
},
});
Worklets
In React-Native we have a few main threads in which code is executed:
- The Main JS thread where our JavaScript code is executed
- The UI Thread in which rendering Native views are done
- The Native Modules thread where Native Code is run
Reanimated adds to this by running some additional JS code in the UI Thread instead of the main JS thread, these are called worklets, and they're pretty much just functions
A Worklet can be defined using either the worklet
directive or is implied for a lambda used in the useAnimatedStyle
hook
A simple worklet can look like so:
const myWorklet = (a: number, b: number) => {
'worklet';
return a + b
}
const onPress = () => {
runOnUI(myWorklet)(1,2)
}
Or, in the useAnimatedStyle
hook:
const width = useSharedValue(200);
const style = useAnimatedStyle(() => {
// this function is also a worklet
return {
width: withSpring(width.value),
};
}, []);
const onPress = () => {
// unusual in react, but we can modify this value directly here
width.value = Math.random() * 500;
};
useSharedValue
creates a value that can be accessed from the UI ThreaduseAnimatedStyle
uses a worklet which can returns a stylewithSpring
creates an interpolated value
Using the above, we can render an element with a variable width
using the Animated.View
export default function App() {
const width = useSharedValue(200);
const style = useAnimatedStyle(() => {
const duration = 500;
const w = withSpring(width.value);
return {
width: w,
};
}, []);
const onPress = () => {
// unusual in react, but we can modify this value directly here
width.value = Math.random() * 500;
};
return (
<View style={styles.container}>
<StatusBar style="auto" />
<Animated.View style={[styles.box, style]} />
<Button color="black" onPress={onPress} title="Change Size" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
box: {
height: 200,
backgroundColor: "blue",
marginBottom: 20,
},
});
Note that though worklets can call non-worklet methods, those methods will be executed on the JS Thread and not the UI Thread