diff --git a/docs/Animations.md b/docs/Animations.md index c1a7b2ba9..04de181e7 100644 --- a/docs/Animations.md +++ b/docs/Animations.md @@ -7,24 +7,310 @@ permalink: docs/animations.html next: accessibility --- -Fluid, meaningful animations are essential to the mobile user -experience. Animation APIs for React Native are currently under heavy -development, the recommendations in this article are intended to be up -to date with the current best-practices. +Fluid, meaningful animations are essential to the mobile user experience. Like +everything in React Native, Animation APIs for React Native are currently under +development, but have started to coalesce around two complementary systems: +`LayoutAnimation` for animated global layout transactions, and `Animated` for +more granular and interactive control of specific values. + +### Animated ### + +The `Animated` library 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. For example, a complete +component with a simple spring bounce on mount looks like this: + +```javascript +class Playground extends React.Component { + constructor(props: any) { + super(props); + this.state = { + bounceValue: new Animated.Value(0), + }; + } + render(): ReactElement { + return ( + + ); + } + componentDidMount() { + this.state.bounceValue.setValue(1.5); // Start large + Animated.spring( // Base: spring, decay, timing + this.state.bounceValue, // Animate `bounceValue` + { + toValue: 0.8, // Animate to smaller size + friction: 1, // Bouncier spring + } + ).start(); // Start the animation + } +} +``` + +`bounceValue` is initialized as part of `state` in the constructor, and mapped +to the scale transform on the image. Behind the scenes, the numeric value is +extracted and used to set scale. When the component mounts, the scale is set to +1.5 and then a spring animation is started on `bounceValue` which will update +all of its dependent mappings on each frame as the spring animates (in this +case, just the scale). 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. + +#### Core API + +Most everything you need hangs directly off the `Animated` module. This +includes two value types, `Value` for single values and `ValueXY` for vectors, +three animation types, `spring`, `decay`, and `timing`, and three component +types, `View`, `Text`, and `Image`. You can make any other component animated with +`Animated.createAnimatedComponent`. + +The three animation types can be used to create almost any animation curve you +want because each can be customized: + +* `spring`: Simple single-spring physics model that matches [Origami](https://facebook.github.io/origami/). + * `friction`: Controls "bounciness"/overshoot. Default 7. + * `tension`: Controls speed. Default 40. +* `decay`: Starts with an initial velocity and gradually slows to a stop. + * `velocity`: Initial velocity. Required. + * `deceleration`: Rate of decay. Default 0.997. +* `timing`: Maps time range to easing value. + * `duration`: Length of animation (milliseconds). Default 500. + * `easing`: Easing function to define curve. See `Easing` module for several + predefined functions. iOS default is `Easing.inOut(Easing.ease)`. + * `delay`: Start the animation after delay (milliseconds). Default 0. + +Animations are started by calling `start`. `start` takes a completion callback +that will be called when the animation is done. If the animation is done +because it finished running normally, the completion callback will be invoked +with `{finished: true}`, but if the animation is done because `stop` was called +on it before it could finish (e.g. because it was interrupted by a gesture or +another animation), then it will receive `{finished: false}`. + +#### Composing Animations + +Animations can also be composed with `parallel`, `sequence`, `stagger`, and +`delay`, each of which simply take an array of animations to execute and +automatically calls start/stop as appropriate. For example: + +```javascript +Animated.sequence([ // spring to start and twirl after decay finishes + 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 +``` + +By default, if one animation is stopped or interrupted, then all other +animations in the group are also stopped. Parallel has a `stopTogether` option +that can be set to `false` to disable this. + +#### Interpolation + +Another powerful part of the `Animated` API is the `interpolate` function. It +allows input ranges to map to different output ranges. For example, 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], +}); +``` + +`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 + +`interpolation` also supports arbitrary easing functions, many of which are +already implemented in the +[`Easing`](https://github.com/facebook/react-native/blob/master/Libraries/Animation/Animated/Easing.js) +class including quadradic, exponential, and bezier curves as well as functions +like step and bounce. `interpolation` also has configurable behavior for +extrapolation, the default being `'extend'`, but `'clamp'` is also very useful +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 with +spring physics for an interaction like "Chat Heads", or via `timing` with +`duration: 0` for rigid/instant 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(); +``` + +`ValueXY` is a handy way to deal with 2D interactions, such as panning/dragging. +It is a simple wrapper that basically just 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. For example, in the code +snippet above, `leader` and `follower` could both be of type `ValueXY` and the x +and y values will both track as you would expect. + +#### Input Events + +`Animated.event` is the input side of the Animated API, allowing gestures and +other events to map directly to animated values. 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. In the example, you can see that `scrollX` maps to +`event.nativeEvent.contentOffset.x` (`event` is normally the first arg to the +handler), and `pan.x` maps to `gestureState.dx` (`gestureState` is the second +arg passed to the `PanResponder` handler). + +```javascript +onScroll={Animated.event( + [{nativeEvent: {contentOffset: {y: pan.y}}}] // pan.y = e.nativeEvent.contentOffset.y +)} +onPanResponderMove={Animated.event([ + null, // ignore the native event + {dx: pan.x, dy: pan.y} // extract dx and dy from gestureState +]); +``` + +#### 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 60fps. + +#### Future Work + +As previously mentioned, we're planning on optimizing Animated under the hood to +make it even more performant. We would also like to experiment with more +declarative and higher level gestures and triggers, such as horizontal vs. +vertical panning. + +The above API gives a powerful tool for expressing all sorts of animations in a +concise, robust, and performant way. Check out more example code in +[UIExplorer/AnimationExample](https://github.com/facebook/react-native/blob/master/Examples/UIExplorer/AnimationExample). Of course there may still be times where `Animated` +doesn't support what you need, and the following sections cover other animation +systems. + +### LayoutAnimation + +`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. + +![](/react-native/img/LayoutAnimationExample.gif) + +```javascript +var App = React.createClass({ + componentWillMount() { + // Animate creation + LayoutAnimation.spring(); + }, + + getInitialState() { + return { w: 100, h: 100 } + }, + + _onPress() { + // Animate the update + LayoutAnimation.spring(); + this.setState({w: this.state.w + 15, h: this.state.h + 15}) + }, + + render: function() { + return ( + + + + + Press me! + + + + ); + } +}); +``` +[Run this example](https://rnplay.org/apps/uaQrGQ) + +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/Animation/LayoutAnimation.js) +for more information. ### 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. +animations that underlies all of the JavaScript-based animation APIs. In +general, you shouldn't need to call this yourself - the animation API's will +manage frame updates for you. -### JavaScript-based Animation APIs - -These APIs do all of the calculations in JavaScript, then send over -updated properties to the native side on each frame. - -#### react-tween-state +### react-tween-state [react-tween-state](https://github.com/chenglou/react-tween-state) is a minimal library that does exactly what its name suggests: it *tweens* a @@ -91,7 +377,7 @@ Here we animated the opacity, but as you might guess, we can animate any numeric value. Read more about react-tween-state in its [README](https://github.com/chenglou/react-tween-state). -#### Rebound +### Rebound [Rebound.js](https://github.com/facebook/rebound-js) is a JavaScript port of [Rebound for Android](https://github.com/facebook/rebound). It is @@ -232,7 +518,7 @@ using the can monitor the frame rate by using the In-App Developer Menu "FPS Monitor" tool. -#### Navigator Scene Transitions +### Navigator Scene Transitions As mentioned in the [Navigator Comparison](https://facebook.github.io/react-native/docs/navigator-comparison.html#content), @@ -271,53 +557,7 @@ var CustomSceneConfig = Object.assign({}, BaseConfig, { For further information about customizing scene transitions, [read the source](https://github.com/facebook/react-native/blob/master/Libraries/CustomComponents/Navigator/NavigatorSceneConfigs.js). -### Native-based Animation APIs - -#### LayoutAnimation - -LayoutAnimation allows you to globally configure `create` and `update` -animations that will be used for all views in the next render cycle. - -![](/react-native/img/LayoutAnimationExample.gif) - -```javascript -var App = React.createClass({ - componentWillMount() { - // Animate creation - LayoutAnimation.configureNext(LayoutAnimation.Presets.spring); - }, - - getInitialState() { - return { w: 100, h: 100 } - }, - - _onPress() { - // Animate the update - LayoutAnimation.configureNext(LayoutAnimation.Presets.spring); - this.setState({w: this.state.w + 15, h: this.state.h + 15}) - }, - - render: function() { - return ( - - - - - Press me! - - - - ); - } -}); -``` -[Run this example](https://rnplay.org/apps/uaQrGQ) - -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/Animation/LayoutAnimation.js) -for more information. - -#### AnimationExperimental *(Deprecated)* +### AnimationExperimental *(Deprecated)* As the name would suggest, this was only ever an experimental API and it is **not recommended to use this on your apps**. It has some rough edges @@ -379,7 +619,7 @@ AnimationExperimental.startAnimation( ![](/react-native/img/AnimationExperimentalOpacity.gif) -#### Pop *(Unsupported, not recommended)* +### Pop *(Unsupported, not recommended)* [Facebook Pop](https://github.com/facebook/pop) "supports spring and decay dynamic animations, making it useful for building realistic,