diff --git a/Examples/UIExplorer/AnimationExample/AnExApp.js b/Examples/UIExplorer/AnimationExample/AnExApp.js
new file mode 100644
index 000000000..9ea48f419
--- /dev/null
+++ b/Examples/UIExplorer/AnimationExample/AnExApp.js
@@ -0,0 +1,319 @@
+/**
+ * The examples provided by Facebook are for non-commercial testing and
+ * evaluation purposes only.
+ *
+ * Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * @providesModule AnExApp
+ * @flow
+ */
+'use strict';
+
+var React = require('react-native');
+var {
+ Animated,
+ LayoutAnimation,
+ PanResponder,
+ StyleSheet,
+ View,
+} = React;
+
+var AnExSet = require('AnExSet');
+
+var CIRCLE_SIZE = 80;
+var CIRCLE_MARGIN = 18;
+var NUM_CIRCLES = 30;
+
+class Circle extends React.Component {
+ _onLongPress: () => void;
+ _toggleIsActive: () => void;
+ constructor(props: Object): void {
+ super();
+ this._onLongPress = this._onLongPress.bind(this);
+ this._toggleIsActive = this._toggleIsActive.bind(this);
+ this.state = {isActive: false};
+ this.state.pan = new Animated.ValueXY(); // Vectors reduce boilerplate. (step1: uncomment)
+ this.state.pop = new Animated.Value(0); // Initial value. (step2a: uncomment)
+ }
+
+ _onLongPress(): void {
+ var config = {tension: 40, friction: 3};
+ this.state.pan.addListener((value) => { // Async listener for state changes (step1: uncomment)
+ this.props.onMove && this.props.onMove(value);
+ });
+ Animated.spring(this.state.pop, {
+ toValue: 1, // Pop to larger size. (step2b: uncomment)
+ ...config, // Reuse config for convenient consistency (step2b: uncomment)
+ }).start();
+ this.setState({panResponder: PanResponder.create({
+ onPanResponderMove: Animated.event([
+ null, // native event - ignore (step1: uncomment)
+ {dx: this.state.pan.x, dy: this.state.pan.y}, // links pan to gestureState (step1: uncomment)
+ ]),
+ onPanResponderRelease: (e, gestureState) => {
+ LayoutAnimation.easeInEaseOut(); // @flowfixme animates layout update as one batch (step3: uncomment)
+ Animated.spring(this.state.pop, {
+ toValue: 0, // Pop back to 0 (step2c: uncomment)
+ ...config,
+ }).start();
+ this.setState({panResponder: undefined});
+ this.props.onMove && this.props.onMove({
+ x: gestureState.dx + this.props.restLayout.x,
+ y: gestureState.dy + this.props.restLayout.y,
+ });
+ this.props.onDeactivate();
+ },
+ })}, () => {
+ this.props.onActivate();
+ });
+ }
+
+ render(): ReactElement {
+ if (this.state.panResponder) {
+ var handlers = this.state.panResponder.panHandlers;
+ var dragStyle = { // Used to position while dragging
+ position: 'absolute', // Hoist out of layout (step1: uncomment)
+ ...this.state.pan.getLayout(), // Convenience converter (step1: uncomment)
+ };
+ } else {
+ handlers = {
+ onStartShouldSetResponder: () => !this.state.isActive,
+ onResponderGrant: () => {
+ this.state.pan.setValue({x: 0, y: 0}); // reset (step1: uncomment)
+ this.state.pan.setOffset(this.props.restLayout); // offset from onLayout (step1: uncomment)
+ this.longTimer = setTimeout(this._onLongPress, 300);
+ },
+ onResponderRelease: () => {
+ if (!this.state.panResponder) {
+ clearTimeout(this.longTimer);
+ this._toggleIsActive();
+ }
+ }
+ };
+ }
+ var animatedStyle: Object = {
+ shadowOpacity: this.state.pop, // no need for interpolation (step2d: uncomment)
+ transform: [
+ {scale: this.state.pop.interpolate({
+ inputRange: [0, 1],
+ outputRange: [1, 1.3] // scale up from 1 to 1.3 (step2d: uncomment)
+ })},
+ ],
+ };
+ var openVal = this.props.openVal;
+ if (this.props.dummy) {
+ animatedStyle.opacity = 0;
+ } else if (this.state.isActive) {
+ var innerOpenStyle = [styles.open, { // (step4: uncomment)
+ left: openVal.interpolate({inputRange: [0, 1], outputRange: [this.props.restLayout.x, 0]}),
+ top: openVal.interpolate({inputRange: [0, 1], outputRange: [this.props.restLayout.y, 0]}),
+ width: openVal.interpolate({inputRange: [0, 1], outputRange: [CIRCLE_SIZE, this.props.containerLayout.width]}),
+ height: openVal.interpolate({inputRange: [0, 1], outputRange: [CIRCLE_SIZE, this.props.containerLayout.height]}),
+ margin: openVal.interpolate({inputRange: [0, 1], outputRange: [CIRCLE_MARGIN, 0]}),
+ borderRadius: openVal.interpolate({inputRange: [-0.15, 0, 0.5, 1], outputRange: [0, CIRCLE_SIZE / 2, CIRCLE_SIZE * 1.3, 0]}),
+ }];
+ }
+ return (
+
+
+
+
+
+ );
+ }
+ _toggleIsActive(velocity) {
+ var config = {tension: 30, friction: 7};
+ if (this.state.isActive) {
+ Animated.spring(this.props.openVal, {toValue: 0, ...config}).start(() => { // (step4: uncomment)
+ this.setState({isActive: false}, this.props.onDeactivate);
+ }); // (step4: uncomment)
+ } else {
+ this.props.onActivate();
+ this.setState({isActive: true, panResponder: undefined}, () => {
+ // this.props.openVal.setValue(1); // (step4: comment)
+ Animated.spring(this.props.openVal, {toValue: 1, ...config}).start(); // (step4: uncomment)
+ });
+ }
+ }
+}
+
+class AnExApp extends React.Component {
+ _onMove: (position: Point) => void;
+ constructor(props: any): void {
+ super(props);
+ var keys = [];
+ for (var idx = 0; idx < NUM_CIRCLES; idx++) {
+ keys.push('E' + idx);
+ }
+ this.state = {
+ keys,
+ restLayouts: [],
+ openVal: new Animated.Value(0),
+ };
+ this._onMove = this._onMove.bind(this);
+ }
+
+ render(): ReactElement {
+ var circles = this.state.keys.map((key, idx) => {
+ if (key === this.state.activeKey) {
+ return ;
+ } else {
+ if (!this.state.restLayouts[idx]) {
+ var onLayout = function(index, e) {
+ var layout = e.nativeEvent.layout;
+ this.setState((state) => {
+ state.restLayouts[index] = layout;
+ return state;
+ });
+ }.bind(this, idx);
+ }
+ return (
+
+ );
+ }
+ });
+ if (this.state.activeKey) {
+ circles.push(
+
+ );
+ circles.push(
+ { this.setState({activeKey: undefined}); }}
+ />
+ );
+ }
+ return (
+
+ this.setState({layout: e.nativeEvent.layout})}>
+ {circles}
+
+
+ );
+ }
+
+ _onMove(position: Point): void {
+ var newKeys = moveToClosest(this.state, position);
+ if (newKeys !== this.state.keys) {
+ LayoutAnimation.easeInEaseOut(); // animates layout update as one batch (step3: uncomment)
+ this.setState({keys: newKeys});
+ }
+ }
+}
+
+type Point = {x: number, y: number};
+function distance(p1: Point, p2: Point): number {
+ var dx = p1.x - p2.x;
+ var dy = p1.y - p2.y;
+ return dx * dx + dy * dy;
+}
+
+function moveToClosest({activeKey, keys, restLayouts}, position) {
+ var activeIdx = -1;
+ var closestIdx = activeIdx;
+ var minDist = Infinity;
+ var newKeys = [];
+ keys.forEach((key, idx) => {
+ var dist = distance(position, restLayouts[idx]);
+ if (key === activeKey) {
+ idx = activeIdx;
+ } else {
+ newKeys.push(key);
+ }
+ if (dist < minDist) {
+ minDist = dist;
+ closestIdx = idx;
+ }
+ });
+ if (closestIdx === activeIdx) {
+ return keys; // nothing changed
+ } else {
+ newKeys.splice(closestIdx, 0, activeKey);
+ return newKeys;
+ }
+}
+
+AnExApp.title = 'Animated - Gratuitous App';
+AnExApp.description = 'Bunch of Animations - tap a circle to ' +
+ 'open a view with more animations, or longPress and drag to reorder circles.';
+
+var styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ paddingTop: 64, // push content below nav bar
+ },
+ grid: {
+ flex: 1,
+ justifyContent: 'center',
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ backgroundColor: 'transparent',
+ },
+ circle: {
+ width: CIRCLE_SIZE,
+ height: CIRCLE_SIZE,
+ borderRadius: CIRCLE_SIZE / 2,
+ borderWidth: 1,
+ borderColor: 'black',
+ margin: CIRCLE_MARGIN,
+ overflow: 'hidden',
+ },
+ dragView: {
+ shadowRadius: 10,
+ shadowColor: 'rgba(0,0,0,0.7)',
+ shadowOffset: {height: 8},
+ alignSelf: 'flex-start',
+ backgroundColor: 'transparent',
+ },
+ open: {
+ position: 'absolute',
+ left: 0,
+ top: 0,
+ right: 0,
+ bottom: 0,
+ width: undefined, // unset value from styles.circle
+ height: undefined, // unset value from styles.circle
+ borderRadius: 0, // unset value from styles.circle
+ },
+ darkening: {
+ backgroundColor: 'black',
+ position: 'absolute',
+ left: 0,
+ top: 0,
+ right: 0,
+ bottom: 0,
+ },
+});
+
+module.exports = AnExApp;
diff --git a/Examples/UIExplorer/AnimationExample/AnExBobble.js b/Examples/UIExplorer/AnimationExample/AnExBobble.js
new file mode 100644
index 000000000..bebad819d
--- /dev/null
+++ b/Examples/UIExplorer/AnimationExample/AnExBobble.js
@@ -0,0 +1,162 @@
+/**
+ * The examples provided by Facebook are for non-commercial testing and
+ * evaluation purposes only.
+ *
+ * Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * @providesModule AnExBobble
+ * @flow
+ */
+'use strict';
+
+var React = require('react-native');
+var {
+ Animated,
+ Image,
+ PanResponder,
+ StyleSheet,
+ View,
+} = React;
+
+var NUM_BOBBLES = 5;
+var RAD_EACH = Math.PI / 2 / (NUM_BOBBLES - 2);
+var RADIUS = 160;
+var BOBBLE_SPOTS = [...Array(NUM_BOBBLES)].map((_, i) => { // static positions
+ return i === 0 ? {x: 0, y: 0} : { // first bobble is the selector
+ x: -Math.cos(RAD_EACH * (i - 1)) * RADIUS,
+ y: -Math.sin(RAD_EACH * (i - 1)) * RADIUS,
+ };
+});
+
+class AnExBobble extends React.Component {
+ constructor(props: Object) {
+ super(props);
+ this.state = {};
+ this.state.bobbles = BOBBLE_SPOTS.map((_, i) => {
+ return new Animated.ValueXY();
+ });
+ this.state.selectedBobble = null;
+ var bobblePanListener = (e, gestureState) => { // async events => change selection
+ var newSelected = computeNewSelected(gestureState);
+ if (this.state.selectedBobble !== newSelected) {
+ if (this.state.selectedBobble !== null) {
+ var restSpot = BOBBLE_SPOTS[this.state.selectedBobble];
+ Animated.spring(this.state.bobbles[this.state.selectedBobble], {
+ toValue: restSpot, // return previously selected bobble to rest position
+ }).start();
+ }
+ if (newSelected !== null && newSelected !== 0) {
+ Animated.spring(this.state.bobbles[newSelected], {
+ toValue: this.state.bobbles[0], // newly selected should track the selector
+ }).start();
+ }
+ this.state.selectedBobble = newSelected;
+ }
+ };
+ var releaseBobble = () => {
+ this.state.bobbles.forEach((bobble, i) => {
+ Animated.spring(bobble, {
+ toValue: {x: 0, y: 0} // all bobbles return to zero
+ }).start();
+ });
+ };
+ this.state.bobbleResponder = PanResponder.create({
+ onStartShouldSetPanResponder: () => true,
+ onPanResponderGrant: () => {
+ BOBBLE_SPOTS.forEach((spot, idx) => {
+ Animated.spring(this.state.bobbles[idx], {
+ toValue: spot, // spring each bobble to its spot
+ friction: 3, // less friction => bouncier
+ }).start();
+ });
+ },
+ onPanResponderMove: Animated.event(
+ [ null, {dx: this.state.bobbles[0].x, dy: this.state.bobbles[0].y} ],
+ {listener: bobblePanListener} // async state changes with arbitrary logic
+ ),
+ onPanResponderRelease: releaseBobble,
+ onPanResponderTerminate: releaseBobble,
+ });
+ }
+
+ render(): ReactElement {
+ return (
+
+ {this.state.bobbles.map((_, i) => {
+ var j = this.state.bobbles.length - i - 1; // reverse so lead on top
+ var handlers = j > 0 ? {} : this.state.bobbleResponder.panHandlers;
+ return (
+
+ );
+ })}
+
+ );
+ }
+}
+
+var styles = StyleSheet.create({
+ circle: {
+ position: 'absolute',
+ height: 60,
+ width: 60,
+ borderRadius: 30,
+ borderWidth: 0.5,
+ },
+ bobbleContainer: {
+ top: -68,
+ paddingRight: 66,
+ flexDirection: 'row',
+ flex: 1,
+ justifyContent: 'flex-end',
+ backgroundColor: 'transparent',
+ },
+});
+
+function computeNewSelected(
+ gestureState: Object,
+): ?number {
+ var {dx, dy} = gestureState;
+ var minDist = Infinity;
+ var newSelected = null;
+ var pointRadius = Math.sqrt(dx * dx + dy * dy);
+ if (Math.abs(RADIUS - pointRadius) < 80) {
+ BOBBLE_SPOTS.forEach((spot, idx) => {
+ var delta = {x: spot.x - dx, y: spot.y - dy};
+ var dist = delta.x * delta.x + delta.y * delta.y;
+ if (dist < minDist) {
+ minDist = dist;
+ newSelected = idx;
+ }
+ });
+ }
+ return newSelected;
+}
+
+function randColor(): string {
+ var colors = [0,1,2].map(() => Math.floor(Math.random() * 150 + 100));
+ return 'rgb(' + colors.join(',') + ')';
+}
+
+var BOBBLE_IMGS = [
+ 'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xpf1/t39.1997-6/10173489_272703316237267_1025826781_n.png',
+ 'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xaf1/l/t39.1997-6/p240x240/851578_631487400212668_2087073502_n.png',
+ 'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xaf1/t39.1997-6/p240x240/851583_654446917903722_178118452_n.png',
+ 'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xaf1/t39.1997-6/p240x240/851565_641023175913294_875343096_n.png',
+ 'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xaf1/t39.1997-6/851562_575284782557566_1188781517_n.png',
+];
+
+module.exports = AnExBobble;
diff --git a/Examples/UIExplorer/AnimationExample/AnExChained.js b/Examples/UIExplorer/AnimationExample/AnExChained.js
new file mode 100644
index 000000000..cf4083cff
--- /dev/null
+++ b/Examples/UIExplorer/AnimationExample/AnExChained.js
@@ -0,0 +1,114 @@
+/**
+ * The examples provided by Facebook are for non-commercial testing and
+ * evaluation purposes only.
+ *
+ * Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * @providesModule AnExChained
+ * @flow
+ */
+'use strict';
+
+var React = require('react-native');
+var {
+ Animated,
+ Image,
+ PanResponder,
+ StyleSheet,
+ View,
+} = React;
+
+class AnExChained extends React.Component {
+ constructor(props: Object) {
+ super(props);
+ this.state = {
+ stickers: [new Animated.ValueXY()], // 1 leader
+ };
+ var stickerConfig = {tension: 2, friction: 3}; // soft spring
+ for (var i = 0; i < 4; i++) { // 4 followers
+ var sticker = new Animated.ValueXY();
+ Animated.spring(sticker, {
+ ...stickerConfig,
+ toValue: this.state.stickers[i], // Animated toValue's are tracked
+ }).start();
+ this.state.stickers.push(sticker); // push on the followers
+ }
+ var releaseChain = (e, gestureState) => {
+ this.state.stickers[0].flattenOffset(); // merges offset into value and resets
+ Animated.sequence([ // spring to start after decay finishes
+ Animated.decay(this.state.stickers[0], { // coast to a stop
+ velocity: {x: gestureState.vx, y: gestureState.vy},
+ deceleration: 0.997,
+ }),
+ Animated.spring(this.state.stickers[0], {
+ toValue: {x: 0, y: 0} // return to start
+ }),
+ ]).start();
+ };
+ this.state.chainResponder = PanResponder.create({
+ onStartShouldSetPanResponder: () => true,
+ onPanResponderGrant: () => {
+ this.state.stickers[0].stopAnimation((value) => {
+ this.state.stickers[0].setOffset(value); // start where sticker animated to
+ this.state.stickers[0].setValue({x: 0, y: 0}); // avoid flicker before next event
+ });
+ },
+ onPanResponderMove: Animated.event(
+ [null, {dx: this.state.stickers[0].x, dy: this.state.stickers[0].y}] // map gesture to leader
+ ),
+ onPanResponderRelease: releaseChain,
+ onPanResponderTerminate: releaseChain,
+ });
+ }
+
+ render() {
+ return (
+
+ {this.state.stickers.map((_, i) => {
+ var j = this.state.stickers.length - i - 1; // reverse so leader is on top
+ var handlers = (j === 0) ? this.state.chainResponder.panHandlers : {};
+ return (
+
+ );
+ })}
+
+ );
+ }
+}
+
+var styles = StyleSheet.create({
+ chained: {
+ alignSelf: 'flex-end',
+ top: -160,
+ right: 126
+ },
+ sticker: {
+ position: 'absolute',
+ height: 120,
+ width: 120,
+ backgroundColor: 'transparent',
+ },
+});
+
+var CHAIN_IMGS = [
+ 'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xpf1/t39.1997-6/p160x160/10574705_1529175770666007_724328156_n.png',
+ 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-xfa1/t39.1997-6/p160x160/851575_392309884199657_1917957497_n.png',
+ 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-xfa1/t39.1997-6/p160x160/851567_555288911225630_1628791128_n.png',
+ 'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xfa1/t39.1997-6/p160x160/851583_531111513625557_903469595_n.png',
+ 'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xpa1/t39.1997-6/p160x160/851577_510515972354399_2147096990_n.png',
+];
+
+module.exports = AnExChained;
diff --git a/Examples/UIExplorer/AnimationExample/AnExScroll.js b/Examples/UIExplorer/AnimationExample/AnExScroll.js
new file mode 100644
index 000000000..f96acb380
--- /dev/null
+++ b/Examples/UIExplorer/AnimationExample/AnExScroll.js
@@ -0,0 +1,115 @@
+/**
+ * The examples provided by Facebook are for non-commercial testing and
+ * evaluation purposes only.
+ *
+ * Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * @providesModule AnExScroll
+ */
+'use strict';
+
+var React = require('react-native');
+var {
+ Animated,
+ Image,
+ ScrollView,
+ StyleSheet,
+ Text,
+ View,
+} = React;
+
+class AnExScroll extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ scrollX: new Animated.Value(0),
+ };
+ }
+
+ render() {
+ var width = this.props.panelWidth;
+ return (
+
+
+
+
+
+ {'I\'ll find something to put here.'}
+
+
+
+ {'And here.'}
+
+
+ {'But not here.'}
+
+
+
+
+ );
+ }
+}
+
+var styles = StyleSheet.create({
+ container: {
+ backgroundColor: 'transparent',
+ flex: 1,
+ },
+ text: {
+ padding: 4,
+ paddingBottom: 10,
+ fontWeight: 'bold',
+ fontSize: 18,
+ backgroundColor: 'transparent',
+ },
+ bunny: {
+ backgroundColor: 'transparent',
+ position: 'absolute',
+ height: 160,
+ width: 160,
+ },
+ page: {
+ alignItems: 'center',
+ justifyContent: 'flex-end',
+ },
+});
+
+var HAWK_PIC = {uri: 'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xfa1/t39.1997-6/10734304_1562225620659674_837511701_n.png'};
+var BUNNY_PIC = {uri: 'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xaf1/t39.1997-6/851564_531111380292237_1898871086_n.png'};
+
+module.exports = AnExScroll;
diff --git a/Examples/UIExplorer/AnimationExample/AnExSet.js b/Examples/UIExplorer/AnimationExample/AnExSet.js
new file mode 100644
index 000000000..f25301f77
--- /dev/null
+++ b/Examples/UIExplorer/AnimationExample/AnExSet.js
@@ -0,0 +1,150 @@
+/**
+ * The examples provided by Facebook are for non-commercial testing and
+ * evaluation purposes only.
+ *
+ * Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * @providesModule AnExSet
+ * @flow
+ */
+'use strict';
+
+var React = require('react-native');
+var {
+ Animated,
+ PanResponder,
+ StyleSheet,
+ Text,
+ View,
+} = React;
+
+var AnExBobble = require('./AnExBobble');
+var AnExChained = require('./AnExChained');
+var AnExScroll = require('./AnExScroll');
+var AnExTilt = require('./AnExTilt');
+
+class AnExSet extends React.Component {
+ constructor(props: Object) {
+ super(props);
+ function randColor() {
+ var colors = [0,1,2].map(() => Math.floor(Math.random() * 150 + 100));
+ return 'rgb(' + colors.join(',') + ')';
+ }
+ this.state = {
+ closeColor: randColor(),
+ openColor: randColor(),
+ };
+ }
+ render(): ReactElement {
+ var backgroundColor = this.props.openVal ?
+ this.props.openVal.interpolate({
+ inputRange: [0, 1],
+ outputRange: [
+ this.state.closeColor, // interpolates color strings
+ this.state.openColor
+ ],
+ }) :
+ this.state.closeColor;
+ var panelWidth = this.props.containerLayout && this.props.containerLayout.width || 320;
+ return (
+
+
+
+ {this.props.id}
+
+
+ {this.props.isActive &&
+
+
+
+ July 2nd
+
+
+
+
+
+
+
+ }
+
+ );
+ }
+
+ componentWillMount() {
+ this.state.dismissY = new Animated.Value(0);
+ this.state.dismissResponder = PanResponder.create({
+ onStartShouldSetPanResponder: () => this.props.isActive,
+ onPanResponderGrant: () => {
+ Animated.spring(this.props.openVal, { // Animated value passed in.
+ toValue: this.state.dismissY.interpolate({ // Track dismiss gesture
+ inputRange: [0, 300], // and interpolate pixel distance
+ outputRange: [1, 0], // to a fraction.
+ })
+ }).start();
+ },
+ onPanResponderMove: Animated.event(
+ [null, {dy: this.state.dismissY}] // track pan gesture
+ ),
+ onPanResponderRelease: (e, gestureState) => {
+ if (gestureState.dy > 100) {
+ this.props.onDismiss(gestureState.vy); // delegates dismiss action to parent
+ } else {
+ Animated.spring(this.props.openVal, {
+ toValue: 1, // animate back open if released early
+ }).start();
+ }
+ },
+ });
+ }
+}
+
+var styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ header: {
+ alignItems: 'center',
+ paddingTop: 18,
+ height: 90,
+ },
+ stream: {
+ flex: 1,
+ backgroundColor: 'rgb(230, 230, 230)',
+ },
+ card: {
+ margin: 8,
+ padding: 8,
+ borderRadius: 6,
+ backgroundColor: 'white',
+ shadowRadius: 2,
+ shadowColor: 'black',
+ shadowOpacity: 0.2,
+ shadowOffset: {height: 0.5},
+ },
+ text: {
+ padding: 4,
+ paddingBottom: 10,
+ fontWeight: 'bold',
+ fontSize: 18,
+ backgroundColor: 'transparent',
+ },
+ headerText: {
+ fontSize: 25,
+ color: 'white',
+ shadowRadius: 3,
+ shadowColor: 'black',
+ shadowOpacity: 1,
+ shadowOffset: {height: 1},
+ },
+});
+
+module.exports = AnExSet;
diff --git a/Examples/UIExplorer/AnimationExample/AnExSlides.md b/Examples/UIExplorer/AnimationExample/AnExSlides.md
new file mode 100644
index 000000000..bdc0bf1e0
--- /dev/null
+++ b/Examples/UIExplorer/AnimationExample/AnExSlides.md
@@ -0,0 +1,107 @@
+
+# React Native: Animated
+
+ReactEurope 2015, Paris - Spencer Ahrens - Facebook
+
+
+
+## Fluid Interactions
+
+- People expect smooth, delightful experiences
+- Complex interactions are hard
+- Common patterns can be optimized
+
+
+
+
+## Declarative Interactions
+
+- Wire up inputs (events) to outputs (props) + transforms (springs, easing, etc.)
+- Arbitrary code can define/update this config
+- Config can be serialized -> native/main thread
+- No refs or lifecycle to worry about
+
+
+
+
+## var { Animated } = require('react-native');
+
+- New library soon to be released for React Native
+- 100% JS implementation -> X-Platform
+- Per-platform native optimizations planned
+- This talk -> usage examples, not implementation
+
+
+
+
+## Gratuitous Animation Demo App
+
+- Layout uses `flexWrap: 'wrap'`
+- longPress -> drag to reorder
+- Tap to open example sets
+
+
+
+## Gratuitous Animation Codez
+
+- Step 1: 2D tracking pan gesture
+- Step 2: Simple pop-out spring on select
+- Step 3: Animate grid reordering with `LayoutAnimation`
+- Step 4: Opening animation
+
+
+
+## Animation Example Set
+
+- `Animated.Value` `this.props.open` passed in from parent
+- `interpolate` works with string "shapes," e.g. `'rgb(0, 0, 255)'`, `'45deg'`
+- Examples easily composed as separate components
+- Dismissing tracks interpolated gesture
+- Custom release logic
+
+
+
+
+## Tilting Photo
+
+- Pan -> translateX * 2, rotate, opacity (via tracking)
+- Gesture release triggers separate animations
+- `addListener` for async, arbitrary logic on animation progress
+- `interpolate` easily creates parallax and other effects
+
+
+
+## Bobbles
+
+- Static positions defined
+- Listens to events to maybe change selection
+ - Springs previous selection back
+ - New selection tracks selector
+- `getTranslateTransform` adds convenience
+
+
+
+## Chained
+
+- Classic "Chat Heads" animation
+- Each sticker tracks the one before it with a soft spring
+- `decay` maintains gesture velocity, followed by `spring` to home
+- `stopAnimation` provides the last value for `setOffset`
+
+
+
+## Scrolling
+
+- `Animated.event` can track all sorts of stuff
+- Multi-part ranges and extrapolation options
+- Transforms decompose into ordered components
+
+
+
+# React Native: Animated
+
+- Landing soon in master (days)
+- GitHub: @vjeux, @sahrens
+- Questions?
+
+
diff --git a/Examples/UIExplorer/AnimationExample/AnExTilt.js b/Examples/UIExplorer/AnimationExample/AnExTilt.js
new file mode 100644
index 000000000..3cea77917
--- /dev/null
+++ b/Examples/UIExplorer/AnimationExample/AnExTilt.js
@@ -0,0 +1,139 @@
+/**
+ * The examples provided by Facebook are for non-commercial testing and
+ * evaluation purposes only.
+ *
+ * Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * @providesModule AnExTilt
+ * @flow
+ */
+'use strict';
+
+var React = require('react-native');
+var {
+ Animated,
+ Image,
+ PanResponder,
+ StyleSheet,
+ View,
+} = React;
+
+class AnExTilt extends React.Component {
+ constructor(props: Object) {
+ super(props);
+ this.state = {
+ panX: new Animated.Value(0),
+ opacity: new Animated.Value(1),
+ burns: new Animated.Value(1.15),
+ };
+ this.state.tiltPanResponder = PanResponder.create({
+ onStartShouldSetPanResponder: () => true,
+ onPanResponderGrant: () => {
+ Animated.timing(this.state.opacity, {
+ toValue: this.state.panX.interpolate({
+ inputRange: [-300, 0, 300], // pan is in pixels
+ outputRange: [0, 1, 0], // goes to zero at both edges
+ }),
+ duration: 0, // direct tracking
+ }).start();
+ },
+ onPanResponderMove: Animated.event(
+ [null, {dx: this.state.panX}] // panX is linked to the gesture
+ ),
+ onPanResponderRelease: (e, gestureState) => {
+ var toValue = 0;
+ if (gestureState.dx > 100) {
+ toValue = 500;
+ } else if (gestureState.dx < -100) {
+ toValue = -500;
+ }
+ Animated.spring(this.state.panX, {
+ toValue, // animate back to center or off screen
+ velocity: gestureState.vx, // maintain gesture velocity
+ tension: 10,
+ friction: 3,
+ }).start();
+ this.state.panX.removeAllListeners();
+ var id = this.state.panX.addListener(({value}) => { // listen until offscreen
+ if (Math.abs(value) > 400) {
+ this.state.panX.removeListener(id); // offscreen, so stop listening
+ Animated.timing(this.state.opacity, {
+ toValue: 1, // Fade back in. This unlinks it from tracking this.state.panX
+ }).start();
+ this.state.panX.setValue(0); // Note: stops the spring animation
+ toValue !== 0 && this._startBurnsZoom();
+ }
+ });
+ },
+ });
+ }
+
+ _startBurnsZoom() {
+ this.state.burns.setValue(1); // reset to beginning
+ Animated.decay(this.state.burns, {
+ velocity: 1, // sublte zoom
+ deceleration: 0.9999, // slow decay
+ }).start();
+ }
+
+ componentWillMount() {
+ this._startBurnsZoom();
+ }
+
+ render(): ReactElement {
+ return (
+
+
+
+ );
+ }
+}
+
+var styles = StyleSheet.create({
+ tilt: {
+ overflow: 'hidden',
+ height: 200,
+ marginBottom: 4,
+ backgroundColor: 'rgb(130, 130, 255)',
+ borderColor: 'rgba(0, 0, 0, 0.2)',
+ borderWidth: 1,
+ borderRadius: 20,
+ },
+});
+
+var NATURE_IMAGE = {uri: 'http://www.deshow.net/d/file/travel/2009-04/scenic-beauty-of-nature-photography-2-504-4.jpg'};
+
+module.exports = AnExTilt;
diff --git a/Examples/UIExplorer/UIExplorerList.js b/Examples/UIExplorer/UIExplorerList.js
index 8a7782484..47707f4e4 100644
--- a/Examples/UIExplorer/UIExplorerList.js
+++ b/Examples/UIExplorer/UIExplorerList.js
@@ -50,6 +50,7 @@ var COMMON_COMPONENTS = [
];
var COMMON_APIS = [
+ require('./AnimationExample/AnExApp'),
require('./GeolocationExample'),
require('./LayoutExample'),
require('./PanResponderExample'),
diff --git a/Libraries/Animation/Animated/Animated.js b/Libraries/Animation/Animated/Animated.js
new file mode 100644
index 000000000..fd0809825
--- /dev/null
+++ b/Libraries/Animation/Animated/Animated.js
@@ -0,0 +1,1295 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule Animated
+ * @flow
+ */
+'use strict';
+
+var Easing = require('Easing');
+var Image = require('Image');
+var InteractionManager = require('InteractionManager');
+var Interpolation = require('Interpolation');
+var React = require('React');
+var Set = require('Set');
+var Text = require('Text');
+var View = require('View');
+var invariant = require('invariant');
+
+var flattenStyle = require('flattenStyle');
+var rebound = require('rebound');
+
+import type InterpolationConfigType from 'Interpolation';
+
+type EndResult = {finished: bool};
+type EndCallback = (result: EndResult) => void;
+
+// Note(vjeux): this would be better as an interface but flow doesn't
+// support them yet
+class Animated {
+ attach(): void {}
+ detach(): void {}
+ __getValue(): any {}
+ getAnimatedValue(): any { return this.__getValue(); }
+ addChild(child: Animated) {}
+ removeChild(child: Animated) {}
+ getChildren(): Array { return []; }
+}
+
+// Important note: start() and stop() will only be called at most once.
+// Once an animation has been stopped or finished its course, it will
+// not be reused.
+class Animation {
+ __active: bool;
+ __onEnd: ?EndCallback;
+ start(
+ fromValue: number,
+ onUpdate: (value: number) => void,
+ onEnd: ?EndCallback,
+ previousAnimation: ?Animation,
+ ): void {}
+ stop(): void {}
+ // Helper function for subclasses to make sure onEnd is only called once.
+ __debouncedOnEnd(result: EndResult) {
+ var onEnd = this.__onEnd;
+ this.__onEnd = null;
+ onEnd && onEnd(result);
+ }
+}
+
+class AnimatedWithChildren extends Animated {
+ _children: Array;
+
+ constructor() {
+ super();
+ this._children = [];
+ }
+
+ addChild(child: Animated): void {
+ if (this._children.length === 0) {
+ this.attach();
+ }
+ this._children.push(child);
+ }
+
+ removeChild(child: Animated): void {
+ var index = this._children.indexOf(child);
+ if (index === -1) {
+ console.warn('Trying to remove a child that doesn\'t exist');
+ return;
+ }
+ this._children.splice(index, 1);
+ if (this._children.length === 0) {
+ this.detach();
+ }
+ }
+
+ getChildren(): Array {
+ return this._children;
+ }
+}
+
+/**
+ * Animated works by building a directed acyclic graph of dependencies
+ * transparently when you render your Animated components.
+ *
+ * new Animated.Value(0)
+ * .interpolate() .interpolate() new Animated.Value(1)
+ * opacity translateY scale
+ * style transform
+ * View#234 style
+ * View#123
+ *
+ * A) Top Down phase
+ * When an Animated.Value is updated, we recursively go down through this
+ * graph in order to find leaf nodes: the views that we flag as needing
+ * an update.
+ *
+ * B) Bottom Up phase
+ * When a view is flagged as needing an update, we recursively go back up
+ * in order to build the new value that it needs. The reason why we need
+ * this two-phases process is to deal with composite props such as
+ * transform which can receive values from multiple parents.
+ */
+function _flush(rootNode: AnimatedValue): void {
+ var animatedStyles = new Set();
+ function findAnimatedStyles(node) {
+ if (typeof node.update === 'function') {
+ animatedStyles.add(node);
+ } else {
+ node.getChildren().forEach(findAnimatedStyles);
+ }
+ }
+ findAnimatedStyles(rootNode);
+ animatedStyles.forEach(animatedStyle => animatedStyle.update());
+}
+
+type TimingAnimationConfig = {
+ toValue: number;
+ easing?: (value: number) => number;
+ duration?: number;
+ delay?: number;
+};
+
+var easeInOut = Easing.inOut(Easing.ease);
+
+class TimingAnimation extends Animation {
+ _startTime: number;
+ _fromValue: number;
+ _toValue: number;
+ _duration: number;
+ _delay: number;
+ _easing: (value: number) => number;
+ _onUpdate: (value: number) => void;
+ _animationFrame: any;
+ _timeout: any;
+
+ constructor(
+ config: TimingAnimationConfig,
+ ) {
+ super();
+ this._toValue = config.toValue;
+ this._easing = config.easing || easeInOut;
+ this._duration = config.duration !== undefined ? config.duration : 500;
+ this._delay = config.delay || 0;
+ }
+
+ start(
+ fromValue: number,
+ onUpdate: (value: number) => void,
+ onEnd: ?EndCallback,
+ ): void {
+ this.__active = true;
+ this._fromValue = fromValue;
+ this._onUpdate = onUpdate;
+ this.__onEnd = onEnd;
+
+ var start = () => {
+ if (this._duration === 0) {
+ this._onUpdate(this._toValue);
+ } else {
+ this._startTime = Date.now();
+ this._animationFrame = window.requestAnimationFrame(this.onUpdate.bind(this));
+ }
+ };
+ if (this._delay) {
+ this._timeout = setTimeout(start, this._delay);
+ } else {
+ start();
+ }
+ }
+
+ onUpdate(): void {
+ var now = Date.now();
+ if (now >= this._startTime + this._duration) {
+ if (this._duration === 0) {
+ this._onUpdate(this._toValue);
+ } else {
+ this._onUpdate(
+ this._fromValue + this._easing(1) * (this._toValue - this._fromValue)
+ );
+ }
+ this.__debouncedOnEnd({finished: true});
+ return;
+ }
+
+ this._onUpdate(
+ this._fromValue +
+ this._easing((now - this._startTime) / this._duration) *
+ (this._toValue - this._fromValue)
+ );
+ if (this.__active) {
+ this._animationFrame = window.requestAnimationFrame(this.onUpdate.bind(this));
+ }
+ }
+
+ stop(): void {
+ this.__active = false;
+ clearTimeout(this._timeout);
+ window.cancelAnimationFrame(this._animationFrame);
+ this.__debouncedOnEnd({finished: false});
+ }
+}
+
+type DecayAnimationConfig = {
+ velocity: number | {x: number, y: number};
+ deceleration?: number;
+};
+
+type DecayAnimationConfigSingle = {
+ velocity: number;
+ deceleration?: number;
+};
+
+class DecayAnimation extends Animation {
+ _startTime: number;
+ _lastValue: number;
+ _fromValue: number;
+ _deceleration: number;
+ _velocity: number;
+ _onUpdate: (value: number) => void;
+ _animationFrame: any;
+
+ constructor(
+ config: DecayAnimationConfigSingle,
+ ) {
+ super();
+ this._deceleration = config.deceleration || 0.998;
+ this._velocity = config.velocity;
+ }
+
+ start(
+ fromValue: number,
+ onUpdate: (value: number) => void,
+ onEnd: ?EndCallback,
+ ): void {
+ this.__active = true;
+ this._lastValue = fromValue;
+ this._fromValue = fromValue;
+ this._onUpdate = onUpdate;
+ this.__onEnd = onEnd;
+ this._startTime = Date.now();
+ this._animationFrame = window.requestAnimationFrame(this.onUpdate.bind(this));
+ }
+
+ onUpdate(): void {
+ var now = Date.now();
+
+ var value = this._fromValue +
+ (this._velocity / (1 - this._deceleration)) *
+ (1 - Math.exp(-(1 - this._deceleration) * (now - this._startTime)));
+
+ this._onUpdate(value);
+
+ if (Math.abs(this._lastValue - value) < 0.1) {
+ this.__debouncedOnEnd({finished: true});
+ return;
+ }
+
+ this._lastValue = value;
+ if (this.__active) {
+ this._animationFrame = window.requestAnimationFrame(this.onUpdate.bind(this));
+ }
+ }
+
+ stop(): void {
+ this.__active = false;
+ window.cancelAnimationFrame(this._animationFrame);
+ this.__debouncedOnEnd({finished: false});
+ }
+}
+
+type SpringAnimationConfig = {
+ toValue: number | AnimatedValue | {x: number, y: number} | AnimatedValueXY;
+ overshootClamping?: bool;
+ restDisplacementThreshold?: number;
+ restSpeedThreshold?: number;
+ velocity?: number | {x: number, y: number};
+ bounciness?: number;
+ speed?: number;
+ tension?: number;
+ friction?: number;
+};
+
+type SpringAnimationConfigSingle = {
+ toValue: number | AnimatedValue;
+ overshootClamping?: bool;
+ restDisplacementThreshold?: number;
+ restSpeedThreshold?: number;
+ velocity?: number;
+ bounciness?: number;
+ speed?: number;
+ tension?: number;
+ friction?: number;
+};
+
+function withDefault(value: ?T, defaultValue: T): T {
+ if (value === undefined || value === null) {
+ return defaultValue;
+ }
+ return value;
+}
+
+class SpringAnimation extends Animation {
+ _overshootClamping: bool;
+ _restDisplacementThreshold: number;
+ _restSpeedThreshold: number;
+ _initialVelocity: ?number;
+ _lastVelocity: number;
+ _startPosition: number;
+ _lastPosition: number;
+ _fromValue: number;
+ _toValue: any;
+ _tension: number;
+ _friction: number;
+ _lastTime: number;
+ _onUpdate: (value: number) => void;
+ _animationFrame: any;
+
+ constructor(
+ config: SpringAnimationConfigSingle,
+ ) {
+ super();
+
+ this._overshootClamping = withDefault(config.overshootClamping, false);
+ this._restDisplacementThreshold = withDefault(config.restDisplacementThreshold, 0.001);
+ this._restSpeedThreshold = withDefault(config.restSpeedThreshold, 0.001);
+ this._initialVelocity = config.velocity;
+ this._lastVelocity = withDefault(config.velocity, 0);
+ this._toValue = config.toValue;
+
+ var springConfig;
+ if (config.bounciness !== undefined || config.speed !== undefined) {
+ invariant(
+ config.tension === undefined && config.friction === undefined,
+ 'You can only define bounciness/speed or tension/friction but not both',
+ );
+ springConfig = rebound.SpringConfig.fromBouncinessAndSpeed(
+ withDefault(config.bounciness, 8),
+ withDefault(config.speed, 12),
+ );
+ } else {
+ springConfig = rebound.SpringConfig.fromOrigamiTensionAndFriction(
+ withDefault(config.tension, 40),
+ withDefault(config.friction, 7),
+ );
+ }
+ this._tension = springConfig.tension;
+ this._friction = springConfig.friction;
+ }
+
+ start(
+ fromValue: number,
+ onUpdate: (value: number) => void,
+ onEnd: ?EndCallback,
+ previousAnimation: ?Animation,
+ ): void {
+ this.__active = true;
+ this._startPosition = fromValue;
+ this._lastPosition = this._startPosition;
+
+ this._onUpdate = onUpdate;
+ this.__onEnd = onEnd;
+ this._lastTime = Date.now();
+
+ if (previousAnimation instanceof SpringAnimation) {
+ var internalState = previousAnimation.getInternalState();
+ this._lastPosition = internalState.lastPosition;
+ this._lastVelocity = internalState.lastVelocity;
+ this._lastTime = internalState.lastTime;
+ }
+ if (this._initialVelocity !== undefined &&
+ this._initialVelocity !== null) {
+ this._lastVelocity = this._initialVelocity;
+ }
+ this.onUpdate();
+ }
+
+ getInternalState(): Object {
+ return {
+ lastPosition: this._lastPosition,
+ lastVelocity: this._lastVelocity,
+ lastTime: this._lastTime,
+ };
+ }
+
+ onUpdate(): void {
+ var position = this._lastPosition;
+ var velocity = this._lastVelocity;
+
+ var tempPosition = this._lastPosition;
+ var tempVelocity = this._lastVelocity;
+
+ // If for some reason we lost a lot of frames (e.g. process large payload or
+ // stopped in the debugger), we only advance by 4 frames worth of
+ // computation and will continue on the next frame. It's better to have it
+ // running at faster speed than jumping to the end.
+ var MAX_STEPS = 64;
+ var now = Date.now();
+ if (now > this._lastTime + MAX_STEPS) {
+ now = this._lastTime + MAX_STEPS;
+ }
+
+ // We are using a fixed time step and a maximum number of iterations.
+ // The following post provides a lot of thoughts into how to build this
+ // loop: http://gafferongames.com/game-physics/fix-your-timestep/
+ var TIMESTEP_MSEC = 1;
+ var numSteps = Math.floor((now - this._lastTime) / TIMESTEP_MSEC);
+
+ for (var i = 0; i < numSteps; ++i) {
+ // Velocity is based on seconds instead of milliseconds
+ var step = TIMESTEP_MSEC / 1000;
+
+ // This is using RK4. A good blog post to understand how it works:
+ // http://gafferongames.com/game-physics/integration-basics/
+ var aVelocity = velocity;
+ var aAcceleration = this._tension * (this._toValue - tempPosition) - this._friction * tempVelocity;
+ var tempPosition = position + aVelocity * step / 2;
+ var tempVelocity = velocity + aAcceleration * step / 2;
+
+ var bVelocity = tempVelocity;
+ var bAcceleration = this._tension * (this._toValue - tempPosition) - this._friction * tempVelocity;
+ tempPosition = position + bVelocity * step / 2;
+ tempVelocity = velocity + bAcceleration * step / 2;
+
+ var cVelocity = tempVelocity;
+ var cAcceleration = this._tension * (this._toValue - tempPosition) - this._friction * tempVelocity;
+ tempPosition = position + cVelocity * step / 2;
+ tempVelocity = velocity + cAcceleration * step / 2;
+
+ var dVelocity = tempVelocity;
+ var dAcceleration = this._tension * (this._toValue - tempPosition) - this._friction * tempVelocity;
+ tempPosition = position + cVelocity * step / 2;
+ tempVelocity = velocity + cAcceleration * step / 2;
+
+ var dxdt = (aVelocity + 2 * (bVelocity + cVelocity) + dVelocity) / 6;
+ var dvdt = (aAcceleration + 2 * (bAcceleration + cAcceleration) + dAcceleration) / 6;
+
+ position += dxdt * step;
+ velocity += dvdt * step;
+ }
+
+ this._lastTime = now;
+ this._lastPosition = position;
+ this._lastVelocity = velocity;
+
+ this._onUpdate(position);
+ if (!this.__active) { // a listener might have stopped us in _onUpdate
+ return;
+ }
+
+ // Conditions for stopping the spring animation
+ var isOvershooting = false;
+ if (this._overshootClamping && this._tension !== 0) {
+ if (this._startPosition < this._toValue) {
+ isOvershooting = position > this._toValue;
+ } else {
+ isOvershooting = position < this._toValue;
+ }
+ }
+ var isVelocity = Math.abs(velocity) <= this._restSpeedThreshold;
+ var isDisplacement = true;
+ if (this._tension !== 0) {
+ isDisplacement = Math.abs(this._toValue - position) <= this._restDisplacementThreshold;
+ }
+ if (isOvershooting || (isVelocity && isDisplacement)) {
+ this.__debouncedOnEnd({finished: true});
+ return;
+ }
+ this._animationFrame = window.requestAnimationFrame(this.onUpdate.bind(this));
+ }
+
+ stop(): void {
+ this.__active = false;
+ window.cancelAnimationFrame(this._animationFrame);
+ this.__debouncedOnEnd({finished: false});
+ }
+}
+
+type ValueListenerCallback = (state: {value: number}) => void;
+
+var _uniqueId = 1;
+
+class AnimatedValue extends AnimatedWithChildren {
+ _value: number;
+ _offset: number;
+ _animation: ?Animation;
+ _tracking: ?Animated;
+ _listeners: {[key: string]: ValueListenerCallback};
+
+ constructor(value: number) {
+ super();
+ this._value = value;
+ this._offset = 0;
+ this._animation = null;
+ this._listeners = {};
+ }
+
+ detach() {
+ this.stopAnimation();
+ }
+
+ __getValue(): number {
+ return this._value + this._offset;
+ }
+
+ setValue(value: number): void {
+ if (this._animation) {
+ this._animation.stop();
+ this._animation = null;
+ }
+ this._updateValue(value);
+ }
+
+ setOffset(offset: number): void {
+ this._offset = offset;
+ }
+
+ flattenOffset(): void {
+ this._value += this._offset;
+ this._offset = 0;
+ }
+
+ addListener(callback: ValueListenerCallback): string {
+ var id = String(_uniqueId++);
+ this._listeners[id] = callback;
+ return id;
+ }
+
+ removeListener(id: string): void {
+ delete this._listeners[id];
+ }
+
+ removeAllListeners(): void {
+ this._listeners = {};
+ }
+
+ animate(animation: Animation, callback: ?EndCallback): void {
+ var handle = InteractionManager.createInteractionHandle();
+ var previousAnimation = this._animation;
+ this._animation && this._animation.stop();
+ this._animation = animation;
+ animation.start(
+ this._value,
+ (value) => {
+ this._updateValue(value);
+ },
+ (result) => {
+ this._animation = null;
+ InteractionManager.clearInteractionHandle(handle);
+ callback && callback(result);
+ },
+ previousAnimation,
+ );
+ }
+
+ stopAnimation(callback?: ?(value: number) => void): void {
+ this.stopTracking();
+ this._animation && this._animation.stop();
+ this._animation = null;
+ callback && callback(this.__getValue());
+ }
+
+ stopTracking(): void {
+ this._tracking && this._tracking.detach();
+ this._tracking = null;
+ }
+
+ track(tracking: Animated): void {
+ this.stopTracking();
+ this._tracking = tracking;
+ }
+
+ interpolate(config: InterpolationConfigType): AnimatedInterpolation {
+ return new AnimatedInterpolation(this, Interpolation.create(config));
+ }
+
+ _updateValue(value: number): void {
+ this._value = value;
+ _flush(this);
+ for (var key in this._listeners) {
+ this._listeners[key]({value: this.__getValue()});
+ }
+ }
+}
+
+type ValueXYListenerCallback = (value: {x: number; y: number}) => void;
+class AnimatedValueXY extends AnimatedWithChildren {
+ x: AnimatedValue;
+ y: AnimatedValue;
+ _listeners: {[key: string]: {x: string; y: string}};
+
+ constructor(valueIn?: ?{x: number | AnimatedValue; y: number | AnimatedValue}) {
+ super();
+ var value: any = valueIn || {x: 0, y: 0}; // @flowfixme: shouldn't need `: any`
+ if (typeof value.x === 'number' && typeof value.y === 'number') {
+ this.x = new AnimatedValue(value.x);
+ this.y = new AnimatedValue(value.y);
+ } else {
+ invariant(
+ value.x instanceof AnimatedValue &&
+ value.y instanceof AnimatedValue,
+ 'AnimatedValueXY must be initalized with an object of numbers or ' +
+ 'AnimatedValues.'
+ );
+ this.x = value.x;
+ this.y = value.y;
+ }
+ this._listeners = {};
+ }
+
+ setValue(value: {x: number; y: number}) {
+ this.x.setValue(value.x);
+ this.y.setValue(value.y);
+ }
+
+ setOffset(offset: {x: number; y: number}) {
+ this.x.setOffset(offset.x);
+ this.y.setOffset(offset.y);
+ }
+
+ flattenOffset(): void {
+ this.x.flattenOffset();
+ this.y.flattenOffset();
+ }
+
+ __getValue(): {x: number; y: number} {
+ return {
+ x: this.x.__getValue(),
+ y: this.y.__getValue(),
+ };
+ }
+
+ stopAnimation(callback?: ?() => number): void {
+ this.x.stopAnimation();
+ this.y.stopAnimation();
+ callback && callback(this.__getValue());
+ }
+
+ addListener(callback: ValueXYListenerCallback): string {
+ var id = String(_uniqueId++);
+ var jointCallback = ({value: number}) => {
+ callback(this.__getValue());
+ };
+ this._listeners[id] = {
+ x: this.x.addListener(jointCallback),
+ y: this.y.addListener(jointCallback),
+ };
+ return id;
+ }
+
+ removeListener(id: string): void {
+ this.x.removeListener(this._listeners[id].x);
+ this.y.removeListener(this._listeners[id].y);
+ delete this._listeners[id];
+ }
+
+ getLayout(): {[key: string]: AnimatedValue} {
+ return {
+ left: this.x,
+ top: this.y,
+ };
+ }
+
+ getTranslateTransform(): Array<{[key: string]: AnimatedValue}> {
+ return [
+ {translateX: this.x},
+ {translateY: this.y}
+ ];
+ }
+}
+
+class AnimatedInterpolation extends AnimatedWithChildren {
+ _parent: Animated;
+ _interpolation: (input: number) => number | string;
+
+ constructor(parent: Animated, interpolation: (input: number) => number | string) {
+ super();
+ this._parent = parent;
+ this._interpolation = interpolation;
+ }
+
+ __getValue(): number | string {
+ var parentValue: number = this._parent.__getValue();
+ invariant(
+ typeof parentValue === 'number',
+ 'Cannot interpolate an input which is not a number.'
+ );
+ return this._interpolation(parentValue);
+ }
+
+ interpolate(config: InterpolationConfigType): AnimatedInterpolation {
+ return new AnimatedInterpolation(this, Interpolation.create(config));
+ }
+
+ attach(): void {
+ this._parent.addChild(this);
+ }
+
+ detach(): void {
+ this._parent.removeChild(this);
+ }
+}
+
+class AnimatedTransform extends AnimatedWithChildren {
+ _transforms: Array;
+
+ constructor(transforms: Array) {
+ super();
+ this._transforms = transforms;
+ }
+
+ __getValue(): Array {
+ return this._transforms.map(transform => {
+ var result = {};
+ for (var key in transform) {
+ var value = transform[key];
+ if (value instanceof Animated) {
+ result[key] = value.__getValue();
+ } else {
+ result[key] = value;
+ }
+ }
+ return result;
+ });
+ }
+
+ getAnimatedValue(): Array {
+ return this._transforms.map(transform => {
+ var result = {};
+ for (var key in transform) {
+ var value = transform[key];
+ if (value instanceof Animated) {
+ result[key] = value.getAnimatedValue();
+ } else {
+ // All transform components needed to recompose matrix
+ result[key] = value;
+ }
+ }
+ return result;
+ });
+ }
+
+ attach(): void {
+ this._transforms.forEach(transform => {
+ for (var key in transform) {
+ var value = transform[key];
+ if (value instanceof Animated) {
+ value.addChild(this);
+ }
+ }
+ });
+ }
+
+ detach(): void {
+ this._transforms.forEach(transform => {
+ for (var key in transform) {
+ var value = transform[key];
+ if (value instanceof Animated) {
+ value.removeChild(this);
+ }
+ }
+ });
+ }
+}
+
+class AnimatedStyle extends AnimatedWithChildren {
+ _style: Object;
+
+ constructor(style: any) {
+ super();
+ style = flattenStyle(style) || {};
+ if (style.transform) {
+ style = {
+ ...style,
+ transform: new AnimatedTransform(style.transform),
+ };
+ }
+ this._style = style;
+ }
+
+ __getValue(): Object {
+ var style = {};
+ for (var key in this._style) {
+ var value = this._style[key];
+ if (value instanceof Animated) {
+ style[key] = value.__getValue();
+ } else {
+ style[key] = value;
+ }
+ }
+ return style;
+ }
+
+ getAnimatedValue(): Object {
+ var style = {};
+ for (var key in this._style) {
+ var value = this._style[key];
+ if (value instanceof Animated) {
+ style[key] = value.getAnimatedValue();
+ }
+ }
+ return style;
+ }
+
+ attach(): void {
+ for (var key in this._style) {
+ var value = this._style[key];
+ if (value instanceof Animated) {
+ value.addChild(this);
+ }
+ }
+ }
+
+ detach(): void {
+ for (var key in this._style) {
+ var value = this._style[key];
+ if (value instanceof Animated) {
+ value.removeChild(this);
+ }
+ }
+ }
+}
+
+class AnimatedProps extends Animated {
+ _props: Object;
+ _callback: () => void;
+
+ constructor(
+ props: Object,
+ callback: () => void,
+ ) {
+ super();
+ if (props.style) {
+ props = {
+ ...props,
+ style: new AnimatedStyle(props.style),
+ };
+ }
+ this._props = props;
+ this._callback = callback;
+ this.attach();
+ }
+
+ __getValue(): Object {
+ var props = {};
+ for (var key in this._props) {
+ var value = this._props[key];
+ if (value instanceof Animated) {
+ props[key] = value.__getValue();
+ } else {
+ props[key] = value;
+ }
+ }
+ return props;
+ }
+
+ getAnimatedValue(): Object {
+ var props = {};
+ for (var key in this._props) {
+ var value = this._props[key];
+ if (value instanceof Animated) {
+ props[key] = value.getAnimatedValue();
+ }
+ }
+ return props;
+ }
+
+ attach(): void {
+ for (var key in this._props) {
+ var value = this._props[key];
+ if (value instanceof Animated) {
+ value.addChild(this);
+ }
+ }
+ }
+
+ detach(): void {
+ for (var key in this._props) {
+ var value = this._props[key];
+ if (value instanceof Animated) {
+ value.removeChild(this);
+ }
+ }
+ }
+
+ update(): void {
+ this._callback();
+ }
+}
+
+function createAnimatedComponent(Component: any): any {
+ var refName = 'node';
+
+ class AnimatedComponent extends React.Component {
+ _propsAnimated: AnimatedProps;
+
+ componentWillUnmount() {
+ this._propsAnimated && this._propsAnimated.detach();
+ }
+
+ setNativeProps(props) {
+ this.refs[refName].setNativeProps(props);
+ }
+
+ componentWillMount() {
+ this.attachProps(this.props);
+ }
+
+ attachProps(nextProps) {
+ var oldPropsAnimated = this._propsAnimated;
+
+ // The system is best designed when setNativeProps is implemented. It is
+ // able to avoid re-rendering and directly set the attributes that
+ // changed. However, setNativeProps can only be implemented on leaf
+ // native components. If you want to animate a composite component, you
+ // need to re-render it. In this case, we have a fallback that uses
+ // forceUpdate.
+ var callback = () => {
+ if (this.refs[refName].setNativeProps) {
+ var value = this._propsAnimated.getAnimatedValue();
+ this.refs[refName].setNativeProps(value);
+ } else {
+ this.forceUpdate();
+ }
+ };
+
+ this._propsAnimated = new AnimatedProps(
+ nextProps,
+ callback,
+ );
+
+ // When you call detach, it removes the element from the parent list
+ // of children. If it goes to 0, then the parent also detaches itself
+ // and so on.
+ // An optimization is to attach the new elements and THEN detach the old
+ // ones instead of detaching and THEN attaching.
+ // This way the intermediate state isn't to go to 0 and trigger
+ // this expensive recursive detaching to then re-attach everything on
+ // the very next operation.
+ oldPropsAnimated && oldPropsAnimated.detach();
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.attachProps(nextProps);
+ }
+
+ render() {
+ return (
+
+ );
+ }
+ }
+
+ return AnimatedComponent;
+}
+
+class AnimatedTracking extends Animated {
+ _value: AnimatedValue;
+ _parent: Animated;
+ _callback: ?EndCallback;
+ _animationConfig: Object;
+ _animationClass: any;
+
+ constructor(
+ value: AnimatedValue,
+ parent: Animated,
+ animationClass: any,
+ animationConfig: Object,
+ callback?: ?EndCallback,
+ ) {
+ super();
+ this._value = value;
+ this._parent = parent;
+ this._animationClass = animationClass;
+ this._animationConfig = animationConfig;
+ this._callback = callback;
+ this.attach();
+ }
+
+ __getValue(): Object {
+ return this._parent.__getValue();
+ }
+
+ attach(): void {
+ this._parent.addChild(this);
+ }
+
+ detach(): void {
+ this._parent.removeChild(this);
+ }
+
+ update(): void {
+ this._value.animate(new this._animationClass({
+ ...this._animationConfig,
+ toValue: (this._animationConfig.toValue: any).__getValue(),
+ }), this._callback);
+ }
+}
+
+type CompositeAnimation = {
+ start: (callback?: ?EndCallback) => void;
+ stop: () => void;
+};
+
+var maybeVectorAnim = function(
+ value: AnimatedValue | AnimatedValueXY,
+ config: Object,
+ anim: (value: AnimatedValue, config: Object) => CompositeAnimation
+): ?CompositeAnimation {
+ if (value instanceof AnimatedValueXY) {
+ var configX = {...config};
+ var configY = {...config};
+ for (var key in config) {
+ var {x, y} = config[key];
+ if (x !== undefined && y !== undefined) {
+ configX[key] = x;
+ configY[key] = y;
+ }
+ }
+ var aX = anim((value: AnimatedValueXY).x, configX);
+ var aY = anim((value: AnimatedValueXY).y, configY);
+ // We use `stopTogether: false` here because otherwise tracking will break
+ // because the second animation will get stopped before it can update.
+ return parallel([aX, aY], {stopTogether: false});
+ }
+ return null;
+};
+
+var spring = function(
+ value: AnimatedValue | AnimatedValueXY,
+ config: SpringAnimationConfig,
+): CompositeAnimation {
+ return maybeVectorAnim(value, config, spring) || {
+ start: function(callback?: ?EndCallback): void {
+ var singleValue: any = value;
+ var singleConfig: any = config;
+ singleValue.stopTracking();
+ if (config.toValue instanceof Animated) {
+ singleValue.track(new AnimatedTracking(
+ singleValue,
+ config.toValue,
+ SpringAnimation,
+ singleConfig,
+ callback
+ ));
+ } else {
+ singleValue.animate(new SpringAnimation(singleConfig), callback);
+ }
+ },
+
+ stop: function(): void {
+ value.stopAnimation();
+ },
+ };
+};
+
+var timing = function(
+ value: AnimatedValue | AnimatedValueXY,
+ config: TimingAnimationConfig,
+): CompositeAnimation {
+ return maybeVectorAnim(value, config, timing) || {
+ start: function(callback?: ?EndCallback): void {
+ var singleValue: any = value;
+ var singleConfig: any = config;
+ singleValue.stopTracking();
+ if (config.toValue instanceof Animated) {
+ singleValue.track(new AnimatedTracking(
+ singleValue,
+ config.toValue,
+ TimingAnimation,
+ singleConfig,
+ callback
+ ));
+ } else {
+ singleValue.animate(new TimingAnimation(singleConfig), callback);
+ }
+ },
+
+ stop: function(): void {
+ value.stopAnimation();
+ },
+ };
+};
+
+var decay = function(
+ value: AnimatedValue | AnimatedValueXY,
+ config: DecayAnimationConfig,
+): CompositeAnimation {
+ return maybeVectorAnim(value, config, decay) || {
+ start: function(callback?: ?EndCallback): void {
+ var singleValue: any = value;
+ var singleConfig: any = config;
+ singleValue.stopTracking();
+ singleValue.animate(new DecayAnimation(singleConfig), callback);
+ },
+
+ stop: function(): void {
+ value.stopAnimation();
+ },
+ };
+};
+
+var sequence = function(
+ animations: Array,
+): CompositeAnimation {
+ var current = 0;
+ return {
+ start: function(callback?: ?EndCallback) {
+ var onComplete = function(result) {
+ if (!result.finished) {
+ callback && callback(result);
+ return;
+ }
+
+ current++;
+
+ if (current === animations.length) {
+ callback && callback(result);
+ return;
+ }
+
+ animations[current].start(onComplete);
+ };
+
+ if (animations.length === 0) {
+ callback && callback({finished: true});
+ } else {
+ animations[current].start(onComplete);
+ }
+ },
+
+ stop: function() {
+ if (current < animations.length) {
+ animations[current].stop();
+ }
+ }
+ };
+};
+
+type ParallelConfig = {
+ stopTogether?: bool; // If one is stopped, stop all. default: true
+}
+var parallel = function(
+ animations: Array,
+ config?: ?ParallelConfig,
+): CompositeAnimation {
+ var doneCount = 0;
+ // Make sure we only call stop() at most once for each animation
+ var hasEnded = {};
+ var stopTogether = !(config && config.stopTogether === false);
+
+ var result = {
+ start: function(callback?: ?EndCallback) {
+ if (doneCount === animations.length) {
+ callback && callback({finished: true});
+ return;
+ }
+
+ animations.forEach((animation, idx) => {
+ animation.start(endResult => {
+ hasEnded[idx] = true;
+ doneCount++;
+ if (doneCount === animations.length) {
+ doneCount = 0;
+ callback && callback(endResult);
+ return;
+ }
+
+ if (!endResult.finished && stopTogether) {
+ result.stop();
+ }
+ });
+ });
+ },
+
+ stop: function(): void {
+ animations.forEach((animation, idx) => {
+ !hasEnded[idx] && animation.stop();
+ hasEnded[idx] = true;
+ });
+ }
+ };
+
+ return result;
+};
+
+var delay = function(time: number): CompositeAnimation {
+ // Would be nice to make a specialized implementation
+ return timing(new AnimatedValue(0), {toValue: 0, delay: time, duration: 0});
+};
+
+var stagger = function(
+ time: number,
+ animations: Array,
+): CompositeAnimation {
+ return parallel(animations.map((animation, i) => {
+ return sequence([
+ delay(time * i),
+ animation,
+ ]);
+ }));
+};
+
+type Mapping = {[key: string]: Mapping} | AnimatedValue;
+
+/**
+ * Takes an array of mappings and extracts values from each arg accordingly,
+ * then calls setValue on the mapped outputs. e.g.
+ *
+ * onScroll={this.AnimatedEvent(
+ * [{nativeEvent: {contentOffset: {x: this._scrollX}}}]
+ * {listener} // optional listener invoked asynchronously
+ * )
+ * ...
+ * onPanResponderMove: this.AnimatedEvent([
+ * null, // raw event arg
+ * {dx: this._panX}, // gestureState arg
+ * ]),
+ *
+ */
+type EventConfig = {listener?: ?Function};
+var event = function(
+ argMapping: Array,
+ config?: ?EventConfig,
+): () => void {
+ return function(...args): void {
+ var traverse = function(recMapping, recEvt, key) {
+ if (typeof recEvt === 'number') {
+ invariant(
+ recMapping instanceof AnimatedValue,
+ 'Bad mapping of type ' + typeof recMapping + ' for key ' + key +
+ ', event value must map to AnimatedValue'
+ );
+ recMapping.setValue(recEvt);
+ return;
+ }
+ invariant(
+ typeof recMapping === 'object',
+ 'Bad mapping of type ' + typeof recMapping + ' for key ' + key
+ );
+ invariant(
+ typeof recEvt === 'object',
+ 'Bad event of type ' + typeof recEvt + ' for key ' + key
+ );
+ for (var key in recMapping) {
+ traverse(recMapping[key], recEvt[key], key);
+ }
+ };
+ argMapping.forEach((mapping, idx) => {
+ traverse(mapping, args[idx], 'arg' + idx);
+ });
+ if (config && config.listener) {
+ config.listener.apply(null, args);
+ }
+ };
+};
+
+module.exports = {
+ delay,
+ sequence,
+ parallel,
+ stagger,
+
+ decay,
+ timing,
+ spring,
+
+ event,
+
+ Value: AnimatedValue,
+ ValueXY: AnimatedValueXY,
+ __PropsOnlyForTests: AnimatedProps,
+ View: createAnimatedComponent(View),
+ Text: createAnimatedComponent(Text),
+ Image: createAnimatedComponent(Image),
+ createAnimatedComponent,
+};
diff --git a/Libraries/Animation/Animated/Easing.js b/Libraries/Animation/Animated/Easing.js
new file mode 100644
index 000000000..ae90136d1
--- /dev/null
+++ b/Libraries/Animation/Animated/Easing.js
@@ -0,0 +1,148 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule Easing
+ * @flow
+ */
+'use strict';
+
+var bezier = require('bezier');
+
+/**
+ * This class implements common easing functions. The math is pretty obscure,
+ * but this cool website has nice visual illustrations of what they represent:
+ * http://xaedes.de/dev/transitions/
+ */
+class Easing {
+ static step0(n) {
+ return n > 0 ? 1 : 0;
+ }
+
+ static step1(n) {
+ return n >= 1 ? 1 : 0;
+ }
+
+ static linear(t) {
+ return t;
+ }
+
+ static ease(t: number): number {
+ return ease(t);
+ }
+
+ static quad(t) {
+ return t * t;
+ }
+
+ static cubic(t) {
+ return t * t * t;
+ }
+
+ static poly(n) {
+ return (t) => Math.pow(t, n);
+ }
+
+ static sin(t) {
+ return 1 - Math.cos(t * Math.PI / 2);
+ }
+
+ static circle(t) {
+ return 1 - Math.sqrt(1 - t * t);
+ }
+
+ static exp(t) {
+ return Math.pow(2, 10 * (t - 1));
+ }
+
+ static elastic(a: number, p: number): (t: number) => number {
+ var tau = Math.PI * 2;
+ // flow isn't smart enough to figure out that s is always assigned to a
+ // number before being used in the returned function
+ var s: any;
+ if (arguments.length < 2) {
+ p = 0.45;
+ }
+ if (arguments.length) {
+ s = p / tau * Math.asin(1 / a);
+ } else {
+ a = 1;
+ s = p / 4;
+ }
+ return (t) => 1 + a * Math.pow(2, -10 * t) * Math.sin((t - s) * tau / p);
+ };
+
+ static back(s: number): (t: number) => number {
+ if (s === undefined) {
+ s = 1.70158;
+ }
+ return (t) => t * t * ((s + 1) * t - s);
+ };
+
+ static bounce(t: number): number {
+ if (t < 1 / 2.75) {
+ return 7.5625 * t * t;
+ }
+
+ if (t < 2 / 2.75) {
+ t -= 1.5 / 2.75;
+ return 7.5625 * t * t + 0.75;
+ }
+
+ if (t < 2.5 / 2.75) {
+ t -= 2.25 / 2.75;
+ return 7.5625 * t * t + 0.9375;
+ }
+
+ t -= 2.625 / 2.75;
+ return 7.5625 * t * t + 0.984375;
+ };
+
+ static bezier(
+ x1: number,
+ y1: number,
+ x2: number,
+ y2: number,
+ epsilon?: ?number,
+ ): (t: number) => number {
+ if (epsilon === undefined) {
+ // epsilon determines the precision of the solved values
+ // a good approximation is:
+ var duration = 500; // duration of animation in milliseconds.
+ epsilon = (1000 / 60 / duration) / 4;
+ }
+
+ return bezier(x1, y1, x2, y2, epsilon);
+ }
+
+ static in(
+ easing: (t: number) => number,
+ ): (t: number) => number {
+ return easing;
+ }
+
+ static out(
+ easing: (t: number) => number,
+ ): (t: number) => number {
+ return (t) => 1 - easing(1 - t);
+ }
+
+ static inOut(
+ easing: (t: number) => number,
+ ): (t: number) => number {
+ return (t) => {
+ if (t < 0.5) {
+ return easing(t * 2) / 2;
+ }
+ return 1 - easing((1 - t) * 2) / 2;
+ };
+ }
+}
+
+var ease = Easing.bezier(0.42, 0, 1, 1);
+
+module.exports = Easing;
diff --git a/Libraries/Animation/Animated/Interpolation.js b/Libraries/Animation/Animated/Interpolation.js
new file mode 100644
index 000000000..bed22ec85
--- /dev/null
+++ b/Libraries/Animation/Animated/Interpolation.js
@@ -0,0 +1,258 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule Interpolation
+ * @flow
+ */
+'use strict';
+
+var invariant = require('invariant');
+
+type ExtrapolateType = 'extend' | 'identity' | 'clamp';
+
+// $FlowFixMe D2163827
+export type InterpolationConfigType = {
+ inputRange: Array;
+ outputRange: (Array | Array);
+ easing?: ((input: number) => number);
+ extrapolate?: ExtrapolateType;
+ extrapolateLeft?: ExtrapolateType;
+ extrapolateRight?: ExtrapolateType;
+};
+
+var linear = (t) => t;
+
+/**
+ * Very handy helper to map input ranges to output ranges with an easing
+ * function and custom behavior outside of the ranges.
+ */
+class Interpolation {
+ static create(config: InterpolationConfigType): (input: number) => number | string {
+
+ if (config.outputRange && typeof config.outputRange[0] === 'string') {
+ return createInterpolationFromStringOutputRange(config);
+ }
+
+ var outputRange: Array = (config.outputRange: any);
+ checkInfiniteRange('outputRange', outputRange);
+
+ var inputRange = config.inputRange;
+ checkInfiniteRange('inputRange', inputRange);
+ checkValidInputRange(inputRange);
+
+ invariant(
+ inputRange.length === outputRange.length,
+ 'inputRange (' + inputRange.length + ') and outputRange (' +
+ outputRange.length + ') must have the same length'
+ );
+
+ var easing = config.easing || linear;
+
+ var extrapolateLeft: ExtrapolateType = 'extend';
+ if (config.extrapolateLeft !== undefined) {
+ extrapolateLeft = config.extrapolateLeft;
+ } else if (config.extrapolate !== undefined) {
+ extrapolateLeft = config.extrapolate;
+ }
+
+ var extrapolateRight: ExtrapolateType = 'extend';
+ if (config.extrapolateRight !== undefined) {
+ extrapolateRight = config.extrapolateRight;
+ } else if (config.extrapolate !== undefined) {
+ extrapolateRight = config.extrapolate;
+ }
+
+ return (input) => {
+ invariant(
+ typeof input === 'number',
+ 'Cannot interpolation an input which is not a number'
+ );
+
+ var range = findRange(input, inputRange);
+ return interpolate(
+ input,
+ inputRange[range],
+ inputRange[range + 1],
+ outputRange[range],
+ outputRange[range + 1],
+ easing,
+ extrapolateLeft,
+ extrapolateRight,
+ );
+ };
+ }
+}
+
+function interpolate(
+ input: number,
+ inputMin: number,
+ inputMax: number,
+ outputMin: number,
+ outputMax: number,
+ easing: ((input: number) => number),
+ extrapolateLeft: ExtrapolateType,
+ extrapolateRight: ExtrapolateType,
+) {
+ var result = input;
+
+ // Extrapolate
+ if (result < inputMin) {
+ if (extrapolateLeft === 'identity') {
+ return result;
+ } else if (extrapolateLeft === 'clamp') {
+ result = inputMin;
+ } else if (extrapolateLeft === 'extend') {
+ // noop
+ }
+ }
+
+ if (result > inputMax) {
+ if (extrapolateRight === 'identity') {
+ return result;
+ } else if (extrapolateRight === 'clamp') {
+ result = inputMax;
+ } else if (extrapolateRight === 'extend') {
+ // noop
+ }
+ }
+
+ if (outputMin === outputMax) {
+ return outputMin;
+ }
+
+ if (inputMin === inputMax) {
+ if (input <= inputMin) {
+ return outputMin;
+ }
+ return outputMax;
+ }
+
+ // Input Range
+ if (inputMin === -Infinity) {
+ result = -result;
+ } else if (inputMax === Infinity) {
+ result = result - inputMin;
+ } else {
+ result = (result - inputMin) / (inputMax - inputMin);
+ }
+
+ // Easing
+ result = easing(result);
+
+ // Output Range
+ if (outputMin === -Infinity) {
+ result = -result;
+ } else if (outputMax === Infinity) {
+ result = result + outputMin;
+ } else {
+ result = result * (outputMax - outputMin) + outputMin;
+ }
+
+ return result;
+}
+
+var stringShapeRegex = /[0-9\.-]+/g;
+
+/**
+ * Supports string shapes by extracting numbers so new values can be computed,
+ * and recombines those values into new strings of the same shape. Supports
+ * things like:
+ *
+ * rgba(123, 42, 99, 0.36) // colors
+ * -45deg // values with units
+ */
+function createInterpolationFromStringOutputRange(
+ config: InterpolationConfigType,
+): (input: number) => string {
+ var outputRange: Array = (config.outputRange: any);
+ invariant(outputRange.length >= 2, 'Bad output range');
+ checkPattern(outputRange);
+
+ // ['rgba(0, 100, 200, 0)', 'rgba(50, 150, 250, 0.5)']
+ // ->
+ // [
+ // [0, 50],
+ // [100, 150],
+ // [200, 250],
+ // [0, 0.5],
+ // ]
+ var outputRanges = outputRange[0].match(stringShapeRegex).map(() => []);
+ outputRange.forEach(value => {
+ value.match(stringShapeRegex).forEach((number, i) => {
+ outputRanges[i].push(+number);
+ });
+ });
+
+ var interpolations = outputRange[0].match(stringShapeRegex).map((value, i) => {
+ return Interpolation.create({
+ ...config,
+ outputRange: outputRanges[i],
+ });
+ });
+
+ return (input) => {
+ var i = 0;
+ // 'rgba(0, 100, 200, 0)'
+ // ->
+ // 'rgba(${interpolations[0](input)}, ${interpolations[1](input)}, ...'
+ return outputRange[0].replace(stringShapeRegex, () => {
+ return String(interpolations[i++](input));
+ });
+ };
+}
+
+function checkPattern(arr: Array) {
+ var pattern = arr[0].replace(stringShapeRegex, '');
+ for (var i = 1; i < arr.length; ++i) {
+ invariant(
+ pattern === arr[i].replace(stringShapeRegex, ''),
+ 'invalid pattern ' + arr[0] + ' and ' + arr[i],
+ );
+ }
+}
+
+function findRange(input: number, inputRange: Array) {
+ for (var i = 1; i < inputRange.length - 1; ++i) {
+ if (inputRange[i] >= input) {
+ break;
+ }
+ }
+ return i - 1;
+}
+
+function checkValidInputRange(arr: Array) {
+ invariant(arr.length >= 2, 'inputRange must have at least 2 elements');
+ for (var i = 1; i < arr.length; ++i) {
+ invariant(
+ arr[i] >= arr[i - 1],
+ /* $FlowFixMe(>=0.13.0) - In the addition expression below this comment,
+ * one or both of the operands may be something that doesn't cleanly
+ * convert to a string, like undefined, null, and object, etc. If you really
+ * mean this implicit string conversion, you can do something like
+ * String(myThing)
+ */
+ 'inputRange must be monolithically increasing ' + arr
+ );
+ }
+}
+
+function checkInfiniteRange(name: string, arr: Array) {
+ invariant(arr.length >= 2, name + ' must have at least 2 elements');
+ invariant(
+ arr.length !== 2 || arr[0] !== -Infinity || arr[1] !== Infinity,
+ /* $FlowFixMe(>=0.13.0) - In the addition expression below this comment,
+ * one or both of the operands may be something that doesn't cleanly convert
+ * to a string, like undefined, null, and object, etc. If you really mean
+ * this implicit string conversion, you can do something like
+ * String(myThing)
+ */
+ name + 'cannot be ]-infinity;+infinity[ ' + arr
+ );
+}
+
+module.exports = Interpolation;
diff --git a/Libraries/Animation/Animated/__tests__/Animated-test.js b/Libraries/Animation/Animated/__tests__/Animated-test.js
new file mode 100644
index 000000000..bd9ef68e5
--- /dev/null
+++ b/Libraries/Animation/Animated/__tests__/Animated-test.js
@@ -0,0 +1,449 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+'use strict';
+
+jest
+ .autoMockOff()
+ .setMock('Text', {})
+ .setMock('View', {})
+ .setMock('Image', {})
+ .setMock('React', {Component: class {}});
+
+var Animated = require('Animated');
+
+describe('Animated', () => {
+ it('works end to end', () => {
+ var anim = new Animated.Value(0);
+
+ var callback = jest.genMockFunction();
+
+ var node = new Animated.__PropsOnlyForTests({
+ style: {
+ backgroundColor: 'red',
+ opacity: anim,
+ transform: [
+ {translateX: anim.interpolate({
+ inputRange: [0, 1],
+ outputRange: [100, 200],
+ })},
+ {scale: anim},
+ ]
+ }
+ }, callback);
+
+ expect(anim.getChildren().length).toBe(3);
+
+ expect(node.__getValue()).toEqual({
+ style: {
+ backgroundColor: 'red',
+ opacity: 0,
+ transform: [
+ {translateX: 100},
+ {scale: 0},
+ ],
+ },
+ });
+
+ anim.setValue(0.5);
+
+ expect(callback).toBeCalled();
+
+ expect(node.__getValue()).toEqual({
+ style: {
+ backgroundColor: 'red',
+ opacity: 0.5,
+ transform: [
+ {translateX: 150},
+ {scale: 0.5},
+ ],
+ },
+ });
+
+ node.detach();
+ expect(anim.getChildren().length).toBe(0);
+
+ anim.setValue(1);
+ expect(callback.mock.calls.length).toBe(1);
+ });
+
+ it('does not detach on updates', () => {
+ var anim = new Animated.Value(0);
+ anim.detach = jest.genMockFunction();
+
+ var c = new Animated.View();
+ c.props = {
+ style: {
+ opacity: anim,
+ },
+ };
+ c.componentWillMount();
+
+ expect(anim.detach).not.toBeCalled();
+ c.componentWillReceiveProps({
+ style: {
+ opacity: anim,
+ },
+ });
+ expect(anim.detach).not.toBeCalled();
+
+ c.componentWillUnmount();
+ expect(anim.detach).toBeCalled();
+ });
+
+
+ it('stops animation when detached', () => {
+ // jest environment doesn't have requestAnimationFrame :(
+ window.requestAnimationFrame = jest.genMockFunction();
+ window.cancelAnimationFrame = jest.genMockFunction();
+
+ var anim = new Animated.Value(0);
+ var callback = jest.genMockFunction();
+
+ var c = new Animated.View();
+ c.props = {
+ style: {
+ opacity: anim,
+ },
+ };
+ c.componentWillMount();
+
+ Animated.timing(anim, {toValue: 10, duration: 1000}).start(callback);
+
+ c.componentWillUnmount();
+
+ expect(callback).toBeCalledWith({finished: false});
+ expect(callback).toBeCalledWith({finished: false});
+ });
+
+ it('triggers callback when spring is at rest', () => {
+ var anim = new Animated.Value(0);
+ var callback = jest.genMockFunction();
+ Animated.spring(anim, {toValue: 0, velocity: 0}).start(callback);
+ expect(callback).toBeCalled();
+ });
+});
+
+
+describe('Animated Sequence', () => {
+
+ it('works with an empty sequence', () => {
+ var cb = jest.genMockFunction();
+ Animated.sequence([]).start(cb);
+ expect(cb).toBeCalledWith({finished: true});
+ });
+
+ it('sequences well', () => {
+ var anim1 = {start: jest.genMockFunction()};
+ var anim2 = {start: jest.genMockFunction()};
+ var cb = jest.genMockFunction();
+
+ var seq = Animated.sequence([anim1, anim2]);
+
+ expect(anim1.start).not.toBeCalled();
+ expect(anim2.start).not.toBeCalled();
+
+ seq.start(cb);
+
+ expect(anim1.start).toBeCalled();
+ expect(anim2.start).not.toBeCalled();
+ expect(cb).not.toBeCalled();
+
+ anim1.start.mock.calls[0][0]({finished: true});
+
+ expect(anim2.start).toBeCalled();
+ expect(cb).not.toBeCalled();
+
+ anim2.start.mock.calls[0][0]({finished: true});
+ expect(cb).toBeCalledWith({finished: true});
+ });
+
+ it('supports interrupting sequence', () => {
+ var anim1 = {start: jest.genMockFunction()};
+ var anim2 = {start: jest.genMockFunction()};
+ var cb = jest.genMockFunction();
+
+ Animated.sequence([anim1, anim2]).start(cb);
+
+ anim1.start.mock.calls[0][0]({finished: false});
+
+ expect(anim1.start).toBeCalled();
+ expect(anim2.start).not.toBeCalled();
+ expect(cb).toBeCalledWith({finished: false});
+ });
+
+ it('supports stopping sequence', () => {
+ var anim1 = {start: jest.genMockFunction(), stop: jest.genMockFunction()};
+ var anim2 = {start: jest.genMockFunction(), stop: jest.genMockFunction()};
+ var cb = jest.genMockFunction();
+
+ var seq = Animated.sequence([anim1, anim2]);
+ seq.start(cb);
+ seq.stop();
+
+ expect(anim1.stop).toBeCalled();
+ expect(anim2.stop).not.toBeCalled();
+ expect(cb).not.toBeCalled();
+
+ anim1.start.mock.calls[0][0]({finished: false});
+
+ expect(cb).toBeCalledWith({finished: false});
+ });
+});
+
+
+describe('Animated Parallel', () => {
+
+ it('works with an empty parallel', () => {
+ var cb = jest.genMockFunction();
+ Animated.parallel([]).start(cb);
+ expect(cb).toBeCalledWith({finished: true});
+ });
+
+
+ it('parellelizes well', () => {
+ var anim1 = {start: jest.genMockFunction()};
+ var anim2 = {start: jest.genMockFunction()};
+ var cb = jest.genMockFunction();
+
+ var par = Animated.parallel([anim1, anim2]);
+
+ expect(anim1.start).not.toBeCalled();
+ expect(anim2.start).not.toBeCalled();
+
+ par.start(cb);
+
+ expect(anim1.start).toBeCalled();
+ expect(anim2.start).toBeCalled();
+ expect(cb).not.toBeCalled();
+
+ anim1.start.mock.calls[0][0]({finished: true});
+ expect(cb).not.toBeCalled();
+
+ anim2.start.mock.calls[0][0]({finished: true});
+ expect(cb).toBeCalledWith({finished: true});
+ });
+
+ it('supports stopping parallel', () => {
+ var anim1 = {start: jest.genMockFunction(), stop: jest.genMockFunction()};
+ var anim2 = {start: jest.genMockFunction(), stop: jest.genMockFunction()};
+ var cb = jest.genMockFunction();
+
+ var seq = Animated.parallel([anim1, anim2]);
+ seq.start(cb);
+ seq.stop();
+
+ expect(anim1.stop).toBeCalled();
+ expect(anim2.stop).toBeCalled();
+ expect(cb).not.toBeCalled();
+
+ anim1.start.mock.calls[0][0]({finished: false});
+ expect(cb).not.toBeCalled();
+
+ anim2.start.mock.calls[0][0]({finished: false});
+ expect(cb).toBeCalledWith({finished: false});
+ });
+
+
+ it('does not call stop more than once when stopping', () => {
+ var anim1 = {start: jest.genMockFunction(), stop: jest.genMockFunction()};
+ var anim2 = {start: jest.genMockFunction(), stop: jest.genMockFunction()};
+ var anim3 = {start: jest.genMockFunction(), stop: jest.genMockFunction()};
+ var cb = jest.genMockFunction();
+
+ var seq = Animated.parallel([anim1, anim2, anim3]);
+ seq.start(cb);
+
+ anim1.start.mock.calls[0][0]({finished: false});
+
+ expect(anim1.stop.mock.calls.length).toBe(0);
+ expect(anim2.stop.mock.calls.length).toBe(1);
+ expect(anim3.stop.mock.calls.length).toBe(1);
+
+ anim2.start.mock.calls[0][0]({finished: false});
+
+ expect(anim1.stop.mock.calls.length).toBe(0);
+ expect(anim2.stop.mock.calls.length).toBe(1);
+ expect(anim3.stop.mock.calls.length).toBe(1);
+
+ anim3.start.mock.calls[0][0]({finished: false});
+
+ expect(anim1.stop.mock.calls.length).toBe(0);
+ expect(anim2.stop.mock.calls.length).toBe(1);
+ expect(anim3.stop.mock.calls.length).toBe(1);
+ });
+});
+
+describe('Animated Events', () => {
+ it('should map events', () => {
+ var value = new Animated.Value(0);
+ var handler = Animated.event(
+ [null, {state: {foo: value}}],
+ );
+ handler({bar: 'ignoreBar'}, {state: {baz: 'ignoreBaz', foo: 42}});
+ expect(value.__getValue()).toBe(42);
+ });
+ it('should call listeners', () => {
+ var value = new Animated.Value(0);
+ var listener = jest.genMockFunction();
+ var handler = Animated.event(
+ [{foo: value}],
+ {listener},
+ );
+ handler({foo: 42});
+ expect(value.__getValue()).toBe(42);
+ expect(listener.mock.calls.length).toBe(1);
+ expect(listener).toBeCalledWith({foo: 42});
+ });
+});
+
+describe('Animated Tracking', () => {
+ it('should track values', () => {
+ var value1 = new Animated.Value(0);
+ var value2 = new Animated.Value(0);
+ Animated.timing(value2, {
+ toValue: value1,
+ duration: 0,
+ }).start();
+ value1.setValue(42);
+ expect(value2.__getValue()).toBe(42);
+ value1.setValue(7);
+ expect(value2.__getValue()).toBe(7);
+ });
+
+ it('should track interpolated values', () => {
+ var value1 = new Animated.Value(0);
+ var value2 = new Animated.Value(0);
+ Animated.timing(value2, {
+ toValue: value1.interpolate({
+ inputRange: [0, 2],
+ outputRange: [0, 1]
+ }),
+ duration: 0,
+ }).start();
+ value1.setValue(42);
+ expect(value2.__getValue()).toBe(42 / 2);
+ });
+
+ it('should stop tracking when animated', () => {
+ var value1 = new Animated.Value(0);
+ var value2 = new Animated.Value(0);
+ Animated.timing(value2, {
+ toValue: value1,
+ duration: 0,
+ }).start();
+ value1.setValue(42);
+ expect(value2.__getValue()).toBe(42);
+ Animated.timing(value2, {
+ toValue: 7,
+ duration: 0,
+ }).start();
+ value1.setValue(1492);
+ expect(value2.__getValue()).toBe(7);
+ });
+});
+
+describe('Animated Vectors', () => {
+ it('should animate vectors', () => {
+ var vec = new Animated.ValueXY();
+
+ var callback = jest.genMockFunction();
+
+ var node = new Animated.__PropsOnlyForTests({
+ style: {
+ opacity: vec.x.interpolate({
+ inputRange: [0, 42],
+ outputRange: [0.2, 0.8],
+ }),
+ transform: vec.getTranslateTransform(),
+ ...vec.getLayout(),
+ }
+ }, callback);
+
+ expect(node.__getValue()).toEqual({
+ style: {
+ opacity: 0.2,
+ transform: [
+ {translateX: 0},
+ {translateY: 0},
+ ],
+ left: 0,
+ top: 0,
+ },
+ });
+
+ vec.setValue({x: 42, y: 1492});
+
+ expect(callback.mock.calls.length).toBe(2); // once each for x, y
+
+ expect(node.__getValue()).toEqual({
+ style: {
+ opacity: 0.8,
+ transform: [
+ {translateX: 42},
+ {translateY: 1492},
+ ],
+ left: 42,
+ top: 1492,
+ },
+ });
+
+ node.detach();
+
+ vec.setValue({x: 1, y: 1});
+ expect(callback.mock.calls.length).toBe(2);
+ });
+
+ it('should track vectors', () => {
+ var value1 = new Animated.ValueXY();
+ var value2 = new Animated.ValueXY();
+ Animated.timing(value2, {
+ toValue: value1,
+ duration: 0,
+ }).start();
+ value1.setValue({x: 42, y: 1492});
+ expect(value2.__getValue()).toEqual({x: 42, y: 1492});
+
+ // Make sure tracking keeps working (see stopTogether in ParallelConfig used
+ // by maybeVectorAnim).
+ value1.setValue({x: 3, y: 4});
+ expect(value2.__getValue()).toEqual({x: 3, y: 4});
+ });
+});
+
+describe('Animated Listeners', () => {
+ it('should get updates', () => {
+ var value1 = new Animated.Value(0);
+ var listener = jest.genMockFunction();
+ var id = value1.addListener(listener);
+ value1.setValue(42);
+ expect(listener.mock.calls.length).toBe(1);
+ expect(listener).toBeCalledWith({value: 42});
+ expect(value1.__getValue()).toBe(42);
+ value1.setValue(7);
+ expect(listener.mock.calls.length).toBe(2);
+ expect(listener).toBeCalledWith({value: 7});
+ expect(value1.__getValue()).toBe(7);
+ value1.removeListener(id);
+ value1.setValue(1492);
+ expect(listener.mock.calls.length).toBe(2);
+ expect(value1.__getValue()).toBe(1492);
+ });
+
+ it('should removeAll', () => {
+ var value1 = new Animated.Value(0);
+ var listener = jest.genMockFunction();
+ [1,2,3,4].forEach(() => value1.addListener(listener));
+ value1.setValue(42);
+ expect(listener.mock.calls.length).toBe(4);
+ expect(listener).toBeCalledWith({value: 42});
+ value1.removeAllListeners();
+ value1.setValue(7);
+ expect(listener.mock.calls.length).toBe(4);
+ });
+});
diff --git a/Libraries/Animation/Animated/__tests__/Easing-test.js b/Libraries/Animation/Animated/__tests__/Easing-test.js
new file mode 100644
index 000000000..bee894d6f
--- /dev/null
+++ b/Libraries/Animation/Animated/__tests__/Easing-test.js
@@ -0,0 +1,119 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+'use strict';
+
+jest.dontMock('Easing');
+
+var Easing = require('Easing');
+describe('Easing', () => {
+ it('should work with linear', () => {
+ var easing = Easing.linear;
+
+ expect(easing(0)).toBe(0);
+ expect(easing(0.5)).toBe(0.5);
+ expect(easing(0.8)).toBe(0.8);
+ expect(easing(1)).toBe(1);
+ });
+
+ it('should work with ease in linear', () => {
+ var easing = Easing.in(Easing.linear);
+ expect(easing(0)).toBe(0);
+ expect(easing(0.5)).toBe(0.5);
+ expect(easing(0.8)).toBe(0.8);
+ expect(easing(1)).toBe(1);
+ });
+
+ it('should work with easy out linear', () => {
+ var easing = Easing.out(Easing.linear);
+ expect(easing(0)).toBe(0);
+ expect(easing(0.5)).toBe(0.5);
+ expect(easing(0.6)).toBe(0.6);
+ expect(easing(1)).toBe(1);
+ });
+
+ it('should work with ease in quad', () => {
+ function easeInQuad(t) {
+ return t * t;
+ }
+ var easing = Easing.in(Easing.quad);
+ for (var t = -0.5; t < 1.5; t += 0.1) {
+ expect(easing(t)).toBe(easeInQuad(t));
+ }
+ });
+
+ it('should work with ease out quad', () => {
+ function easeOutQuad(t) {
+ return -t * (t - 2);
+ }
+ var easing = Easing.out(Easing.quad);
+ for (var t = 0; t <= 1; t += 0.1) {
+ expect(easing(1)).toBe(easeOutQuad(1));
+ }
+ });
+
+ it('should work with ease in-out quad', () => {
+ function easeInOutQuad(t) {
+ t = t * 2;
+ if (t < 1) {
+ return 0.5 * t * t;
+ }
+ return -((t - 1) * (t - 3) - 1) / 2;
+ }
+ var easing = Easing.inOut(Easing.quad);
+ for (var t = -0.5; t < 1.5; t += 0.1) {
+ expect(easing(t)).toBeCloseTo(easeInOutQuad(t), 4);
+ }
+ });
+
+ function sampleEasingFunction(easing) {
+ var DURATION = 300;
+ var tickCount = Math.round(DURATION * 60 / 1000);
+ var samples = [];
+ for (var i = 0; i <= tickCount; i++) {
+ samples.push(easing(i / tickCount));
+ }
+ return samples;
+ }
+
+ var Samples = {
+ in_quad: [0,0.0030864197530864196,0.012345679012345678,0.027777777777777776,0.04938271604938271,0.0771604938271605,0.1111111111111111,0.15123456790123457,0.19753086419753085,0.25,0.308641975308642,0.37345679012345684,0.4444444444444444,0.5216049382716049,0.6049382716049383,0.6944444444444445,0.7901234567901234,0.8919753086419753,1],
+ out_quad: [0,0.10802469135802469,0.20987654320987653,0.3055555555555555,0.3950617283950617,0.47839506172839513,0.5555555555555556,0.6265432098765432,0.691358024691358,0.75,0.8024691358024691,0.8487654320987654,0.888888888888889,0.9228395061728394,0.9506172839506174,0.9722222222222221,0.9876543209876543,0.9969135802469136,1],
+ inOut_quad: [0,0.006172839506172839,0.024691358024691357,0.05555555555555555,0.09876543209876543,0.154320987654321,0.2222222222222222,0.30246913580246915,0.3950617283950617,0.5,0.6049382716049383,0.697530864197531,0.7777777777777777,0.845679012345679,0.9012345679012346,0.9444444444444444,0.9753086419753086,0.9938271604938271,1],
+ in_cubic: [0,0.00017146776406035664,0.0013717421124828531,0.004629629629629629,0.010973936899862825,0.021433470507544586,0.037037037037037035,0.05881344307270234,0.0877914951989026,0.125,0.1714677640603567,0.22822359396433475,0.2962962962962963,0.37671467764060357,0.4705075445816187,0.5787037037037038,0.7023319615912208,0.8424211248285322,1],
+ out_cubic: [0,0.15757887517146785,0.2976680384087792,0.42129629629629617,0.5294924554183813,0.6232853223593964,0.7037037037037036,0.7717764060356652,0.8285322359396433,0.875,0.9122085048010974,0.9411865569272977,0.9629629629629629,0.9785665294924554,0.9890260631001372,0.9953703703703703,0.9986282578875172,0.9998285322359396,1],
+ inOut_cubic: [0,0.0006858710562414266,0.0054869684499314125,0.018518518518518517,0.0438957475994513,0.08573388203017834,0.14814814814814814,0.23525377229080935,0.3511659807956104,0.5,0.6488340192043895,0.7647462277091908,0.8518518518518519,0.9142661179698217,0.9561042524005487,0.9814814814814815,0.9945130315500685,0.9993141289437586,1],
+ in_sin: [0,0.003805301908254455,0.01519224698779198,0.03407417371093169,0.06030737921409157,0.09369221296335006,0.1339745962155613,0.1808479557110082,0.233955556881022,0.2928932188134524,0.35721239031346064,0.42642356364895384,0.4999999999999999,0.5773817382593005,0.6579798566743311,0.7411809548974793,0.8263518223330696,0.9128442572523416,0.9999999999999999],
+ out_sin: [0,0.08715574274765817,0.17364817766693033,0.25881904510252074,0.3420201433256687,0.42261826174069944,0.49999999999999994,0.573576436351046,0.6427876096865393,0.7071067811865475,0.766044443118978,0.8191520442889918,0.8660254037844386,0.9063077870366499,0.9396926207859083,0.9659258262890683,0.984807753012208,0.9961946980917455,1],
+ inOut_sin: [0,0.00759612349389599,0.030153689607045786,0.06698729810778065,0.116977778440511,0.17860619515673032,0.24999999999999994,0.32898992833716556,0.4131759111665348,0.49999999999999994,0.5868240888334652,0.6710100716628343,0.7499999999999999,0.8213938048432696,0.883022221559489,0.9330127018922194,0.9698463103929542,0.9924038765061041,1],
+ in_exp: [0,0.0014352875901128893,0.002109491677524035,0.0031003926796253885,0.004556754060844206,0.006697218616039631,0.009843133202303688,0.014466792379488908,0.021262343752724643,0.03125,0.045929202883612456,0.06750373368076916,0.09921256574801243,0.1458161299470146,0.2143109957132682,0.31498026247371835,0.46293735614364506,0.6803950000871883,1],
+ out_exp: [0,0.31960499991281155,0.5370626438563548,0.6850197375262816,0.7856890042867318,0.8541838700529854,0.9007874342519875,0.9324962663192309,0.9540707971163875,0.96875,0.9787376562472754,0.9855332076205111,0.9901568667976963,0.9933027813839603,0.9954432459391558,0.9968996073203746,0.9978905083224759,0.9985647124098871,1],
+ inOut_exp: [0,0.0010547458387620175,0.002278377030422103,0.004921566601151844,0.010631171876362321,0.022964601441806228,0.049606282874006216,0.1071554978566341,0.23146867807182253,0.5,0.7685313219281775,0.892844502143366,0.9503937171259937,0.9770353985581938,0.9893688281236377,0.9950784333988482,0.9977216229695779,0.998945254161238,1],
+ in_circle: [0,0.0015444024660317135,0.006192010000093506,0.013986702816730645,0.025003956956430873,0.03935464078941209,0.057190958417936644,0.07871533601238889,0.10419358352238339,0.1339745962155614,0.1685205807169019,0.20845517506805522,0.2546440075000701,0.3083389112228482,0.37146063894529113,0.4472292016074334,0.5418771527091488,0.6713289009389102,1],
+ out_circle: [0,0.3286710990610898,0.45812284729085123,0.5527707983925666,0.6285393610547089,0.6916610887771518,0.7453559924999298,0.7915448249319448,0.8314794192830981,0.8660254037844386,0.8958064164776166,0.9212846639876111,0.9428090415820634,0.9606453592105879,0.9749960430435691,0.9860132971832694,0.9938079899999065,0.9984555975339683,1],
+ inOut_circle: [0,0.003096005000046753,0.012501978478215436,0.028595479208968322,0.052096791761191696,0.08426029035845095,0.12732200375003505,0.18573031947264557,0.2709385763545744,0.5,0.7290614236454256,0.8142696805273546,0.8726779962499649,0.915739709641549,0.9479032082388084,0.9714045207910317,0.9874980215217846,0.9969039949999532,1],
+ in_back_: [0,-0.004788556241426612,-0.017301289437585736,-0.0347587962962963,-0.05438167352537723,-0.07339051783264748,-0.08900592592592595,-0.09844849451303156,-0.0989388203017833,-0.08769750000000004,-0.06194513031550073,-0.018902307956104283,0.044210370370370254,0.13017230795610413,0.2417629080932785,0.3817615740740742,0.5529477091906719,0.7581007167352535,0.9999999999999998],
+ out_back_: [2.220446049250313e-16,0.24189928326474652,0.44705229080932807,0.6182384259259258,0.7582370919067215,0.8698276920438959,0.9557896296296297,1.0189023079561044,1.0619451303155008,1.0876975,1.0989388203017834,1.0984484945130315,1.089005925925926,1.0733905178326475,1.0543816735253773,1.0347587962962963,1.0173012894375857,1.0047885562414267,1],
+ };
+
+ Object.keys(Samples).forEach(function(type) {
+ it('should ease ' + type, function() {
+ var [modeName, easingName, isFunction] = type.split('_');
+ var easing = Easing[easingName];
+ if (isFunction !== undefined) {
+ easing = easing();
+ }
+ var computed = sampleEasingFunction(Easing[modeName](easing));
+ var samples = Samples[type];
+
+ computed.forEach((value, key) => {
+ expect(value).toBeCloseTo(samples[key], 2);
+ });
+ });
+ });
+});
diff --git a/Libraries/Animation/Animated/__tests__/Interpolation-test.js b/Libraries/Animation/Animated/__tests__/Interpolation-test.js
new file mode 100644
index 000000000..aec20ba2b
--- /dev/null
+++ b/Libraries/Animation/Animated/__tests__/Interpolation-test.js
@@ -0,0 +1,256 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+'use strict';
+
+jest
+ .dontMock('Interpolation')
+ .dontMock('Easing');
+
+var Interpolation = require('Interpolation');
+var Easing = require('Easing');
+
+describe('Interpolation', () => {
+ it('should work with defaults', () => {
+ var interpolation = Interpolation.create({
+ inputRange: [0, 1],
+ outputRange: [0, 1],
+ });
+
+ expect(interpolation(0)).toBe(0);
+ expect(interpolation(0.5)).toBe(0.5);
+ expect(interpolation(0.8)).toBe(0.8);
+ expect(interpolation(1)).toBe(1);
+ });
+
+ it('should work with output range', () => {
+ var interpolation = Interpolation.create({
+ inputRange: [0, 1],
+ outputRange: [100, 200],
+ });
+
+ expect(interpolation(0)).toBe(100);
+ expect(interpolation(0.5)).toBe(150);
+ expect(interpolation(0.8)).toBe(180);
+ expect(interpolation(1)).toBe(200);
+ });
+
+ it('should work with input range', () => {
+ var interpolation = Interpolation.create({
+ inputRange: [100, 200],
+ outputRange: [0, 1],
+ });
+
+ expect(interpolation(100)).toBe(0);
+ expect(interpolation(150)).toBe(0.5);
+ expect(interpolation(180)).toBe(0.8);
+ expect(interpolation(200)).toBe(1);
+ });
+
+ it('should throw for non monotonic input ranges', () => {
+ expect(() => Interpolation.create({
+ inputRange: [0, 2, 1],
+ outputRange: [0, 1, 2],
+ })).toThrow();
+
+ expect(() => Interpolation.create({
+ inputRange: [0, 1, 2],
+ outputRange: [0, 3, 1],
+ })).not.toThrow();
+ });
+
+ it('should work with empty input range', () => {
+ var interpolation = Interpolation.create({
+ inputRange: [0, 10, 10],
+ outputRange: [1, 2, 3],
+ extrapolate: 'extend',
+ });
+
+ expect(interpolation(0)).toBe(1);
+ expect(interpolation(5)).toBe(1.5);
+ expect(interpolation(10)).toBe(2);
+ expect(interpolation(10.1)).toBe(3);
+ expect(interpolation(15)).toBe(3);
+ });
+
+ it('should work with empty output range', () => {
+ var interpolation = Interpolation.create({
+ inputRange: [1, 2, 3],
+ outputRange: [0, 10, 10],
+ extrapolate: 'extend',
+ });
+
+ expect(interpolation(0)).toBe(-10);
+ expect(interpolation(1.5)).toBe(5);
+ expect(interpolation(2)).toBe(10);
+ expect(interpolation(2.5)).toBe(10);
+ expect(interpolation(3)).toBe(10);
+ expect(interpolation(4)).toBe(10);
+ });
+
+ it('should work with easing', () => {
+ var interpolation = Interpolation.create({
+ inputRange: [0, 1],
+ outputRange: [0, 1],
+ easing: Easing.quad,
+ });
+
+ expect(interpolation(0)).toBe(0);
+ expect(interpolation(0.5)).toBe(0.25);
+ expect(interpolation(0.9)).toBe(0.81);
+ expect(interpolation(1)).toBe(1);
+ });
+
+ it('should work with extrapolate', () => {
+ var interpolation = Interpolation.create({
+ inputRange: [0, 1],
+ outputRange: [0, 1],
+ extrapolate: 'extend',
+ easing: Easing.quad,
+ });
+
+ expect(interpolation(-2)).toBe(4);
+ expect(interpolation(2)).toBe(4);
+
+ interpolation = Interpolation.create({
+ inputRange: [0, 1],
+ outputRange: [0, 1],
+ extrapolate: 'clamp',
+ easing: Easing.quad,
+ });
+
+ expect(interpolation(-2)).toBe(0);
+ expect(interpolation(2)).toBe(1);
+
+ interpolation = Interpolation.create({
+ inputRange: [0, 1],
+ outputRange: [0, 1],
+ extrapolate: 'identity',
+ easing: Easing.quad,
+ });
+
+ expect(interpolation(-2)).toBe(-2);
+ expect(interpolation(2)).toBe(2);
+ });
+
+ it('should work with keyframes with extrapolate', () => {
+ var interpolation = Interpolation.create({
+ inputRange: [0, 10, 100, 1000],
+ outputRange: [0, 5, 50, 500],
+ extrapolate: true,
+ });
+
+ expect(interpolation(-5)).toBe(-2.5);
+ expect(interpolation(0)).toBe(0);
+ expect(interpolation(5)).toBe(2.5);
+ expect(interpolation(10)).toBe(5);
+ expect(interpolation(50)).toBe(25);
+ expect(interpolation(100)).toBe(50);
+ expect(interpolation(500)).toBe(250);
+ expect(interpolation(1000)).toBe(500);
+ expect(interpolation(2000)).toBe(1000);
+ });
+
+ it('should work with keyframes without extrapolate', () => {
+ var interpolation = Interpolation.create({
+ inputRange: [0, 1, 2],
+ outputRange: [0.2, 1, 0.2],
+ extrapolate: 'clamp',
+ });
+
+ expect(interpolation(5)).toBeCloseTo(0.2);
+ });
+
+ it('should throw for an infinite input range', () => {
+ expect(() => Interpolation.create({
+ inputRange: [-Infinity, Infinity],
+ outputRange: [0, 1],
+ })).toThrow();
+
+ expect(() => Interpolation.create({
+ inputRange: [-Infinity, 0, Infinity],
+ outputRange: [1, 2, 3],
+ })).not.toThrow();
+ });
+
+ it('should work with negative infinite', () => {
+ var interpolation = Interpolation.create({
+ inputRange: [-Infinity, 0],
+ outputRange: [-Infinity, 0],
+ easing: Easing.quad,
+ extrapolate: 'identity',
+ });
+
+ expect(interpolation(-Infinity)).toBe(-Infinity);
+ expect(interpolation(-100)).toBeCloseTo(-10000);
+ expect(interpolation(-10)).toBeCloseTo(-100);
+ expect(interpolation(0)).toBeCloseTo(0);
+ expect(interpolation(1)).toBeCloseTo(1);
+ expect(interpolation(100)).toBeCloseTo(100);
+ });
+
+ it('should work with positive infinite', () => {
+ var interpolation = Interpolation.create({
+ inputRange: [5, Infinity],
+ outputRange: [5, Infinity],
+ easing: Easing.quad,
+ extrapolate: 'identity',
+ });
+
+ expect(interpolation(-100)).toBeCloseTo(-100);
+ expect(interpolation(-10)).toBeCloseTo(-10);
+ expect(interpolation(0)).toBeCloseTo(0);
+ expect(interpolation(5)).toBeCloseTo(5);
+ expect(interpolation(6)).toBeCloseTo(5 + 1);
+ expect(interpolation(10)).toBeCloseTo(5 + 25);
+ expect(interpolation(100)).toBeCloseTo(5 + (95 * 95));
+ expect(interpolation(Infinity)).toBe(Infinity);
+ });
+
+ it('should work with output ranges as string', () => {
+ var interpolation = Interpolation.create({
+ inputRange: [0, 1],
+ outputRange: ['rgba(0, 100, 200, 0)', 'rgba(50, 150, 250, 0.5)'],
+ });
+
+ expect(interpolation(0)).toBe('rgba(0, 100, 200, 0)');
+ expect(interpolation(0.5)).toBe('rgba(25, 125, 225, 0.25)');
+ expect(interpolation(1)).toBe('rgba(50, 150, 250, 0.5)');
+ });
+
+ it('should work with negative and decimal values in string ranges', () => {
+ var interpolation = Interpolation.create({
+ inputRange: [0, 1],
+ outputRange: ['-100.5deg', '100deg'],
+ });
+
+ expect(interpolation(0)).toBe('-100.5deg');
+ expect(interpolation(0.5)).toBe('-0.25deg');
+ expect(interpolation(1)).toBe('100deg');
+ });
+
+ it('should crash when chaining an interpolation that returns a string', () => {
+ var interpolation = Interpolation.create({
+ inputRange: [0, 1],
+ outputRange: [0, 1],
+ });
+ expect(() => { interpolation('45rad'); }).toThrow();
+ });
+
+ it('should crash when defining output range with different pattern', () => {
+ expect(() => Interpolation.create({
+ inputRange: [0, 1],
+ outputRange: ['rgba(0, 100, 200, 0)', 'rgb(50, 150, 250)'],
+ })).toThrow();
+
+ expect(() => Interpolation.create({
+ inputRange: [0, 1],
+ outputRange: ['20deg', '30rad'],
+ })).toThrow();
+ });
+});
diff --git a/Libraries/Animation/Animated/package.json b/Libraries/Animation/Animated/package.json
new file mode 100644
index 000000000..35cc7ccac
--- /dev/null
+++ b/Libraries/Animation/Animated/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "react-animated",
+ "description": "Animated provides powerful mechanisms for animating your React views",
+ "version": "0.1.0",
+ "keywords": [
+ "react",
+ "animated",
+ "animation"
+ ],
+ "license": "BSD-3-Clause",
+ "main": "Animated.js",
+ "readmeFilename": "README.md"
+}
diff --git a/Libraries/Animation/bezier.js b/Libraries/Animation/bezier.js
new file mode 100644
index 000000000..11b02f501
--- /dev/null
+++ b/Libraries/Animation/bezier.js
@@ -0,0 +1,80 @@
+/**
+ * https://github.com/arian/cubic-bezier
+ *
+ * MIT License
+ *
+ * Copyright (c) 2013 Arian Stolwijk
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * @providesModule bezier
+ * @nolint
+ */
+
+module.exports = function(x1, y1, x2, y2, epsilon){
+
+ var curveX = function(t){
+ var v = 1 - t;
+ return 3 * v * v * t * x1 + 3 * v * t * t * x2 + t * t * t;
+ };
+
+ var curveY = function(t){
+ var v = 1 - t;
+ return 3 * v * v * t * y1 + 3 * v * t * t * y2 + t * t * t;
+ };
+
+ var derivativeCurveX = function(t){
+ var v = 1 - t;
+ return 3 * (2 * (t - 1) * t + v * v) * x1 + 3 * (- t * t * t + 2 * v * t) * x2;
+ };
+
+ return function(t){
+
+ var x = t, t0, t1, t2, x2, d2, i;
+
+ // First try a few iterations of Newton's method -- normally very fast.
+ for (t2 = x, i = 0; i < 8; i++){
+ x2 = curveX(t2) - x;
+ if (Math.abs(x2) < epsilon) return curveY(t2);
+ d2 = derivativeCurveX(t2);
+ if (Math.abs(d2) < 1e-6) break;
+ t2 = t2 - x2 / d2;
+ }
+
+ t0 = 0, t1 = 1, t2 = x;
+
+ if (t2 < t0) return curveY(t0);
+ if (t2 > t1) return curveY(t1);
+
+ // Fallback to the bisection method for reliability.
+ while (t0 < t1){
+ x2 = curveX(t2);
+ if (Math.abs(x2 - x) < epsilon) return curveY(t2);
+ if (x > x2) t0 = t2;
+ else t1 = t2;
+ t2 = (t1 - t0) * .5 + t0;
+ }
+
+ // Failure
+ return curveY(t2);
+
+ };
+
+};
diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js
index 42cf30f51..c683ff95e 100644
--- a/Libraries/react-native/react-native.js
+++ b/Libraries/react-native/react-native.js
@@ -45,6 +45,7 @@ var ReactNative = Object.assign(Object.create(require('React')), {
ActionSheetIOS: require('ActionSheetIOS'),
AdSupportIOS: require('AdSupportIOS'),
AlertIOS: require('AlertIOS'),
+ Animated: require('Animated'),
AppRegistry: require('AppRegistry'),
AppStateIOS: require('AppStateIOS'),
AsyncStorage: require('AsyncStorage'),