diff --git a/docs/Animations.md b/docs/Animations.md new file mode 100644 index 00000000000000..191aab913c9b01 --- /dev/null +++ b/docs/Animations.md @@ -0,0 +1,394 @@ +--- +id: animations +title: Animations +layout: docs +category: Guides +permalink: docs/animations.html +next: nativemodulesios +--- + +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. + +### 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. + +### 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](https://github.com/chenglou/react-tween-state) is a +minimal library that does exactly what its name suggests: it *tweens* a +value in a component's state, starting at a **from** value and ending at +a **to** value. This means that it generates the values in between those +two values, and it sets the state on every `requestAnimationFrame` with +the intermediary value. + +> Tweening definition from [Wikipedia](https://en.wikipedia.org/wiki/Inbetweening) +> +> "... tweening is the process of generating intermediate frames between two +> images to give the appearance that the first image evolves smoothly +> into the second image. [Tweens] are the drawings between the key +> frames which help to create the illusion of motion." + +The most obvious way to animate from one value to another is linearly: +you subtract the end value from the start value and divide the result by +the number of frames over which the animation occurs, and then add that +value to the current value on each frame until the end value is reached. +Linear easing often looks awkward and unnatural, so react-tween-state +provides a selection of popular [easing functions](http://easings.net/) +that can be applied to make your animations more pleasing. + +This library does not ship with React Native - in order to use it on +your project, you will need to install it with `npm i react-tween-state +--save` from your project directory. + +```javascript +var tweenState = require('react-tween-state'); + +var App = React.createClass({ + mixins: [tweenState.Mixin], + + getInitialState() { + return { opacity: 1 } + }, + + _animateOpacity() { + this.tweenState('opacity', { + easing: tweenState.easingTypes.easeOutQuint, + duration: 1000, + endValue: this.state.opacity === 0.2 ? 1 : 0.2, + }); + }, + + render() { + return ( + + + this._box = component} + style={{width: 200, height: 200, backgroundColor: 'red', + opacity: this.getTweeningValue('opacity')}} /> + + + ) + }, +}); +``` +[Run this example](https://rnplay.org/apps/4FUQ-A) + +![](/react-native/img/TweenState.gif) + +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.js](https://github.com/facebook/rebound-js) is a JavaScript port of +[Rebound for Android](https://github.com/facebook/rebound). It is +similar in concept to react-tween-state: you have an initial value and +set an end value, then Rebound generates intermediate values that you can +use for your animation. Rebound is modeled after spring physics; we +don't provide a duration when animating with springs, it is +calculated for us depending on the spring tension, friction, current +value and end value. Rebound [is used +internally](https://github.com/facebook/react-native/search?utf8=%E2%9C%93&q=rebound) +by React Native on `Navigator` and `WarningBox`. + +![](/react-native/img/ReboundImage.gif) + +Notice that Rebound animations can be interrupted - if you release in +the middle of a press, it will animate back from the current state to +the original value. + +```javascript +var rebound = require('rebound'); + +var App = React.createClass({ + // First we initialize the spring and add a listener, which calls + // setState whenever it updates + componentWillMount() { + // Initialize the spring that will drive animations + this.springSystem = new rebound.SpringSystem(); + this._scrollSpring = this.springSystem.createSpring(); + var springConfig = this._scrollSpring.getSpringConfig(); + springConfig.tension = 230; + springConfig.friction = 10; + + this._scrollSpring.addListener({ + onSpringUpdate: () => { + this.setState({scale: this._scrollSpring.getCurrentValue()}); + }, + }); + + // Initialize the spring value at 1 + this._scrollSpring.setCurrentValue(1); + }, + + _onPressIn() { + this._scrollSpring.setEndValue(0.5); + }, + + _onPressOut() { + this._scrollSpring.setEndValue(1); + }, + + render: function() { + var imageStyle = { + width: 250, + height: 200, + transform: [{scaleX: this.state.scale}, {scaleY: this.state.scale}], + }; + + var imageUri = "https://facebook.github.io/react-native/img/ReboundExample.png"; + + return ( + + + + + + ); + } +}); +``` +[Run this example](https://rnplay.org/apps/NNI5eA) + +You can also clamp the spring values so that they don't overshoot and +oscillate around the end value. In the above example, we would add +`this._scrollSpring.setOvershootClampingEnabled(true)` to change this. +See the below gif for an example of where in your interface you might +use this. + +![](/react-native/img/Rebound.gif) Screenshot from +[react-native-scrollable-tab-view](https://github.com/brentvatne/react-native-scrollable-tab-view). +You can run a simlar example [here](https://rnplay.org/apps/qHU_5w). + +#### A sidenote about setNativeProps + +As mentioned [in the Direction Manipulation section](/react-native/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`. + +```javascript +// Outside of our React component +var precomputeStyle = require('precomputeStyle'); + +// Back inside of the App component, replace the scrollSpring listener +// in componentWillMount with this: +this._scrollSpring.addListener({ + onSpringUpdate: () => { + if (!this._photo) { return } + var v = this._scrollSpring.getCurrentValue(); + var newProps = precomputeStyle({transform: [{scaleX: v}, {scaleY: v}]}); + this._photo.setNativeProps(newProps); + }, +}); + +// Lastly, we update the render function to no longer pass in the +// transform via style (avoid clashes when re-rendering) and to set the +// photo ref +render: function() { + return ( + + + this._photo = component} + source={{uri: "https://facebook.github.io/react-native/img/ReboundExample.png"}} + style={{width: 250, height: 200}} /> + + + ); +} +``` +[Run this example](https://rnplay.org/apps/fUqjAg) + +It would not make sense to use `setNativeProps` with react-tween-state +because the updated tween values are set on the state automatically by +the library - Rebound on the other hand gives us an updated value for +each frame with the `onSpringUpdate` function. + +If you find your animations with dropping frames (performing below 60 +frames per second), look into using `setNativeProps` or +`shouldComponentUpdate` to optimize them. You may also want to defer any +computationally intensive work until after animations are complete, +using the +[InteractionManager](/react-native/docs/interactionmanager.html). You +can monitor the frame rate by using the In-App Developer Menu "FPS +Monitor" tool. + +#### Navigator Scene Transitions + +As mentioned in the [Navigator +Comparison](https://facebook.github.io/react-native/docs/navigator-comparison.html#content), +`Navigator` is implemented in JavaScript and `NavigatorIOS` is a wrapper +around native functionality provided by `UINavigationController`, so +these scene transitions apply only to `Navigator`. In order to re-create +the various animations provided by `UINavigationController` and also +make them customizable, React Native exposes a +[NavigatorSceneConfigs](https://github.com/facebook/react-native/blob/master/Libraries/CustomComponents/Navigator/NavigatorSceneConfigs.js) API. + +```javascript +var SCREEN_WIDTH = require('Dimensions').get('window').width; +var BaseConfig = Navigator.SceneConfigs.FloatFromRight; + +var CustomLeftToRightGesture = Object.assign({}, BaseConfig.gestures.pop, { + // Make it snap back really quickly after canceling pop + snapVelocity: 8, + + // Make it so we can drag anywhere on the screen + edgeHitWidth: SCREEN_WIDTH, +}); + +var CustomSceneConfig = Object.assign({}, BaseConfig, { + // A very tighly wound spring will make this transition fast + springTension: 100, + springFriction: 1, + + // Use our custom gesture defined above + gestures: { + pop: CustomLeftToRightGesture, + } +}); +``` +[Run this example](https://rnplay.org/apps/HPy6UA) + +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)* + +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 +and is not under active development. It is built on top of CoreAnimation +explicit animations. + +If you choose to use it anyways, here is what you need to know: + +- You will need to include `RCTAnimationExperimental.xcodeproj` and add + `libRCTAnimationExperimental.a` to `Build Phases`. +- Suited only for static "fire and forget" animations - not continuous gestures. +- Hit detection will not work as expected because animations occur on + the presentation layer. + +```javascript +var AnimationExperimental = require('AnimationExperimental'); + +var App = React.createClass({ + componentDidMount() { + AnimationExperimental.startAnimation( + { + node: this._box, + duration: 1000, + easing: 'easeInOutBack', + property: 'scaleXY', + toValue: { x: 1, y: 1 }, + }, + ); + }, + + render() { + return ( + + this._box = component} + style={{width: 200, height: 200, backgroundColor: 'red'}} /> + + ) + }, +}); +``` +![](/react-native/img/AnimationExperimentalScaleXY.gif) + +Now to demonstrate a known issue, and one of the reasons why it is +recommended not to use `AnimationExperimental` currently, let's try to +animate `opacity` from 1 to 0.5: + +```javascript +AnimationExperimental.startAnimation( + { + node: this._box, + duration: 1000, + easing: 'easeInOutBack', + property: 'opacity', + fromValue: 1, + toValue: 0.5, + }, +); +``` + +![](/react-native/img/AnimationExperimentalOpacity.gif) + +#### Pop *(Unsupported, not recommended)* + +[Facebook Pop](https://github.com/facebook/pop) "supports spring and +decay dynamic animations, making it useful for building realistic, +physics-based interactions." + +This is not officially supported or recommended because the direction is +to move towards JavaScript-driven animations, but if you must use it, +you can find the code to integrate with React Native +[here](https://github.com/facebook/react-native/issues/1365#issuecomment-104792251). +Please do not open questions specific to Pop on the React Native issues, +StackOverflow is a better place to answer those questions as it is not +considered to be part of the core. diff --git a/docs/GestureResponderSystem.md b/docs/GestureResponderSystem.md index 631000118e18cf..fac70d0d28c4c3 100644 --- a/docs/GestureResponderSystem.md +++ b/docs/GestureResponderSystem.md @@ -4,7 +4,7 @@ title: Gesture Responder System layout: docs category: Guides permalink: docs/gesture-responder-system.html -next: nativemodulesios +next: animations --- Gesture recognition on mobile devices is much more complicated than web. A touch can go through several phases as the app determines what the user's intention is. For example, the app needs to determine if the touch is scrolling, sliding on a widget, or tapping. This can even change during the duration of a touch. There can also be multiple simultaneous touches. @@ -68,4 +68,4 @@ However, sometimes a parent will want to make sure that it becomes responder. Th ### PanResponder -For higher-level gesture interpretation, check out [PanResponder](/react-native/docs/panresponder.html). \ No newline at end of file +For higher-level gesture interpretation, check out [PanResponder](/react-native/docs/panresponder.html). diff --git a/website/src/react-native/img/AnimationExperimentalOpacity.gif b/website/src/react-native/img/AnimationExperimentalOpacity.gif new file mode 100644 index 00000000000000..cdc79022b7efbd Binary files /dev/null and b/website/src/react-native/img/AnimationExperimentalOpacity.gif differ diff --git a/website/src/react-native/img/AnimationExperimentalScaleXY.gif b/website/src/react-native/img/AnimationExperimentalScaleXY.gif new file mode 100644 index 00000000000000..850cfd18c2b687 Binary files /dev/null and b/website/src/react-native/img/AnimationExperimentalScaleXY.gif differ diff --git a/website/src/react-native/img/LayoutAnimationExample.gif b/website/src/react-native/img/LayoutAnimationExample.gif new file mode 100644 index 00000000000000..68fcfcee4d77ba Binary files /dev/null and b/website/src/react-native/img/LayoutAnimationExample.gif differ diff --git a/website/src/react-native/img/Rebound.gif b/website/src/react-native/img/Rebound.gif new file mode 100644 index 00000000000000..03716633818f84 Binary files /dev/null and b/website/src/react-native/img/Rebound.gif differ diff --git a/website/src/react-native/img/ReboundExample.png b/website/src/react-native/img/ReboundExample.png new file mode 100644 index 00000000000000..db33e5f8a8532a Binary files /dev/null and b/website/src/react-native/img/ReboundExample.png differ diff --git a/website/src/react-native/img/ReboundImage.gif b/website/src/react-native/img/ReboundImage.gif new file mode 100644 index 00000000000000..9c1da74f150e26 Binary files /dev/null and b/website/src/react-native/img/ReboundImage.gif differ diff --git a/website/src/react-native/img/TweenState.gif b/website/src/react-native/img/TweenState.gif new file mode 100644 index 00000000000000..84f34d2ec8f0e5 Binary files /dev/null and b/website/src/react-native/img/TweenState.gif differ