react-native/docs/Animations.md

493 lines
20 KiB
Markdown

---
id: animations
title: Animations
layout: docs
category: Guides
permalink: docs/animations.html
next: navigation
previous: handling-touches
---
Animations are very important to create a great user experience.
Stationary objects must overcome inertia as they start moving.
Objects in motion have momentum and rarely come to a stop immediately.
Animations allow you to convey physically believable motion in your interface.
React Native provides two complementary animation systems:
[`Animated`](docs/animations.html#animated-api) for granular and interactive control of specific values, and
[`LayoutAnimation`](docs/animations.html#layoutanimation) for animated global layout transactions.
## `Animated` API
The [`Animated`](docs/animated.html) API is designed to make it very easy to concisely express a wide variety of interesting animation and interaction patterns in a very performant way.
`Animated` focuses on declarative relationships between inputs and outputs, with configurable transforms in between, and simple `start`/`stop` methods to control time-based animation execution.
`Animated` exports four animatable component types: `View`, `Text`, `Image`, and `ScrollView`, but you can also create your own using `Animated.createAnimatedComponent()`.
For example, a container view that fades in when it is mounted may look like this:
```SnackPlayer
import React from 'react';
import { Animated, Text, View } from 'react-native';
class FadeInView extends React.Component {
state = {
fadeAnim: new Animated.Value(0), // Initial value for opacity: 0
}
componentDidMount() {
Animated.timing( // Animate over time
this.state.fadeAnim, // The animated value to drive
{
toValue: 1, // Animate to opacity: 1 (opaque)
duration: 10000, // Make it take a while
}
).start(); // Starts the animation
}
render() {
let { fadeAnim } = this.state;
return (
<Animated.View // Special animatable View
style={{
...this.props.style,
opacity: fadeAnim, // Bind opacity to animated value
}}
>
{this.props.children}
</Animated.View>
);
}
}
// You can then use your `FadeInView` in place of a `View` in your components:
export default class App extends React.Component {
render() {
return (
<View style={{flex: 1, alignItems: 'center', justifyContent: 'center'}}>
<FadeInView style={{width: 250, height: 50, backgroundColor: 'powderblue'}}>
<Text style={{fontSize: 28, textAlign: 'center', margin: 10}}>Fading in</Text>
</FadeInView>
</View>
)
}
}
```
Let's break down what's happening here.
In the `FadeInView` constructor, a new `Animated.Value` called `fadeAnim` is initialized as part of `state`.
The opacity property on the `View` is mapped to this animated value.
Behind the scenes, the numeric value is extracted and used to set opacity.
When the component mounts, the opacity is set to 0.
Then, an easing animation is started on the `fadeAnim` animated value,
which will update all of its dependent mappings (in this case, just the opacity) on each frame as the value animates to the final value of 1.
This is done in an optimized way that is faster than calling `setState` and re-rendering.
Because the entire configuration is declarative, we will be able to implement further optimizations that serialize the configuration and runs the animation on a high-priority thread.
### Configuring animations
Animations are heavily configurable. Custom and predefined easing functions, delays, durations, decay factors, spring constants, and more can all be tweaked depending on the type of animation.
`Animated` provides several animation types, the most commonly used one being [`Animated.timing()`](docs/animated.html#timing).
It supports animating a value over time using one of various predefined easing functions, or you can use your own.
Easing functions are typically used in animation to convey gradual acceleration and deceleration of objects.
By default, `timing` will use a easeInOut curve that conveys gradual acceleration to full speed and concludes by gradually decelerating to a stop.
You can specify a different easing function by passing a `easing` parameter.
Custom `duration` or even a `delay` before the animation starts is also supported.
For example, if we want to create a 2-second long animation of an object that slightly backs up before moving to its final position:
```javascript
Animated.timing(
this.state.xPosition,
{
toValue: 100,
easing: Easing.back,
duration: 2000,
}
).start();
```
Take a look at the [Configuring animations](docs/animated.html#configuring-animations) section of the `Animated` API reference to learn more about all the config parameters supported by the built-in animations.
### Composing animations
Animations can be combined and played in sequence or in parallel.
Sequential animations can play immediately after the previous animation has finished,
or they can start after a specified delay.
The `Animated` API provides several methods, such as `sequence()` and `delay()`,
each of which simply take an array of animations to execute and automatically calls `start()`/`stop()` as needed.
For example, the following animation coasts to a stop, then it springs back while twirling in parallel:
```javascript
Animated.sequence([ // decay, then spring to start and twirl
Animated.decay(position, { // coast to a stop
velocity: {x: gestureState.vx, y: gestureState.vy}, // velocity from gesture release
deceleration: 0.997,
}),
Animated.parallel([ // after decay, in parallel:
Animated.spring(position, {
toValue: {x: 0, y: 0} // return to start
}),
Animated.timing(twirl, { // and twirl
toValue: 360,
}),
]),
]).start(); // start the sequence group
```
If one animation is stopped or interrupted, then all other animations in the group are also stopped.
`Animated.parallel` has a `stopTogether` option that can be set to `false` to disable this.
You can find a full list of composition methods in the [Composing animations](docs/animated.html#composing-animations) section of the `Animated` API reference.
### Combining animated values
You can [combine two animated values](docs/animated.html#combining-animated-values) via addition, multiplication, division, or modulo to make a new animated value.
There are some cases where an animated value needs to invert another animated value for calculation.
An example is inverting a scale (2x --> 0.5x):
```javascript
const a = Animated.Value(1);
const b = Animated.divide(1, a);
Animated.spring(a, {
toValue: 2,
}).start();
```
### Interpolation
Each property can be run through an interpolation first.
An interpolation maps input ranges to output ranges,
typically using a linear interpolation but also supports easing functions.
By default, it will extrapolate the curve beyond the ranges given, but you can also have it clamp the output value.
A simple mapping to convert a 0-1 range to a 0-100 range would be:
```javascript
value.interpolate({
inputRange: [0, 1],
outputRange: [0, 100],
});
```
For example, you may want to think about your `Animated.Value` as going from 0 to 1,
but animate the position from 150px to 0px and the opacity from 0 to 1.
This can easily be done by modifying `style` from the example above like so:
```javascript
style={{
opacity: this.state.fadeAnim, // Binds directly
transform: [{
translateY: this.state.fadeAnim.interpolate({
inputRange: [0, 1],
outputRange: [150, 0] // 0 : 150, 0.5 : 75, 1 : 0
}),
}],
}}
```
[`interpolate()`](docs/animated.html#interpolate) supports multiple range segments as well, which is handy for defining dead zones and other handy tricks.
For example, to get an negation relationship at -300 that goes to 0 at -100, then back up to 1 at 0, and then back down to zero at 100 followed by a dead-zone that remains at 0 for everything beyond that, you could do:
```javascript
value.interpolate({
inputRange: [-300, -100, 0, 100, 101],
outputRange: [300, 0, 1, 0, 0],
});
```
Which would map like so:
```
Input | Output
------|-------
-400| 450
-300| 300
-200| 150
-100| 0
-50| 0.5
0| 1
50| 0.5
100| 0
101| 0
200| 0
```
`interpolate()` also supports mapping to strings, allowing you to animate colors as well as values with units. For example, if you wanted to animate a rotation you could do:
```javascript
value.interpolate({
inputRange: [0, 360],
outputRange: ['0deg', '360deg']
})
```
`interpolate()` also supports arbitrary easing functions, many of which are already implemented in the
[`Easing`](docs/easing.html) module.
`interpolate()` also has configurable behavior for extrapolating the `outputRange`.
You can set the extrapolation by setting the `extrapolate`, `extrapolateLeft`, or `extrapolateRight` options.
The default value is `extend` but you can use `clamp` to prevent the output value from exceeding `outputRange`.
### Tracking dynamic values
Animated values can also track other values.
Just set the `toValue` of an animation to another animated value instead of a plain number.
For example, a "Chat Heads" animation like the one used by Messenger on Android could be implemented with a `spring()` pinned on another animated value, or with `timing()` and a `duration` of 0 for rigid tracking.
They can also be composed with interpolations:
```javascript
Animated.spring(follower, {toValue: leader}).start();
Animated.timing(opacity, {
toValue: pan.x.interpolate({
inputRange: [0, 300],
outputRange: [1, 0],
}),
}).start();
```
The `leader` and `follower` animated values would be implemented using `Animated.ValueXY()`.
`ValueXY` is a handy way to deal with 2D interactions, such as panning or dragging.
It is a simple wrapper that basically contains two `Animated.Value` instances and some helper functions that call through to them,
making `ValueXY` a drop-in replacement for `Value` in many cases.
It allows us to track both x and y values in the example above.
### Tracking gestures
Gestures, like panning or scrolling, and other events can map directly to animated values using [`Animated.event`](docs/animated.html#event).
This is done with a structured map syntax so that values can be extracted from complex event objects.
The first level is an array to allow mapping across multiple args, and that array contains nested objects.
For example, when working with horizontal scrolling gestures,
you would do the following in order to map `event.nativeEvent.contentOffset.x` to `scrollX` (an `Animated.Value`):
```javascript
onScroll={Animated.event(
// scrollX = e.nativeEvent.contentOffset.x
[{ nativeEvent: {
contentOffset: {
x: scrollX
}
}
}]
)}
```
When using `PanResponder`, you could use the following code to extract the x and y positions from `gestureState.dx` and `gestureState.dy`.
We use a `null` in the first position of the array, as we are only interested in the second argument passed to the `PanResponder` handler,
which is the `gestureState`.
```javascript
onPanResponderMove={Animated.event(
[null, // ignore the native event
// extract dx and dy from gestureState
// like 'pan.x = gestureState.dx, pan.y = gestureState.dy'
{dx: pan.x, dy: pan.y}
])}
```
### Responding to the current animation value
You may notice that there is no obvious way to read the current value while animating.
This is because the value may only be known in the native runtime due to optimizations.
If you need to run JavaScript in response to the current value, there are two approaches:
- `spring.stopAnimation(callback)` will stop the animation and invoke `callback` with the final value. This is useful when making gesture transitions.
- `spring.addListener(callback)` will invoke `callback` asynchronously while the animation is running, providing a recent value.
This is useful for triggering state changes,
for example snapping a bobble to a new option as the user drags it closer,
because these larger state changes are less sensitive to a few frames of lag compared to continuous gestures like panning which need to run at 60 fps.
`Animated` is designed to be fully serializable so that animations can be run in a high performance way, independent of the normal JavaScript event loop.
This does influence the API, so keep that in mind when it seems a little trickier to do something compared to a fully synchronous system.
Check out `Animated.Value.addListener` as a way to work around some of these limitations,
but use it sparingly since it might have performance implications in the future.
### Using the native driver
The `Animated` API is designed to be serializable.
By using the [native driver](http://facebook.github.io/react-native/blog/2017/02/14/using-native-driver-for-animated.html),
we send everything about the animation to native before starting the animation,
allowing native code to perform the animation on the UI thread without having to go through the bridge on every frame.
Once the animation has started, the JS thread can be blocked without affecting the animation.
Using the native driver for normal animations is quite simple.
Just add `useNativeDriver: true` to the animation config when starting it.
```javascript
Animated.timing(this.state.animatedValue, {
toValue: 1,
duration: 500,
useNativeDriver: true, // <-- Add this
}).start();
```
Animated values are only compatible with one driver so if you use native driver when starting an animation on a value,
make sure every animation on that value also uses the native driver.
The native driver also works with `Animated.event`.
This is specially useful for animations that follow the scroll position as without the native driver,
the animation will always run a frame behind the gesture due to the async nature of React Native.
```javascript
<Animated.ScrollView // <-- Use the Animated ScrollView wrapper
scrollEventThrottle={1} // <-- Use 1 here to make sure no events are ever missed
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: this.state.animatedValue } } }],
{ useNativeDriver: true } // <-- Add this
)}
>
{content}
</Animated.ScrollView>
```
You can see the native driver in action by running the [UIExplorer sample app](https://github.com/facebook/react-native/blob/master/Examples/UIExplorer/),
then loading the Native Animated Example.
You can also take a look at the [source code](https://github.com/facebook/react-native/blob/master/Examples/UIExplorer/js/NativeAnimationsExample.js) to learn how these examples were produced.
#### Caveats
Not everything you can do with `Animated` is currently supported by the native driver.
The main limitation is that you can only animate non-layout properties:
things like `transform`, `opacity` and `backgroundColor` will work, but flexbox and position properties will not.
When using `Animated.event`, it will only work with direct events and not bubbling events.
This means it does not work with `PanResponder` but does work with things like `ScrollView#onScroll`.
### Additional examples
The UIExplorer sample app has various examples of `Animated` in use:
- [AnimatedGratuitousApp](https://github.com/facebook/react-native/tree/master/Examples/UIExplorer/js/AnimatedGratuitousApp)
- [NativeAnimationsExample](https://github.com/facebook/react-native/blob/master/Examples/UIExplorer/js/NativeAnimationsExample.js)
## `LayoutAnimation` API
`LayoutAnimation` allows you to globally configure `create` and `update`
animations that will be used for all views in the next render/layout cycle.
This is useful for doing flexbox layout updates without bothering to measure or
calculate specific properties in order to animate them directly, and is
especially useful when layout changes may affect ancestors, for example a "see
more" expansion that also increases the size of the parent and pushes down the
row below which would otherwise require explicit coordination between the
components in order to animate them all in sync.
Note that although `LayoutAnimation` is very powerful and can be quite useful,
it provides much less control than `Animated` and other animation libraries, so
you may need to use another approach if you can't get `LayoutAnimation` to do
what you want.
Note that in order to get this to work on **Android** you need to set the following flags via `UIManager`:
```javascript
UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true);
```
```SnackPlayer
import React from 'react';
import {
NativeModules,
LayoutAnimation,
Text,
TouchableOpacity,
StyleSheet,
View,
} from 'react-native';
const { UIManager } = NativeModules;
UIManager.setLayoutAnimationEnabledExperimental &&
UIManager.setLayoutAnimationEnabledExperimental(true);
export default class App extends React.Component {
state = {
w: 100,
h: 100,
};
_onPress = () => {
// Animate the update
LayoutAnimation.spring();
this.setState({w: this.state.w + 15, h: this.state.h + 15})
}
render() {
return (
<View style={styles.container}>
<View style={[styles.box, {width: this.state.w, height: this.state.h}]} />
<TouchableOpacity onPress={this._onPress}>
<View style={styles.button}>
<Text style={styles.buttonText}>Press me!</Text>
</View>
</TouchableOpacity>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
box: {
width: 200,
height: 200,
backgroundColor: 'red',
},
button: {
backgroundColor: 'black',
paddingHorizontal: 20,
paddingVertical: 15,
marginTop: 15,
},
buttonText: {
color: '#fff',
fontWeight: 'bold',
},
});
```
This example uses a preset value, you can customize the animations as
you need, see [LayoutAnimation.js](https://github.com/facebook/react-native/blob/master/Libraries/LayoutAnimation/LayoutAnimation.js)
for more information.
## Additional notes
### `requestAnimationFrame`
`requestAnimationFrame` is a polyfill from the browser that you might be
familiar with. It accepts a function as its only argument and calls that
function before the next repaint. It is an essential building block for
animations that underlies all of the JavaScript-based animation APIs. In
general, you shouldn't need to call this yourself - the animation APIs will
manage frame updates for you.
### `setNativeProps`
As mentioned [in the Direction Manipulation section](docs/direct-manipulation.html),
`setNativeProps` allows us to modify properties of native-backed
components (components that are actually backed by native views, unlike
composite components) directly, without having to `setState` and
re-render the component hierarchy.
We could use this in the Rebound example to update the scale - this
might be helpful if the component that we are updating is deeply nested
and hasn't been optimized with `shouldComponentUpdate`.
If you find your animations with dropping frames (performing below 60 frames
per second), look into using `setNativeProps` or `shouldComponentUpdate` to
optimize them. Or you could run the animations on the UI thread rather than
the JavaScript thread [with the useNativeDriver
option](http://facebook.github.io/react-native/blog/2017/02/14/using-native-driver-for-animated.html).
You may also want to defer any computationally intensive work until after
animations are complete, using the
[InteractionManager](docs/interactionmanager.html). You can monitor the
frame rate by using the In-App Developer Menu "FPS Monitor" tool.