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:
Easingfunctions likeEasing.bezierorEasing.bouncewithTimingto set an animation to happen over a set amount of timewithSpringto 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;
};
useSharedValuecreates a value that can be accessed from the UI ThreaduseAnimatedStyleuses a worklet which can returns a stylewithSpringcreates 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