From 5590b1b5ad058bcc377521464b025bc4808a22f4 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Tue, 22 Aug 2017 17:57:38 -0700 Subject: [PATCH] Split up AnimatedImplementation.js Summary: AnimatedImplementation.js is getting pretty hard to navigate and reason about so I split it up into different modules. Also took the opportunity to run prettier on that code and do some minor const/let refactorings. This doesn't change any logic, mostly just moves code around and add proper imports / exports. This opens the door for further cleanup and flow type improvements but want to keep this already big PR as small as possible. Discussion points: - Should I use haste for this? Animated is pretty much a standalone module, it still uses a few haste imports but for new modules I used commonjs imports to avoid polluting the haste global namespace too much. The new modules are all internal to Animated and should not be imported externally. - We're using `requestAnimationFrame` from fbjs instead of the one available globally in RN and browsers is there a reason for that? - Should we even support web in this implementation? There is a standalone repo that exist for Animated web. Is this implementation of Animated web used internally at facebook? - Probably still related to web, we used some weird Set polyfill is that still needed? Notes: There is a small regression for docs where the type of some classes that are exported like AnimatedValue show up as CallExpression instead if Class. screen shot 2017-08-14 at 3 19 18 am **Test plan** Tested that all Animated related examples still work in RNTester on iOS and Android. Tested that the doc is still working Ran unit tests Closes https://github.com/facebook/react-native/pull/15485 Differential Revision: D5679886 Pulled By: sahrens fbshipit-source-id: d3e9b6987ab5ff2cd20108c3b9d860c7536be8a0 --- Libraries/Animated/src/AnimatedEvent.js | 195 ++ .../Animated/src/AnimatedImplementation.js | 2461 ++--------------- .../Animated/src/NativeAnimatedHelper.js | 99 +- .../src/__tests__/Interpolation-test.js | 104 +- .../Animated/src/animations/Animation.js | 72 + .../Animated/src/animations/DecayAnimation.js | 111 + .../src/animations/SpringAnimation.js | 290 ++ .../src/animations/TimingAnimation.js | 154 ++ .../Animated/src/createAnimatedComponent.js | 192 ++ .../Animated/src/nodes/AnimatedAddition.js | 64 + .../Animated/src/nodes/AnimatedDiffClamp.js | 72 + .../Animated/src/nodes/AnimatedDivision.js | 69 + .../AnimatedInterpolation.js} | 244 +- .../Animated/src/nodes/AnimatedModulo.js | 63 + .../src/nodes/AnimatedMultiplication.js | 64 + Libraries/Animated/src/nodes/AnimatedNode.js | 72 + Libraries/Animated/src/nodes/AnimatedProps.js | 166 ++ Libraries/Animated/src/nodes/AnimatedStyle.js | 126 + .../Animated/src/nodes/AnimatedTracking.js | 66 + .../Animated/src/nodes/AnimatedTransform.js | 122 + Libraries/Animated/src/nodes/AnimatedValue.js | 308 +++ .../Animated/src/nodes/AnimatedValueXY.js | 179 ++ .../src/nodes/AnimatedWithChildren.js | 76 + 23 files changed, 2968 insertions(+), 2401 deletions(-) create mode 100644 Libraries/Animated/src/AnimatedEvent.js create mode 100644 Libraries/Animated/src/animations/Animation.js create mode 100644 Libraries/Animated/src/animations/DecayAnimation.js create mode 100644 Libraries/Animated/src/animations/SpringAnimation.js create mode 100644 Libraries/Animated/src/animations/TimingAnimation.js create mode 100644 Libraries/Animated/src/createAnimatedComponent.js create mode 100644 Libraries/Animated/src/nodes/AnimatedAddition.js create mode 100644 Libraries/Animated/src/nodes/AnimatedDiffClamp.js create mode 100644 Libraries/Animated/src/nodes/AnimatedDivision.js rename Libraries/Animated/src/{Interpolation.js => nodes/AnimatedInterpolation.js} (51%) create mode 100644 Libraries/Animated/src/nodes/AnimatedModulo.js create mode 100644 Libraries/Animated/src/nodes/AnimatedMultiplication.js create mode 100644 Libraries/Animated/src/nodes/AnimatedNode.js create mode 100644 Libraries/Animated/src/nodes/AnimatedProps.js create mode 100644 Libraries/Animated/src/nodes/AnimatedStyle.js create mode 100644 Libraries/Animated/src/nodes/AnimatedTracking.js create mode 100644 Libraries/Animated/src/nodes/AnimatedTransform.js create mode 100644 Libraries/Animated/src/nodes/AnimatedValue.js create mode 100644 Libraries/Animated/src/nodes/AnimatedValueXY.js create mode 100644 Libraries/Animated/src/nodes/AnimatedWithChildren.js diff --git a/Libraries/Animated/src/AnimatedEvent.js b/Libraries/Animated/src/AnimatedEvent.js new file mode 100644 index 00000000000000..a0b133926ac46e --- /dev/null +++ b/Libraries/Animated/src/AnimatedEvent.js @@ -0,0 +1,195 @@ +/** + * 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. + * + * @flow + * @format + */ +'use strict'; + +const AnimatedValue = require('./nodes/AnimatedValue'); +const NativeAnimatedHelper = require('./NativeAnimatedHelper'); +const ReactNative = require('ReactNative'); + +const invariant = require('fbjs/lib/invariant'); +const {shouldUseNativeDriver} = require('./NativeAnimatedHelper'); + +export type Mapping = {[key: string]: Mapping} | AnimatedValue; +export type EventConfig = { + listener?: ?Function, + useNativeDriver?: boolean, +}; + +function attachNativeEvent( + viewRef: any, + eventName: string, + argMapping: Array, +) { + // Find animated values in `argMapping` and create an array representing their + // key path inside the `nativeEvent` object. Ex.: ['contentOffset', 'x']. + const eventMappings = []; + + const traverse = (value, path) => { + if (value instanceof AnimatedValue) { + value.__makeNative(); + + eventMappings.push({ + nativeEventPath: path, + animatedValueTag: value.__getNativeTag(), + }); + } else if (typeof value === 'object') { + for (const key in value) { + traverse(value[key], path.concat(key)); + } + } + }; + + invariant( + argMapping[0] && argMapping[0].nativeEvent, + 'Native driven events only support animated values contained inside `nativeEvent`.', + ); + + // Assume that the event containing `nativeEvent` is always the first argument. + traverse(argMapping[0].nativeEvent, []); + + const viewTag = ReactNative.findNodeHandle(viewRef); + + eventMappings.forEach(mapping => { + NativeAnimatedHelper.API.addAnimatedEventToView( + viewTag, + eventName, + mapping, + ); + }); + + return { + detach() { + eventMappings.forEach(mapping => { + NativeAnimatedHelper.API.removeAnimatedEventFromView( + viewTag, + eventName, + mapping.animatedValueTag, + ); + }); + }, + }; +} + +class AnimatedEvent { + _argMapping: Array; + _listeners: Array = []; + _callListeners: Function; + _attachedEvent: ?{ + detach: () => void, + }; + __isNative: boolean; + + constructor(argMapping: Array, config?: EventConfig = {}) { + this._argMapping = argMapping; + if (config.listener) { + this.__addListener(config.listener); + } + this._callListeners = this._callListeners.bind(this); + this._attachedEvent = null; + this.__isNative = shouldUseNativeDriver(config); + + if (__DEV__) { + this._validateMapping(); + } + } + + __addListener(callback: Function): void { + this._listeners.push(callback); + } + + __removeListener(callback: Function): void { + this._listeners = this._listeners.filter(listener => listener !== callback); + } + + __attach(viewRef: any, eventName: string) { + invariant( + this.__isNative, + 'Only native driven events need to be attached.', + ); + + this._attachedEvent = attachNativeEvent( + viewRef, + eventName, + this._argMapping, + ); + } + + __detach(viewTag: any, eventName: string) { + invariant( + this.__isNative, + 'Only native driven events need to be detached.', + ); + + this._attachedEvent && this._attachedEvent.detach(); + } + + __getHandler() { + if (this.__isNative) { + return this._callListeners; + } + + return (...args: any) => { + const traverse = (recMapping, recEvt, key) => { + if (typeof recEvt === 'number' && recMapping instanceof AnimatedValue) { + recMapping.setValue(recEvt); + } else if (typeof recMapping === 'object') { + for (const mappingKey in recMapping) { + /* $FlowFixMe(>=0.53.0 site=react_native_fb) This comment + * suppresses an error found when Flow v0.53 was deployed. To see + * the error delete this comment and run Flow. */ + traverse(recMapping[mappingKey], recEvt[mappingKey], mappingKey); + } + } + }; + + if (!this.__isNative) { + this._argMapping.forEach((mapping, idx) => { + traverse(mapping, args[idx], 'arg' + idx); + }); + } + this._callListeners(...args); + }; + } + + _callListeners(...args) { + this._listeners.forEach(listener => listener(...args)); + } + + _validateMapping() { + const traverse = (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', + ); + 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 (const mappingKey in recMapping) { + traverse(recMapping[mappingKey], recEvt[mappingKey], mappingKey); + } + }; + } +} + +module.exports = {AnimatedEvent, attachNativeEvent}; diff --git a/Libraries/Animated/src/AnimatedImplementation.js b/Libraries/Animated/src/AnimatedImplementation.js index 2dcc01251d52dc..f54763a25ac239 100644 --- a/Libraries/Animated/src/AnimatedImplementation.js +++ b/Libraries/Animated/src/AnimatedImplementation.js @@ -8,1960 +8,38 @@ * * @providesModule AnimatedImplementation * @flow + * @format * @preventMunge */ 'use strict'; -var InteractionManager = require('InteractionManager'); -var Interpolation = require('Interpolation'); -var NativeAnimatedHelper = require('NativeAnimatedHelper'); -var React = require('React'); -var ReactNative = require('ReactNative'); -var Set = require('Set'); -var SpringConfig = require('SpringConfig'); -var ViewStylePropTypes = require('ViewStylePropTypes'); - -var flattenStyle = require('flattenStyle'); -var invariant = require('fbjs/lib/invariant'); -var requestAnimationFrame = require('fbjs/lib/requestAnimationFrame'); - -import type { InterpolationConfigType } from 'Interpolation'; - -type EndResult = {finished: bool}; -type EndCallback = (result: EndResult) => void; - -var NativeAnimatedAPI = NativeAnimatedHelper.API; - -var warnedMissingNativeAnimated = false; - -function shouldUseNativeDriver(config: AnimationConfig | EventConfig): boolean { - if (config.useNativeDriver && - !NativeAnimatedHelper.isNativeAnimatedAvailable()) { - if (!warnedMissingNativeAnimated) { - console.warn( - 'Animated: `useNativeDriver` is not supported because the native ' + - 'animated module is missing. Falling back to JS-based animation. To ' + - 'resolve this, add `RCTAnimation` module to this app, or remove ' + - '`useNativeDriver`. ' + - 'More info: https://github.com/facebook/react-native/issues/11094#issuecomment-263240420' - ); - warnedMissingNativeAnimated = true; - } - return false; - } - - return config.useNativeDriver || false; -} - -// Note(vjeux): this would be better as an interface but flow doesn't -// support them yet -class Animated { - __attach(): void {} - __detach(): void { - if (this.__isNative && this.__nativeTag != null) { - NativeAnimatedAPI.dropAnimatedNode(this.__nativeTag); - this.__nativeTag = undefined; - } - } - __getValue(): any {} - __getAnimatedValue(): any { return this.__getValue(); } - __addChild(child: Animated) {} - __removeChild(child: Animated) {} - __getChildren(): Array { return []; } - - /* Methods and props used by native Animated impl */ - __isNative: bool; - __nativeTag: ?number; - __makeNative() { - if (!this.__isNative) { - throw new Error('This node cannot be made a "native" animated node'); - } - } - __getNativeTag(): ?number { - NativeAnimatedHelper.assertNativeAnimatedModule(); - invariant(this.__isNative, 'Attempt to get native tag from node not marked as "native"'); - if (this.__nativeTag == null) { - var nativeTag: ?number = NativeAnimatedHelper.generateNewNodeTag(); - NativeAnimatedAPI.createAnimatedNode(nativeTag, this.__getNativeConfig()); - this.__nativeTag = nativeTag; - } - return this.__nativeTag; - } - __getNativeConfig(): Object { - throw new Error('This JS animated node type cannot be used as native animated node'); - } - toJSON(): any { return this.__getValue(); } -} - -type AnimationConfig = { - isInteraction?: bool, - useNativeDriver?: bool, - onComplete?: ?EndCallback, - iterations?: number, -}; - -// 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; - __isInteraction: bool; - __nativeId: number; - __onEnd: ?EndCallback; - __iterations: number; - start( - fromValue: number, - onUpdate: (value: number) => void, - onEnd: ?EndCallback, - previousAnimation: ?Animation, - animatedValue: AnimatedValue - ): void {} - stop(): void { - if (this.__nativeId) { - NativeAnimatedAPI.stopAnimation(this.__nativeId); - } - } - __getNativeAnimationConfig(): any { - // Subclasses that have corresponding animation implementation done in native - // should override this method - throw new Error('This animation type cannot be offloaded to native'); - } - // Helper function for subclasses to make sure onEnd is only called once. - __debouncedOnEnd(result: EndResult): void { - var onEnd = this.__onEnd; - this.__onEnd = null; - onEnd && onEnd(result); - } - __startNativeAnimation(animatedValue: AnimatedValue): void { - animatedValue.__makeNative(); - this.__nativeId = NativeAnimatedHelper.generateNewAnimationId(); - NativeAnimatedAPI.startAnimatingNode( - this.__nativeId, - animatedValue.__getNativeTag(), - this.__getNativeAnimationConfig(), - this.__debouncedOnEnd.bind(this) - ); - } -} - -class AnimatedWithChildren extends Animated { - _children: Array; - - constructor() { - super(); - this._children = []; - } - - __makeNative() { - if (!this.__isNative) { - this.__isNative = true; - for (var child of this._children) { - child.__makeNative(); - NativeAnimatedAPI.connectAnimatedNodes(this.__getNativeTag(), child.__getNativeTag()); - } - } - } - - __addChild(child: Animated): void { - if (this._children.length === 0) { - this.__attach(); - } - this._children.push(child); - if (this.__isNative) { - // Only accept "native" animated nodes as children - child.__makeNative(); - NativeAnimatedAPI.connectAnimatedNodes(this.__getNativeTag(), child.__getNativeTag()); - } - } - - __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; - } - if (this.__isNative && child.__isNative) { - NativeAnimatedAPI.disconnectAnimatedNodes(this.__getNativeTag(), child.__getNativeTag()); - } - 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); - /* $FlowFixMe */ - animatedStyles.forEach(animatedStyle => animatedStyle.update()); -} - -type TimingAnimationConfig = AnimationConfig & { - toValue: number | AnimatedValue | {x: number, y: number} | AnimatedValueXY, - easing?: (value: number) => number, - duration?: number, - delay?: number, -}; - -type TimingAnimationConfigSingle = AnimationConfig & { - toValue: number | AnimatedValue, - easing?: (value: number) => number, - duration?: number, - delay?: number, -}; - -let _easeInOut; -function easeInOut() { - if (!_easeInOut) { - const Easing = require('Easing'); - _easeInOut = Easing.inOut(Easing.ease); - } - return _easeInOut; -} - -class TimingAnimation extends Animation { - _startTime: number; - _fromValue: number; - _toValue: any; - _duration: number; - _delay: number; - _easing: (value: number) => number; - _onUpdate: (value: number) => void; - _animationFrame: any; - _timeout: any; - _useNativeDriver: bool; - - constructor( - config: TimingAnimationConfigSingle, - ) { - super(); - this._toValue = config.toValue; - this._easing = config.easing !== undefined ? config.easing : easeInOut(); - this._duration = config.duration !== undefined ? config.duration : 500; - this._delay = config.delay !== undefined ? config.delay : 0; - this.__iterations = config.iterations !== undefined ? config.iterations : 1; - this.__isInteraction = config.isInteraction !== undefined ? config.isInteraction : true; - this._useNativeDriver = shouldUseNativeDriver(config); - } - - __getNativeAnimationConfig(): any { - var frameDuration = 1000.0 / 60.0; - var frames = []; - for (var dt = 0.0; dt < this._duration; dt += frameDuration) { - frames.push(this._easing(dt / this._duration)); - } - frames.push(this._easing(1)); - return { - type: 'frames', - frames, - toValue: this._toValue, - iterations: this.__iterations, - }; - } - - start( - fromValue: number, - onUpdate: (value: number) => void, - onEnd: ?EndCallback, - previousAnimation: ?Animation, - animatedValue: AnimatedValue - ): void { - this.__active = true; - this._fromValue = fromValue; - this._onUpdate = onUpdate; - this.__onEnd = onEnd; - - var start = () => { - // Animations that sometimes have 0 duration and sometimes do not - // still need to use the native driver when duration is 0 so as to - // not cause intermixed JS and native animations. - if (this._duration === 0 && !this._useNativeDriver) { - this._onUpdate(this._toValue); - this.__debouncedOnEnd({finished: true}); - } else { - this._startTime = Date.now(); - if (this._useNativeDriver) { - this.__startNativeAnimation(animatedValue); - } else { - this._animationFrame = 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 = requestAnimationFrame(this.onUpdate.bind(this)); - } - } - - stop(): void { - super.stop(); - this.__active = false; - clearTimeout(this._timeout); - global.cancelAnimationFrame(this._animationFrame); - this.__debouncedOnEnd({finished: false}); - } -} - -type DecayAnimationConfig = AnimationConfig & { - velocity: number | {x: number, y: number}, - deceleration?: number, -}; - -type DecayAnimationConfigSingle = AnimationConfig & { - velocity: number, - deceleration?: number, -}; - -class DecayAnimation extends Animation { - _startTime: number; - _lastValue: number; - _fromValue: number; - _deceleration: number; - _velocity: number; - _onUpdate: (value: number) => void; - _animationFrame: any; - _useNativeDriver: bool; - - constructor( - config: DecayAnimationConfigSingle, - ) { - super(); - this._deceleration = config.deceleration !== undefined ? config.deceleration : 0.998; - this._velocity = config.velocity; - this._useNativeDriver = shouldUseNativeDriver(config); - this.__isInteraction = config.isInteraction !== undefined ? config.isInteraction : true; - this.__iterations = config.iterations !== undefined ? config.iterations : 1; - } - - __getNativeAnimationConfig() { - return { - type: 'decay', - deceleration: this._deceleration, - velocity: this._velocity, - iterations: this.__iterations, - }; - } - - start( - fromValue: number, - onUpdate: (value: number) => void, - onEnd: ?EndCallback, - previousAnimation: ?Animation, - animatedValue: AnimatedValue, - ): void { - this.__active = true; - this._lastValue = fromValue; - this._fromValue = fromValue; - this._onUpdate = onUpdate; - this.__onEnd = onEnd; - this._startTime = Date.now(); - if (this._useNativeDriver) { - this.__startNativeAnimation(animatedValue); - } else { - this._animationFrame = 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 = requestAnimationFrame(this.onUpdate.bind(this)); - } - } - - stop(): void { - super.stop(); - this.__active = false; - global.cancelAnimationFrame(this._animationFrame); - this.__debouncedOnEnd({finished: false}); - } -} - -type SpringAnimationConfig = AnimationConfig & { - 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, - delay?: number, -}; - -type SpringAnimationConfigSingle = AnimationConfig & { - toValue: number | AnimatedValue, - overshootClamping?: bool, - restDisplacementThreshold?: number, - restSpeedThreshold?: number, - velocity?: number, - bounciness?: number, - speed?: number, - tension?: number, - friction?: number, - delay?: 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; - _delay: number; - _timeout: any; - _lastTime: number; - _onUpdate: (value: number) => void; - _animationFrame: any; - _useNativeDriver: bool; - - 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; - this._delay = withDefault(config.delay, 0); - this._useNativeDriver = shouldUseNativeDriver(config); - this.__isInteraction = config.isInteraction !== undefined ? config.isInteraction : true; - this.__iterations = config.iterations !== undefined ? config.iterations : 1; - - 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 = SpringConfig.fromBouncinessAndSpeed( - withDefault(config.bounciness, 8), - withDefault(config.speed, 12), - ); - } else { - springConfig = SpringConfig.fromOrigamiTensionAndFriction( - withDefault(config.tension, 40), - withDefault(config.friction, 7), - ); - } - this._tension = springConfig.tension; - this._friction = springConfig.friction; - } - - __getNativeAnimationConfig() { - return { - type: 'spring', - overshootClamping: this._overshootClamping, - restDisplacementThreshold: this._restDisplacementThreshold, - restSpeedThreshold: this._restSpeedThreshold, - tension: this._tension, - friction: this._friction, - initialVelocity: withDefault(this._initialVelocity, this._lastVelocity), - toValue: this._toValue, - iterations: this.__iterations, - }; - } - - start( - fromValue: number, - onUpdate: (value: number) => void, - onEnd: ?EndCallback, - previousAnimation: ?Animation, - animatedValue: AnimatedValue - ): 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; - } - - var start = () => { - if (this._useNativeDriver) { - this.__startNativeAnimation(animatedValue); - } else { - this.onUpdate(); - } - }; - - // If this._delay is more than 0, we start after the timeout. - if (this._delay) { - this._timeout = setTimeout(start, this._delay); - } else { - start(); - } - } - - 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)) { - if (this._tension !== 0) { - // Ensure that we end up with a round value - this._onUpdate(this._toValue); - } - - this.__debouncedOnEnd({finished: true}); - return; - } - this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this)); - } - - stop(): void { - super.stop(); - this.__active = false; - clearTimeout(this._timeout); - global.cancelAnimationFrame(this._animationFrame); - this.__debouncedOnEnd({finished: false}); - } -} - -type ValueListenerCallback = (state: {value: number}) => void; - -var _uniqueId = 1; - -/** - * Standard value for driving animations. One `Animated.Value` can drive - * multiple properties in a synchronized fashion, but can only be driven by one - * mechanism at a time. Using a new mechanism (e.g. starting a new animation, - * or calling `setValue`) will stop any previous ones. - */ -class AnimatedValue extends AnimatedWithChildren { - _value: number; - _startingValue: number; - _offset: number; - _animation: ?Animation; - _tracking: ?Animated; - _listeners: {[key: string]: ValueListenerCallback}; - __nativeAnimatedValueListener: ?any; - - constructor(value: number) { - super(); - this._startingValue = this._value = value; - this._offset = 0; - this._animation = null; - this._listeners = {}; - } - - __detach() { - this.stopAnimation(); - super.__detach(); - } - - __getValue(): number { - return this._value + this._offset; - } - - __makeNative() { - super.__makeNative(); - - if (Object.keys(this._listeners).length) { - this._startListeningToNativeValueUpdates(); - } - } - - /** - * Directly set the value. This will stop any animations running on the value - * and update all the bound properties. - */ - setValue(value: number): void { - if (this._animation) { - this._animation.stop(); - this._animation = null; - } - this._updateValue( - value, - !this.__isNative /* don't perform a flush for natively driven values */); - if (this.__isNative) { - NativeAnimatedAPI.setAnimatedNodeValue(this.__getNativeTag(), value); - } - } - - /** - * Sets an offset that is applied on top of whatever value is set, whether via - * `setValue`, an animation, or `Animated.event`. Useful for compensating - * things like the start of a pan gesture. - */ - setOffset(offset: number): void { - this._offset = offset; - if (this.__isNative) { - NativeAnimatedAPI.setAnimatedNodeOffset(this.__getNativeTag(), offset); - } - } - - /** - * Merges the offset value into the base value and resets the offset to zero. - * The final output of the value is unchanged. - */ - flattenOffset(): void { - this._value += this._offset; - this._offset = 0; - if (this.__isNative) { - NativeAnimatedAPI.flattenAnimatedNodeOffset(this.__getNativeTag()); - } - } - - /** - * Sets the offset value to the base value, and resets the base value to zero. - * The final output of the value is unchanged. - */ - extractOffset(): void { - this._offset += this._value; - this._value = 0; - if (this.__isNative) { - NativeAnimatedAPI.extractAnimatedNodeOffset(this.__getNativeTag()); - } - } - - /** - * Adds an asynchronous listener to the value so you can observe updates from - * animations. This is useful because there is no way to - * synchronously read the value because it might be driven natively. - */ - addListener(callback: ValueListenerCallback): string { - var id = String(_uniqueId++); - this._listeners[id] = callback; - if (this.__isNative) { - this._startListeningToNativeValueUpdates(); - } - return id; - } - - removeListener(id: string): void { - delete this._listeners[id]; - if (this.__isNative && Object.keys(this._listeners).length === 0) { - this._stopListeningForNativeValueUpdates(); - } - } - - removeAllListeners(): void { - this._listeners = {}; - if (this.__isNative) { - this._stopListeningForNativeValueUpdates(); - } - } - - _startListeningToNativeValueUpdates() { - if (this.__nativeAnimatedValueListener) { - return; - } - - NativeAnimatedAPI.startListeningToAnimatedNodeValue(this.__getNativeTag()); - this.__nativeAnimatedValueListener = NativeAnimatedHelper.nativeEventEmitter.addListener( - 'onAnimatedValueUpdate', - (data) => { - if (data.tag !== this.__getNativeTag()) { - return; - } - this._updateValue(data.value, false /* flush */); - } - ); - } - - _stopListeningForNativeValueUpdates() { - if (!this.__nativeAnimatedValueListener) { - return; - } - - this.__nativeAnimatedValueListener.remove(); - this.__nativeAnimatedValueListener = null; - NativeAnimatedAPI.stopListeningToAnimatedNodeValue(this.__getNativeTag()); - } - - /** - * Stops any running animation or tracking. `callback` is invoked with the - * final value after stopping the animation, which is useful for updating - * state to match the animation position with layout. - */ - stopAnimation(callback?: ?(value: number) => void): void { - this.stopTracking(); - this._animation && this._animation.stop(); - this._animation = null; - callback && callback(this.__getValue()); - } - - /** - * Stops any animation and resets the value to its original - */ - resetAnimation(callback?: ?(value: number) => void): void { - this.stopAnimation(callback); - this._value = this._startingValue; - } - - /** - * Interpolates the value before updating the property, e.g. mapping 0-1 to - * 0-10. - */ - interpolate(config: InterpolationConfigType): AnimatedInterpolation { - return new AnimatedInterpolation(this, config); - } - - /** - * Typically only used internally, but could be used by a custom Animation - * class. - */ - animate(animation: Animation, callback: ?EndCallback): void { - var handle = null; - if (animation.__isInteraction) { - handle = InteractionManager.createInteractionHandle(); - } - var previousAnimation = this._animation; - this._animation && this._animation.stop(); - this._animation = animation; - animation.start( - this._value, - (value) => { - // Natively driven animations will never call into that callback, therefore we can always - // pass flush = true to allow the updated value to propagate to native with setNativeProps - this._updateValue(value, true /* flush */); - }, - (result) => { - this._animation = null; - if (handle !== null) { - InteractionManager.clearInteractionHandle(handle); - } - callback && callback(result); - }, - previousAnimation, - this - ); - } - - /** - * Typically only used internally. - */ - stopTracking(): void { - this._tracking && this._tracking.__detach(); - this._tracking = null; - } - - /** - * Typically only used internally. - */ - track(tracking: Animated): void { - this.stopTracking(); - this._tracking = tracking; - } - - _updateValue(value: number, flush: bool): void { - this._value = value; - if (flush) { - _flush(this); - } - for (var key in this._listeners) { - this._listeners[key]({value: this.__getValue()}); - } - } - - __getNativeConfig(): Object { - return { - type: 'value', - value: this._value, - offset: this._offset, - }; - } -} - -type ValueXYListenerCallback = (value: {x: number, y: number}) => void; - -/** - * 2D Value for driving 2D animations, such as pan gestures. Almost identical - * API to normal `Animated.Value`, but multiplexed. Contains two regular - * `Animated.Value`s under the hood. - * - * #### Example - * - *```javascript - * class DraggableView extends React.Component { - * constructor(props) { - * super(props); - * this.state = { - * pan: new Animated.ValueXY(), // inits to zero - * }; - * this.state.panResponder = PanResponder.create({ - * onStartShouldSetPanResponder: () => true, - * onPanResponderMove: Animated.event([null, { - * dx: this.state.pan.x, // x,y are Animated.Value - * dy: this.state.pan.y, - * }]), - * onPanResponderRelease: () => { - * Animated.spring( - * this.state.pan, // Auto-multiplexed - * {toValue: {x: 0, y: 0}} // Back to zero - * ).start(); - * }, - * }); - * } - * render() { - * return ( - * - * {this.props.children} - * - * ); - * } - * } - *``` - */ -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(); - } - - extractOffset(): void { - this.x.extractOffset(); - this.y.extractOffset(); - } - - __getValue(): {x: number, y: number} { - return { - x: this.x.__getValue(), - y: this.y.__getValue(), - }; - } - - resetAnimation(callback?: (value: {x: number, y: number}) => void): void { - this.x.resetAnimation(); - this.y.resetAnimation(); - callback && callback(this.__getValue()); - } - - stopAnimation(callback?: (value: {x: number, y: number}) => void): 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]; - } - - removeAllListeners(): void { - this.x.removeAllListeners(); - this.y.removeAllListeners(); - this._listeners = {}; - } - - /** - * Converts `{x, y}` into `{left, top}` for use in style, e.g. - * - *```javascript - * style={this.state.anim.getLayout()} - *``` - */ - getLayout(): {[key: string]: AnimatedValue} { - return { - left: this.x, - top: this.y, - }; - } - - /** - * Converts `{x, y}` into a useable translation transform, e.g. - * - *```javascript - * style={{ - * transform: this.state.anim.getTranslateTransform() - * }} - *``` - */ - getTranslateTransform(): Array<{[key: string]: AnimatedValue}> { - return [ - {translateX: this.x}, - {translateY: this.y} - ]; - } -} - -class AnimatedInterpolation extends AnimatedWithChildren { - _parent: Animated; - _config: InterpolationConfigType; - _interpolation: (input: number) => number | string; - - constructor(parent: Animated, config: InterpolationConfigType) { - super(); - this._parent = parent; - this._config = config; - this._interpolation = Interpolation.create(config); - } - - __makeNative() { - this._parent.__makeNative(); - super.__makeNative(); - } - - __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, config); - } - - __attach(): void { - this._parent.__addChild(this); - } - - __detach(): void { - this._parent.__removeChild(this); - super.__detach(); - } - - __transformDataType(range: Array) { - // Change the string array type to number array - // So we can reuse the same logic in iOS and Android platform - return range.map(function (value) { - if (typeof value !== 'string') { - return value; - } - if (/deg$/.test(value)) { - const degrees = parseFloat(value) || 0; - const radians = degrees * Math.PI / 180.0; - return radians; - } else { - // Assume radians - return parseFloat(value) || 0; - } - }); - } - - __getNativeConfig(): any { - if (__DEV__) { - NativeAnimatedHelper.validateInterpolation(this._config); - } - - return { - inputRange: this._config.inputRange, - // Only the `outputRange` can contain strings so we don't need to tranform `inputRange` here - outputRange: this.__transformDataType(this._config.outputRange), - extrapolateLeft: this._config.extrapolateLeft || this._config.extrapolate || 'extend', - extrapolateRight: this._config.extrapolateRight || this._config.extrapolate || 'extend', - type: 'interpolation', - }; - } -} - -class AnimatedAddition extends AnimatedWithChildren { - _a: Animated; - _b: Animated; - - constructor(a: Animated | number, b: Animated | number) { - super(); - this._a = typeof a === 'number' ? new AnimatedValue(a) : a; - this._b = typeof b === 'number' ? new AnimatedValue(b) : b; - } - - __makeNative() { - this._a.__makeNative(); - this._b.__makeNative(); - super.__makeNative(); - } - - __getValue(): number { - return this._a.__getValue() + this._b.__getValue(); - } - - interpolate(config: InterpolationConfigType): AnimatedInterpolation { - return new AnimatedInterpolation(this, config); - } - - __attach(): void { - this._a.__addChild(this); - this._b.__addChild(this); - } - - __detach(): void { - this._a.__removeChild(this); - this._b.__removeChild(this); - super.__detach(); - } - - __getNativeConfig(): any { - return { - type: 'addition', - input: [this._a.__getNativeTag(), this._b.__getNativeTag()], - }; - } -} - -class AnimatedDivision extends AnimatedWithChildren { - _a: Animated; - _b: Animated; - - constructor(a: Animated | number, b: Animated | number) { - super(); - this._a = typeof a === 'number' ? new AnimatedValue(a) : a; - this._b = typeof b === 'number' ? new AnimatedValue(b) : b; - } - - __makeNative() { - this._a.__makeNative(); - this._b.__makeNative(); - super.__makeNative(); - } - - __getValue(): number { - const a = this._a.__getValue(); - const b = this._b.__getValue(); - if (b === 0) { - console.error('Detected division by zero in AnimatedDivision'); - } - return a / b; - } - - interpolate(config: InterpolationConfigType): AnimatedInterpolation { - return new AnimatedInterpolation(this, config); - } - - __attach(): void { - this._a.__addChild(this); - this._b.__addChild(this); - } - - __detach(): void { - this._a.__removeChild(this); - this._b.__removeChild(this); - super.__detach(); - } - - __getNativeConfig(): any { - return { - type: 'division', - input: [this._a.__getNativeTag(), this._b.__getNativeTag()], - }; - } -} - -class AnimatedMultiplication extends AnimatedWithChildren { - _a: Animated; - _b: Animated; - - constructor(a: Animated | number, b: Animated | number) { - super(); - this._a = typeof a === 'number' ? new AnimatedValue(a) : a; - this._b = typeof b === 'number' ? new AnimatedValue(b) : b; - } - - __makeNative() { - this._a.__makeNative(); - this._b.__makeNative(); - super.__makeNative(); - } - - __getValue(): number { - return this._a.__getValue() * this._b.__getValue(); - } - - interpolate(config: InterpolationConfigType): AnimatedInterpolation { - return new AnimatedInterpolation(this, config); - } - - __attach(): void { - this._a.__addChild(this); - this._b.__addChild(this); - } - - __detach(): void { - this._a.__removeChild(this); - this._b.__removeChild(this); - super.__detach(); - } - - __getNativeConfig(): any { - return { - type: 'multiplication', - input: [this._a.__getNativeTag(), this._b.__getNativeTag()], - }; - } -} - -class AnimatedModulo extends AnimatedWithChildren { - _a: Animated; - _modulus: number; - - constructor(a: Animated, modulus: number) { - super(); - this._a = a; - this._modulus = modulus; - } - - __makeNative() { - this._a.__makeNative(); - super.__makeNative(); - } - - __getValue(): number { - return (this._a.__getValue() % this._modulus + this._modulus) % this._modulus; - } - - interpolate(config: InterpolationConfigType): AnimatedInterpolation { - return new AnimatedInterpolation(this, config); - } - - __attach(): void { - this._a.__addChild(this); - } - - __detach(): void { - this._a.__removeChild(this); - super.__detach(); - } - - __getNativeConfig(): any { - return { - type: 'modulus', - input: this._a.__getNativeTag(), - modulus: this._modulus, - }; - } -} - -class AnimatedDiffClamp extends AnimatedWithChildren { - _a: Animated; - _min: number; - _max: number; - _value: number; - _lastValue: number; - - constructor(a: Animated, min: number, max: number) { - super(); - - this._a = a; - this._min = min; - this._max = max; - this._value = this._lastValue = this._a.__getValue(); - } - - __makeNative() { - this._a.__makeNative(); - super.__makeNative(); - } - - interpolate(config: InterpolationConfigType): AnimatedInterpolation { - return new AnimatedInterpolation(this, config); - } - - __getValue(): number { - const value = this._a.__getValue(); - const diff = value - this._lastValue; - this._lastValue = value; - this._value = Math.min(Math.max(this._value + diff, this._min), this._max); - return this._value; - } - - __attach(): void { - this._a.__addChild(this); - } - - __detach(): void { - this._a.__removeChild(this); - super.__detach(); - } - - __getNativeConfig(): any { - return { - type: 'diffclamp', - input: this._a.__getNativeTag(), - min: this._min, - max: this._max, - }; - } -} - -class AnimatedTransform extends AnimatedWithChildren { - _transforms: Array; - - constructor(transforms: Array) { - super(); - this._transforms = transforms; - } - - __makeNative() { - super.__makeNative(); - this._transforms.forEach(transform => { - for (var key in transform) { - var value = transform[key]; - if (value instanceof Animated) { - value.__makeNative(); - } - } - }); - } - - __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); - } - } - }); - super.__detach(); - } - - __getNativeConfig(): any { - var transConfigs = []; - - this._transforms.forEach(transform => { - for (var key in transform) { - var value = transform[key]; - if (value instanceof Animated) { - transConfigs.push({ - type: 'animated', - property: key, - nodeTag: value.__getNativeTag(), - }); - } else { - transConfigs.push({ - type: 'static', - property: key, - value, - }); - } - } - }); - - NativeAnimatedHelper.validateTransform(transConfigs); - return { - type: 'transform', - transforms: transConfigs, - }; - } -} - -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; - } - - // Recursively get values for nested styles (like iOS's shadowOffset) - __walkStyleAndGetValues(style) { - const updatedStyle = {}; - for (const key in style) { - const value = style[key]; - if (value instanceof Animated) { - if (!value.__isNative) { - // We cannot use value of natively driven nodes this way as the value we have access from - // JS may not be up to date. - updatedStyle[key] = value.__getValue(); - } - } else if (value && !Array.isArray(value) && typeof value === 'object') { - // Support animating nested values (for example: shadowOffset.height) - updatedStyle[key] = this.__walkStyleAndGetValues(value); - } else { - updatedStyle[key] = value; - } - } - return updatedStyle; - } - - __getValue(): Object { - return this.__walkStyleAndGetValues(this._style); - } - - // Recursively get animated values for nested styles (like iOS's shadowOffset) - __walkStyleAndGetAnimatedValues(style) { - const updatedStyle = {}; - for (const key in style) { - const value = style[key]; - if (value instanceof Animated) { - updatedStyle[key] = value.__getAnimatedValue(); - } else if (value && !Array.isArray(value) && typeof value === 'object') { - // Support animating nested values (for example: shadowOffset.height) - updatedStyle[key] = this.__walkStyleAndGetAnimatedValues(value); - } - } - return updatedStyle; - } - - __getAnimatedValue(): Object { - return this.__walkStyleAndGetAnimatedValues(this._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); - } - } - super.__detach(); - } - - __makeNative() { - super.__makeNative(); - for (var key in this._style) { - var value = this._style[key]; - if (value instanceof Animated) { - value.__makeNative(); - } - } - } - - __getNativeConfig(): Object { - var styleConfig = {}; - for (const styleKey in this._style) { - if (this._style[styleKey] instanceof Animated) { - styleConfig[styleKey] = this._style[styleKey].__getNativeTag(); - } - // Non-animated styles are set using `setNativeProps`, no need - // to pass those as a part of the node config - } - NativeAnimatedHelper.validateStyles(styleConfig); - return { - type: 'style', - style: styleConfig, - }; - } -} - -class AnimatedProps extends Animated { - _props: Object; - _animatedView: any; - _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) { - if (!value.__isNative || value instanceof AnimatedStyle) { - // We cannot use value of natively driven nodes this way as the value we have access from - // JS may not be up to date. - props[key] = value.__getValue(); - } - } else if (value instanceof AnimatedEvent) { - props[key] = value.__getHandler(); - } 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 { - if (this.__isNative && this._animatedView) { - this.__disconnectAnimatedView(); - } - for (var key in this._props) { - var value = this._props[key]; - if (value instanceof Animated) { - value.__removeChild(this); - } - } - super.__detach(); - } - - update(): void { - this._callback(); - } - - __makeNative(): void { - if (!this.__isNative) { - this.__isNative = true; - for (var key in this._props) { - var value = this._props[key]; - if (value instanceof Animated) { - value.__makeNative(); - } - } - if (this._animatedView) { - this.__connectAnimatedView(); - } - } - } - - setNativeView(animatedView: any): void { - if (this._animatedView === animatedView) { - return; - } - this._animatedView = animatedView; - if (this.__isNative) { - this.__connectAnimatedView(); - } - } - - __connectAnimatedView(): void { - invariant(this.__isNative, 'Expected node to be marked as "native"'); - var nativeViewTag: ?number = ReactNative.findNodeHandle(this._animatedView); - invariant(nativeViewTag != null, 'Unable to locate attached view in the native tree'); - NativeAnimatedAPI.connectAnimatedNodeToView(this.__getNativeTag(), nativeViewTag); - } - - __disconnectAnimatedView(): void { - invariant(this.__isNative, 'Expected node to be marked as "native"'); - var nativeViewTag: ?number = ReactNative.findNodeHandle(this._animatedView); - invariant(nativeViewTag != null, 'Unable to locate attached view in the native tree'); - NativeAnimatedAPI.disconnectAnimatedNodeFromView(this.__getNativeTag(), nativeViewTag); - } - - __getNativeConfig(): Object { - var propsConfig = {}; - for (const propKey in this._props) { - var value = this._props[propKey]; - if (value instanceof Animated) { - propsConfig[propKey] = value.__getNativeTag(); - } - } - return { - type: 'props', - props: propsConfig, - }; - } -} - -function createAnimatedComponent(Component: any): any { - class AnimatedComponent extends React.Component { - _component: any; - _prevComponent: any; - _propsAnimated: AnimatedProps; - _eventDetachers: Array = []; - _setComponentRef: Function; - - static __skipSetNativeProps_FOR_TESTS_ONLY = false; - - constructor(props: Object) { - super(props); - this._setComponentRef = this._setComponentRef.bind(this); - } - - componentWillUnmount() { - this._propsAnimated && this._propsAnimated.__detach(); - this._detachNativeEvents(); - } - - setNativeProps(props) { - this._component.setNativeProps(props); - } - - componentWillMount() { - this._attachProps(this.props); - } - - componentDidMount() { - this._propsAnimated.setNativeView(this._component); - this._attachNativeEvents(); - } - - _attachNativeEvents() { - // Make sure to get the scrollable node for components that implement - // `ScrollResponder.Mixin`. - const scrollableNode = this._component.getScrollableNode ? - this._component.getScrollableNode() : - this._component; - - for (const key in this.props) { - const prop = this.props[key]; - if (prop instanceof AnimatedEvent && prop.__isNative) { - prop.__attach(scrollableNode, key); - this._eventDetachers.push(() => prop.__detach(scrollableNode, key)); - } - } - } - - _detachNativeEvents() { - this._eventDetachers.forEach(remove => remove()); - this._eventDetachers = []; - } - - _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 (!AnimatedComponent.__skipSetNativeProps_FOR_TESTS_ONLY && - this._component.setNativeProps) { - if (!this._propsAnimated.__isNative) { - this._component.setNativeProps( - this._propsAnimated.__getAnimatedValue() - ); - } else { - throw new Error('Attempting to run JS driven animation on animated ' - + 'node that has been moved to "native" earlier by starting an ' - + 'animation with `useNativeDriver: true`'); - } - } 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(newProps) { - this._attachProps(newProps); - } - - componentDidUpdate(prevProps) { - if (this._component !== this._prevComponent) { - this._propsAnimated.setNativeView(this._component); - } - if (this._component !== this._prevComponent || prevProps !== this.props) { - this._detachNativeEvents(); - this._attachNativeEvents(); - } - } - - render() { - const props = this._propsAnimated.__getValue(); - return ( - - ); - } - - _setComponentRef(c) { - this._prevComponent = this._component; - this._component = c; - } - - // A third party library can use getNode() - // to get the node reference of the decorated component - getNode () { - return this._component; - } - } - - // ReactNative `View.propTypes` have been deprecated in favor of - // `ViewPropTypes`. In their place a temporary getter has been added with a - // deprecated warning message. Avoid triggering that warning here by using - // temporary workaround, __propTypesSecretDontUseThesePlease. - // TODO (bvaughn) Revert this particular change any time after April 1 - var propTypes = - Component.__propTypesSecretDontUseThesePlease || - Component.propTypes; - - AnimatedComponent.propTypes = { - style: function(props, propName, componentName) { - if (!propTypes) { - return; - } - - for (var key in ViewStylePropTypes) { - if (!propTypes[key] && props[key] !== undefined) { - console.warn( - 'You are setting the style `{ ' + key + ': ... }` as a prop. You ' + - 'should nest it in a style object. ' + - 'E.g. `{ style: { ' + key + ': ... } }`' - ); - } - } - }, - }; - - 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); - super.__detach(); - } - - update(): void { - this._value.animate(new this._animationClass({ - ...this._animationConfig, - toValue: (this._animationConfig.toValue: any).__getValue(), - }), this._callback); - } -} +const {AnimatedEvent, attachNativeEvent} = require('./AnimatedEvent'); +const AnimatedAddition = require('./nodes/AnimatedAddition'); +const AnimatedDiffClamp = require('./nodes/AnimatedDiffClamp'); +const AnimatedDivision = require('./nodes/AnimatedDivision'); +const AnimatedInterpolation = require('./nodes/AnimatedInterpolation'); +const AnimatedModulo = require('./nodes/AnimatedModulo'); +const AnimatedMultiplication = require('./nodes/AnimatedMultiplication'); +const AnimatedNode = require('./nodes/AnimatedNode'); +const AnimatedProps = require('./nodes/AnimatedProps'); +const AnimatedTracking = require('./nodes/AnimatedTracking'); +const AnimatedValue = require('./nodes/AnimatedValue'); +const AnimatedValueXY = require('./nodes/AnimatedValueXY'); +const DecayAnimation = require('./animations/DecayAnimation'); +const SpringAnimation = require('./animations/SpringAnimation'); +const TimingAnimation = require('./animations/TimingAnimation'); + +const createAnimatedComponent = require('./createAnimatedComponent'); + +import type { + AnimationConfig, + EndCallback, + EndResult, +} from './animations/Animation'; +import type {TimingAnimationConfig} from './animations/TimingAnimation'; +import type {DecayAnimationConfig} from './animations/DecayAnimation'; +import type {SpringAnimationConfig} from './animations/SpringAnimation'; +import type {Mapping, EventConfig} from './AnimatedEvent'; type CompositeAnimation = { start: (callback?: ?EndCallback) => void, @@ -1971,43 +49,43 @@ type CompositeAnimation = { _isUsingNativeDriver: () => boolean, }; -var add = function( - a: Animated | number, - b: Animated | number, +const add = function( + a: AnimatedNode | number, + b: AnimatedNode | number, ): AnimatedAddition { return new AnimatedAddition(a, b); }; -var divide = function( - a: Animated | number, - b: Animated | number, +const divide = function( + a: AnimatedNode | number, + b: AnimatedNode | number, ): AnimatedDivision { return new AnimatedDivision(a, b); }; -var multiply = function( - a: Animated | number, - b: Animated | number, +const multiply = function( + a: AnimatedNode | number, + b: AnimatedNode | number, ): AnimatedMultiplication { return new AnimatedMultiplication(a, b); }; -var modulo = function( - a: Animated, - modulus: number -): AnimatedModulo { +const modulo = function(a: AnimatedNode, modulus: number): AnimatedModulo { return new AnimatedModulo(a, modulus); }; -var diffClamp = function( - a: Animated, +const diffClamp = function( + a: AnimatedNode, min: number, max: number, ): AnimatedDiffClamp { return new AnimatedDiffClamp(a, min, max); }; -const _combineCallbacks = function(callback: ?EndCallback, config : AnimationConfig) { +const _combineCallbacks = function( + callback: ?EndCallback, + config: AnimationConfig, +) { if (callback && config.onComplete) { return (...args) => { config.onComplete && config.onComplete(...args); @@ -2018,23 +96,23 @@ const _combineCallbacks = function(callback: ?EndCallback, config : AnimationCon } }; -var maybeVectorAnim = function( +const maybeVectorAnim = function( value: AnimatedValue | AnimatedValueXY, config: Object, - anim: (value: AnimatedValue, config: Object) => CompositeAnimation + 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]; + const configX = {...config}; + const configY = {...config}; + for (const key in config) { + const {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); + const aX = anim((value: AnimatedValueXY).x, configX); + const 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}); @@ -2042,149 +120,162 @@ var maybeVectorAnim = function( return null; }; -var spring = function( +const spring = function( value: AnimatedValue | AnimatedValueXY, config: SpringAnimationConfig, ): CompositeAnimation { - var start = function( + const start = function( animatedValue: AnimatedValue | AnimatedValueXY, configuration: SpringAnimationConfig, - callback?: ?EndCallback): void { - callback = _combineCallbacks(callback, configuration); - var singleValue: any = animatedValue; - var singleConfig: any = configuration; - singleValue.stopTracking(); - if (configuration.toValue instanceof Animated) { - singleValue.track(new AnimatedTracking( + callback?: ?EndCallback, + ): void { + callback = _combineCallbacks(callback, configuration); + const singleValue: any = animatedValue; + const singleConfig: any = configuration; + singleValue.stopTracking(); + if (configuration.toValue instanceof AnimatedNode) { + singleValue.track( + new AnimatedTracking( singleValue, configuration.toValue, SpringAnimation, singleConfig, - callback - )); - } else { - singleValue.animate(new SpringAnimation(singleConfig), callback); - } + callback, + ), + ); + } else { + singleValue.animate(new SpringAnimation(singleConfig), callback); + } }; - return maybeVectorAnim(value, config, spring) || { - start: function(callback?: ?EndCallback): void { - start(value, config, callback); - }, + return ( + maybeVectorAnim(value, config, spring) || { + start: function(callback?: ?EndCallback): void { + start(value, config, callback); + }, - stop: function(): void { - value.stopAnimation(); - }, + stop: function(): void { + value.stopAnimation(); + }, - reset: function(): void { - value.resetAnimation(); - }, + reset: function(): void { + value.resetAnimation(); + }, - _startNativeLoop: function(iterations?: number): void { - var singleConfig = { ...config, iterations }; - start(value, singleConfig); - }, + _startNativeLoop: function(iterations?: number): void { + const singleConfig = {...config, iterations}; + start(value, singleConfig); + }, - _isUsingNativeDriver: function(): boolean { - return config.useNativeDriver || false; + _isUsingNativeDriver: function(): boolean { + return config.useNativeDriver || false; + }, } - }; + ); }; -var timing = function( +const timing = function( value: AnimatedValue | AnimatedValueXY, config: TimingAnimationConfig, ): CompositeAnimation { - var start = function( + const start = function( animatedValue: AnimatedValue | AnimatedValueXY, configuration: TimingAnimationConfig, - callback?: ?EndCallback): void { - callback = _combineCallbacks(callback, configuration); - var singleValue: any = animatedValue; - var singleConfig: any = configuration; - singleValue.stopTracking(); - if (configuration.toValue instanceof Animated) { - singleValue.track(new AnimatedTracking( + callback?: ?EndCallback, + ): void { + callback = _combineCallbacks(callback, configuration); + const singleValue: any = animatedValue; + const singleConfig: any = configuration; + singleValue.stopTracking(); + if (configuration.toValue instanceof AnimatedNode) { + singleValue.track( + new AnimatedTracking( singleValue, configuration.toValue, TimingAnimation, singleConfig, - callback - )); - } else { - singleValue.animate(new TimingAnimation(singleConfig), callback); - } + callback, + ), + ); + } else { + singleValue.animate(new TimingAnimation(singleConfig), callback); + } }; - return maybeVectorAnim(value, config, timing) || { - start: function(callback?: ?EndCallback): void { - start(value, config, callback); - }, + return ( + maybeVectorAnim(value, config, timing) || { + start: function(callback?: ?EndCallback): void { + start(value, config, callback); + }, - stop: function(): void { - value.stopAnimation(); - }, + stop: function(): void { + value.stopAnimation(); + }, - reset: function(): void { - value.resetAnimation(); - }, + reset: function(): void { + value.resetAnimation(); + }, - _startNativeLoop: function(iterations?: number): void { - var singleConfig = { ...config, iterations }; - start(value, singleConfig); - }, + _startNativeLoop: function(iterations?: number): void { + const singleConfig = {...config, iterations}; + start(value, singleConfig); + }, - _isUsingNativeDriver: function(): boolean { - return config.useNativeDriver || false; + _isUsingNativeDriver: function(): boolean { + return config.useNativeDriver || false; + }, } - }; + ); }; -var decay = function( +const decay = function( value: AnimatedValue | AnimatedValueXY, config: DecayAnimationConfig, ): CompositeAnimation { - var start = function( + const start = function( animatedValue: AnimatedValue | AnimatedValueXY, configuration: DecayAnimationConfig, - callback?: ?EndCallback): void { - callback = _combineCallbacks(callback, configuration); - var singleValue: any = animatedValue; - var singleConfig: any = configuration; - singleValue.stopTracking(); - singleValue.animate(new DecayAnimation(singleConfig), callback); + callback?: ?EndCallback, + ): void { + callback = _combineCallbacks(callback, configuration); + const singleValue: any = animatedValue; + const singleConfig: any = configuration; + singleValue.stopTracking(); + singleValue.animate(new DecayAnimation(singleConfig), callback); }; - return maybeVectorAnim(value, config, decay) || { - start: function(callback?: ?EndCallback): void { - start(value, config, callback); - }, + return ( + maybeVectorAnim(value, config, decay) || { + start: function(callback?: ?EndCallback): void { + start(value, config, callback); + }, - stop: function(): void { - value.stopAnimation(); - }, + stop: function(): void { + value.stopAnimation(); + }, - reset: function(): void { - value.resetAnimation(); - }, + reset: function(): void { + value.resetAnimation(); + }, - _startNativeLoop: function(iterations?: number): void { - var singleConfig = { ...config, iterations }; - start(value, singleConfig); - }, + _startNativeLoop: function(iterations?: number): void { + const singleConfig = {...config, iterations}; + start(value, singleConfig); + }, - _isUsingNativeDriver: function(): boolean { - return config.useNativeDriver || false; + _isUsingNativeDriver: function(): boolean { + return config.useNativeDriver || false; + }, } - }; + ); }; -var sequence = function( +const sequence = function( animations: Array, ): CompositeAnimation { - var current = 0; + let current = 0; return { start: function(callback?: ?EndCallback) { - var onComplete = function(result) { + const onComplete = function(result) { if (!result.finished) { callback && callback(result); return; @@ -2223,28 +314,30 @@ var sequence = function( }, _startNativeLoop: function() { - throw new Error('Loops run using the native driver cannot contain Animated.sequence animations'); + throw new Error( + 'Loops run using the native driver cannot contain Animated.sequence animations', + ); }, _isUsingNativeDriver: function(): boolean { return false; - } + }, }; }; type ParallelConfig = { - stopTogether?: bool, // If one is stopped, stop all. default: true -} -var parallel = function( + stopTogether?: boolean, // If one is stopped, stop all. default: true +}; +const parallel = function( animations: Array, config?: ?ParallelConfig, ): CompositeAnimation { - var doneCount = 0; + let doneCount = 0; // Make sure we only call stop() at most once for each animation - var hasEnded = {}; - var stopTogether = !(config && config.stopTogether === false); + const hasEnded = {}; + const stopTogether = !(config && config.stopTogether === false); - var result = { + const result = { start: function(callback?: ?EndCallback) { if (doneCount === animations.length) { callback && callback({finished: true}); @@ -2252,7 +345,7 @@ var parallel = function( } animations.forEach((animation, idx) => { - var cb = function(endResult) { + const cb = function(endResult) { hasEnded[idx] = true; doneCount++; if (doneCount === animations.length) { @@ -2290,48 +383,51 @@ var parallel = function( }, _startNativeLoop: function() { - throw new Error('Loops run using the native driver cannot contain Animated.parallel animations'); + throw new Error( + 'Loops run using the native driver cannot contain Animated.parallel animations', + ); }, _isUsingNativeDriver: function(): boolean { return false; - } + }, }; return result; }; -var delay = function(time: number): CompositeAnimation { +const 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( +const stagger = function( time: number, animations: Array, ): CompositeAnimation { - return parallel(animations.map((animation, i) => { - return sequence([ - delay(time * i), - animation, - ]); - })); + return parallel( + animations.map((animation, i) => { + return sequence([delay(time * i), animation]); + }), + ); }; -type LoopAnimationConfig = { iterations: number }; +type LoopAnimationConfig = {iterations: number}; -var loop = function( +const loop = function( animation: CompositeAnimation, - { iterations = -1 }: LoopAnimationConfig = {}, + {iterations = -1}: LoopAnimationConfig = {}, ): CompositeAnimation { - var isFinished = false; - var iterationsSoFar = 0; + let isFinished = false; + let iterationsSoFar = 0; return { start: function(callback?: ?EndCallback) { - var restart = function(result: EndResult = {finished: true}): void { - if (isFinished || - (iterationsSoFar === iterations) || - (result.finished === false)) { + const restart = function(result: EndResult = {finished: true}): void { + if ( + isFinished || + iterationsSoFar === iterations || + result.finished === false + ) { callback && callback(result); } else { iterationsSoFar++; @@ -2362,69 +458,21 @@ var loop = function( }, _startNativeLoop: function() { - throw new Error('Loops run using the native driver cannot contain Animated.loop animations'); + throw new Error( + 'Loops run using the native driver cannot contain Animated.loop animations', + ); }, _isUsingNativeDriver: function(): boolean { return animation._isUsingNativeDriver(); - } - }; -}; - -type Mapping = {[key: string]: Mapping} | AnimatedValue; -type EventConfig = { - listener?: ?Function, - useNativeDriver?: bool, -}; - -function attachNativeEvent(viewRef: any, eventName: string, argMapping: Array) { - // Find animated values in `argMapping` and create an array representing their - // key path inside the `nativeEvent` object. Ex.: ['contentOffset', 'x']. - const eventMappings = []; - - const traverse = (value, path) => { - if (value instanceof AnimatedValue) { - value.__makeNative(); - - eventMappings.push({ - nativeEventPath: path, - animatedValueTag: value.__getNativeTag(), - }); - } else if (typeof value === 'object') { - for (const key in value) { - traverse(value[key], path.concat(key)); - } - } - }; - - invariant( - argMapping[0] && argMapping[0].nativeEvent, - 'Native driven events only support animated values contained inside `nativeEvent`.' - ); - - // Assume that the event containing `nativeEvent` is always the first argument. - traverse(argMapping[0].nativeEvent, []); - - const viewTag = ReactNative.findNodeHandle(viewRef); - - eventMappings.forEach((mapping) => { - NativeAnimatedAPI.addAnimatedEventToView(viewTag, eventName, mapping); - }); - - return { - detach() { - eventMappings.forEach((mapping) => { - NativeAnimatedAPI.removeAnimatedEventFromView( - viewTag, - eventName, - mapping.animatedValueTag, - ); - }); }, }; -} +}; -function forkEvent(event: ?AnimatedEvent | ?Function, listener: Function): AnimatedEvent | Function { +function forkEvent( + event: ?AnimatedEvent | ?Function, + listener: Function, +): AnimatedEvent | Function { if (!event) { return listener; } else if (event instanceof AnimatedEvent) { @@ -2438,119 +486,16 @@ function forkEvent(event: ?AnimatedEvent | ?Function, listener: Function): Anima } } -function unforkEvent(event: ?AnimatedEvent | ?Function, listener: Function): void { +function unforkEvent( + event: ?AnimatedEvent | ?Function, + listener: Function, +): void { if (event && event instanceof AnimatedEvent) { event.__removeListener(listener); } } -class AnimatedEvent { - _argMapping: Array; - _listeners: Array = []; - _callListeners: Function; - _attachedEvent: ?{ - detach: () => void, - }; - __isNative: bool; - - constructor( - argMapping: Array, - config?: EventConfig = {} - ) { - this._argMapping = argMapping; - if (config.listener) { - this.__addListener(config.listener); - } - this._callListeners = this._callListeners.bind(this); - this._attachedEvent = null; - this.__isNative = shouldUseNativeDriver(config); - - if (__DEV__) { - this._validateMapping(); - } - } - - __addListener(callback: Function): void { - this._listeners.push(callback); - } - - __removeListener(callback: Function): void { - this._listeners = this._listeners.filter((listener) => listener !== callback); - } - - __attach(viewRef, eventName) { - invariant(this.__isNative, 'Only native driven events need to be attached.'); - - this._attachedEvent = attachNativeEvent(viewRef, eventName, this._argMapping); - } - - __detach(viewTag, eventName) { - invariant(this.__isNative, 'Only native driven events need to be detached.'); - - this._attachedEvent && this._attachedEvent.detach(); - } - - __getHandler() { - if (this.__isNative) { - return this._callListeners; - } - - return (...args) => { - const traverse = (recMapping, recEvt, key) => { - if (typeof recEvt === 'number' && recMapping instanceof AnimatedValue) { - recMapping.setValue(recEvt); - } else if (typeof recMapping === 'object') { - for (const mappingKey in recMapping) { - /* $FlowFixMe(>=0.53.0 site=react_native_fb) This comment - * suppresses an error found when Flow v0.53 was deployed. To see - * the error delete this comment and run Flow. */ - traverse(recMapping[mappingKey], recEvt[mappingKey], mappingKey); - } - } - }; - - if (!this.__isNative) { - this._argMapping.forEach((mapping, idx) => { - traverse(mapping, args[idx], 'arg' + idx); - }); - } - this._callListeners(...args); - }; - } - - _callListeners(...args) { - this._listeners.forEach(listener => listener(...args)); - } - - _validateMapping() { - const traverse = (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' - ); - 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 (const mappingKey in recMapping) { - traverse(recMapping[mappingKey], recEvt[mappingKey], mappingKey); - } - }; - } -} - -var event = function( - argMapping: Array, - config?: EventConfig, -): any { +const event = function(argMapping: Array, config?: EventConfig): any { const animatedEvent = new AnimatedEvent(argMapping, config); if (animatedEvent.__isNative) { return animatedEvent; @@ -2743,7 +688,7 @@ module.exports = { /** * Exported for ease of type checking. All animated values derive from this class. */ - Node: Animated, + Node: AnimatedNode, /** * Animates a value from an initial velocity to zero based on a decay diff --git a/Libraries/Animated/src/NativeAnimatedHelper.js b/Libraries/Animated/src/NativeAnimatedHelper.js index 72c22ef63af1c8..cc25a4b9ceb779 100644 --- a/Libraries/Animated/src/NativeAnimatedHelper.js +++ b/Libraries/Animated/src/NativeAnimatedHelper.js @@ -8,6 +8,7 @@ * * @providesModule NativeAnimatedHelper * @flow + * @format */ 'use strict'; @@ -16,6 +17,9 @@ const NativeEventEmitter = require('NativeEventEmitter'); const invariant = require('fbjs/lib/invariant'); +import type {AnimationConfig} from './animations/Animation'; +import type {EventConfig} from './AnimatedEvent'; + let __nativeAnimatedNodeTagCount = 1; /* used for animated nodes */ let __nativeAnimationIdCount = 1; /* used for started animations */ @@ -49,13 +53,26 @@ const API = { assertNativeAnimatedModule(); NativeAnimatedModule.connectAnimatedNodes(parentTag, childTag); }, - disconnectAnimatedNodes: function(parentTag: ?number, childTag: ?number): void { + disconnectAnimatedNodes: function( + parentTag: ?number, + childTag: ?number, + ): void { assertNativeAnimatedModule(); NativeAnimatedModule.disconnectAnimatedNodes(parentTag, childTag); }, - startAnimatingNode: function(animationId: ?number, nodeTag: ?number, config: Object, endCallback: EndCallback): void { + startAnimatingNode: function( + animationId: ?number, + nodeTag: ?number, + config: Object, + endCallback: EndCallback, + ): void { assertNativeAnimatedModule(); - NativeAnimatedModule.startAnimatingNode(animationId, nodeTag, config, endCallback); + NativeAnimatedModule.startAnimatingNode( + animationId, + nodeTag, + config, + endCallback, + ); }, stopAnimation: function(animationId: ?number) { assertNativeAnimatedModule(); @@ -77,11 +94,17 @@ const API = { assertNativeAnimatedModule(); NativeAnimatedModule.extractAnimatedNodeOffset(nodeTag); }, - connectAnimatedNodeToView: function(nodeTag: ?number, viewTag: ?number): void { + connectAnimatedNodeToView: function( + nodeTag: ?number, + viewTag: ?number, + ): void { assertNativeAnimatedModule(); NativeAnimatedModule.connectAnimatedNodeToView(nodeTag, viewTag); }, - disconnectAnimatedNodeFromView: function(nodeTag: ?number, viewTag: ?number): void { + disconnectAnimatedNodeFromView: function( + nodeTag: ?number, + viewTag: ?number, + ): void { assertNativeAnimatedModule(); NativeAnimatedModule.disconnectAnimatedNodeFromView(nodeTag, viewTag); }, @@ -89,14 +112,30 @@ const API = { assertNativeAnimatedModule(); NativeAnimatedModule.dropAnimatedNode(tag); }, - addAnimatedEventToView: function(viewTag: ?number, eventName: string, eventMapping: EventMapping) { - assertNativeAnimatedModule(); - NativeAnimatedModule.addAnimatedEventToView(viewTag, eventName, eventMapping); + addAnimatedEventToView: function( + viewTag: ?number, + eventName: string, + eventMapping: EventMapping, + ) { + assertNativeAnimatedModule(); + NativeAnimatedModule.addAnimatedEventToView( + viewTag, + eventName, + eventMapping, + ); + }, + removeAnimatedEventFromView( + viewTag: ?number, + eventName: string, + animatedNodeTag: ?number, + ) { + assertNativeAnimatedModule(); + NativeAnimatedModule.removeAnimatedEventFromView( + viewTag, + eventName, + animatedNodeTag, + ); }, - removeAnimatedEventFromView(viewTag: ?number, eventName: string, animatedNodeTag: ?number) { - assertNativeAnimatedModule(); - NativeAnimatedModule.removeAnimatedEventFromView(viewTag, eventName, animatedNodeTag); - } }; /** @@ -128,9 +167,11 @@ const TRANSFORM_WHITELIST = { }; function validateTransform(configs: Array): void { - configs.forEach((config) => { + configs.forEach(config => { if (!TRANSFORM_WHITELIST.hasOwnProperty(config.property)) { - throw new Error(`Property '${config.property}' is not supported by native animated module`); + throw new Error( + `Property '${config.property}' is not supported by native animated module`, + ); } }); } @@ -138,7 +179,9 @@ function validateTransform(configs: Array): void { function validateStyles(styles: Object): void { for (var key in styles) { if (!STYLES_WHITELIST.hasOwnProperty(key)) { - throw new Error(`Style property '${key}' is not supported by native animated module`); + throw new Error( + `Style property '${key}' is not supported by native animated module`, + ); } } } @@ -153,7 +196,9 @@ function validateInterpolation(config: Object): void { }; for (var key in config) { if (!SUPPORTED_INTERPOLATION_PARAMS.hasOwnProperty(key)) { - throw new Error(`Interpolation property '${key}' is not supported by native animated module`); + throw new Error( + `Interpolation property '${key}' is not supported by native animated module`, + ); } } } @@ -170,8 +215,24 @@ function assertNativeAnimatedModule(): void { invariant(NativeAnimatedModule, 'Native animated module is not available'); } -function isNativeAnimatedAvailable(): boolean { - return !!NativeAnimatedModule; +let _warnedMissingNativeAnimated = false; + +function shouldUseNativeDriver(config: AnimationConfig | EventConfig): boolean { + if (config.useNativeDriver && !NativeAnimatedModule) { + if (!_warnedMissingNativeAnimated) { + console.warn( + 'Animated: `useNativeDriver` is not supported because the native ' + + 'animated module is missing. Falling back to JS-based animation. To ' + + 'resolve this, add `RCTAnimation` module to this app, or remove ' + + '`useNativeDriver`. ' + + 'More info: https://github.com/facebook/react-native/issues/11094#issuecomment-263240420', + ); + _warnedMissingNativeAnimated = true; + } + return false; + } + + return config.useNativeDriver || false; } module.exports = { @@ -182,7 +243,7 @@ module.exports = { generateNewNodeTag, generateNewAnimationId, assertNativeAnimatedModule, - isNativeAnimatedAvailable, + shouldUseNativeDriver, get nativeEventEmitter() { if (!nativeEventEmitter) { nativeEventEmitter = new NativeEventEmitter(NativeAnimatedModule); diff --git a/Libraries/Animated/src/__tests__/Interpolation-test.js b/Libraries/Animated/src/__tests__/Interpolation-test.js index 442592bd8f608b..8ff46339c96490 100644 --- a/Libraries/Animated/src/__tests__/Interpolation-test.js +++ b/Libraries/Animated/src/__tests__/Interpolation-test.js @@ -8,12 +8,12 @@ */ 'use strict'; -var Interpolation = require('Interpolation'); +var AnimatedInterpolation = require('../nodes/AnimatedInterpolation'); var Easing = require('Easing'); describe('Interpolation', () => { it('should work with defaults', () => { - var interpolation = Interpolation.create({ + var interpolation = AnimatedInterpolation.__createInterpolation({ inputRange: [0, 1], outputRange: [0, 1], }); @@ -25,7 +25,7 @@ describe('Interpolation', () => { }); it('should work with output range', () => { - var interpolation = Interpolation.create({ + var interpolation = AnimatedInterpolation.__createInterpolation({ inputRange: [0, 1], outputRange: [100, 200], }); @@ -37,7 +37,7 @@ describe('Interpolation', () => { }); it('should work with input range', () => { - var interpolation = Interpolation.create({ + var interpolation = AnimatedInterpolation.__createInterpolation({ inputRange: [100, 200], outputRange: [0, 1], }); @@ -49,19 +49,23 @@ describe('Interpolation', () => { }); 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(); + expect(() => + AnimatedInterpolation.__createInterpolation({ + inputRange: [0, 2, 1], + outputRange: [0, 1, 2], + }), + ).toThrow(); + + expect(() => + AnimatedInterpolation.__createInterpolation({ + inputRange: [0, 1, 2], + outputRange: [0, 3, 1], + }), + ).not.toThrow(); }); it('should work with empty input range', () => { - var interpolation = Interpolation.create({ + var interpolation = AnimatedInterpolation.__createInterpolation({ inputRange: [0, 10, 10], outputRange: [1, 2, 3], extrapolate: 'extend', @@ -75,7 +79,7 @@ describe('Interpolation', () => { }); it('should work with empty output range', () => { - var interpolation = Interpolation.create({ + var interpolation = AnimatedInterpolation.__createInterpolation({ inputRange: [1, 2, 3], outputRange: [0, 10, 10], extrapolate: 'extend', @@ -90,7 +94,7 @@ describe('Interpolation', () => { }); it('should work with easing', () => { - var interpolation = Interpolation.create({ + var interpolation = AnimatedInterpolation.__createInterpolation({ inputRange: [0, 1], outputRange: [0, 1], easing: Easing.quad, @@ -103,7 +107,7 @@ describe('Interpolation', () => { }); it('should work with extrapolate', () => { - var interpolation = Interpolation.create({ + var interpolation = AnimatedInterpolation.__createInterpolation({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'extend', @@ -113,7 +117,7 @@ describe('Interpolation', () => { expect(interpolation(-2)).toBe(4); expect(interpolation(2)).toBe(4); - interpolation = Interpolation.create({ + interpolation = AnimatedInterpolation.__createInterpolation({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp', @@ -123,7 +127,7 @@ describe('Interpolation', () => { expect(interpolation(-2)).toBe(0); expect(interpolation(2)).toBe(1); - interpolation = Interpolation.create({ + interpolation = AnimatedInterpolation.__createInterpolation({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'identity', @@ -135,7 +139,7 @@ describe('Interpolation', () => { }); it('should work with keyframes with extrapolate', () => { - var interpolation = Interpolation.create({ + var interpolation = AnimatedInterpolation.__createInterpolation({ inputRange: [0, 10, 100, 1000], outputRange: [0, 5, 50, 500], extrapolate: true, @@ -153,7 +157,7 @@ describe('Interpolation', () => { }); it('should work with keyframes without extrapolate', () => { - var interpolation = Interpolation.create({ + var interpolation = AnimatedInterpolation.__createInterpolation({ inputRange: [0, 1, 2], outputRange: [0.2, 1, 0.2], extrapolate: 'clamp', @@ -163,19 +167,23 @@ describe('Interpolation', () => { }); 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(); + expect(() => + AnimatedInterpolation.__createInterpolation({ + inputRange: [-Infinity, Infinity], + outputRange: [0, 1], + }), + ).toThrow(); + + expect(() => + AnimatedInterpolation.__createInterpolation({ + inputRange: [-Infinity, 0, Infinity], + outputRange: [1, 2, 3], + }), + ).not.toThrow(); }); it('should work with negative infinite', () => { - var interpolation = Interpolation.create({ + var interpolation = AnimatedInterpolation.__createInterpolation({ inputRange: [-Infinity, 0], outputRange: [-Infinity, 0], easing: Easing.quad, @@ -191,7 +199,7 @@ describe('Interpolation', () => { }); it('should work with positive infinite', () => { - var interpolation = Interpolation.create({ + var interpolation = AnimatedInterpolation.__createInterpolation({ inputRange: [5, Infinity], outputRange: [5, Infinity], easing: Easing.quad, @@ -204,12 +212,12 @@ describe('Interpolation', () => { 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(100)).toBeCloseTo(5 + 95 * 95); expect(interpolation(Infinity)).toBe(Infinity); }); it('should work with output ranges as string', () => { - var interpolation = Interpolation.create({ + var interpolation = AnimatedInterpolation.__createInterpolation({ inputRange: [0, 1], outputRange: ['rgba(0, 100, 200, 0)', 'rgba(50, 150, 250, 0.4)'], }); @@ -220,7 +228,7 @@ describe('Interpolation', () => { }); it('should work with output ranges as short hex string', () => { - var interpolation = Interpolation.create({ + var interpolation = AnimatedInterpolation.__createInterpolation({ inputRange: [0, 1], outputRange: ['#024', '#9BF'], }); @@ -231,7 +239,7 @@ describe('Interpolation', () => { }); it('should work with output ranges as long hex string', () => { - var interpolation = Interpolation.create({ + var interpolation = AnimatedInterpolation.__createInterpolation({ inputRange: [0, 1], outputRange: ['#FF9500', '#87FC70'], }); @@ -242,7 +250,7 @@ describe('Interpolation', () => { }); it('should work with output ranges with mixed hex and rgba strings', () => { - var interpolation = Interpolation.create({ + var interpolation = AnimatedInterpolation.__createInterpolation({ inputRange: [0, 1], outputRange: ['rgba(100, 120, 140, .4)', '#87FC70'], }); @@ -253,7 +261,7 @@ describe('Interpolation', () => { }); it('should work with negative and decimal values in string ranges', () => { - var interpolation = Interpolation.create({ + var interpolation = AnimatedInterpolation.__createInterpolation({ inputRange: [0, 1], outputRange: ['-100.5deg', '100deg'], }); @@ -264,15 +272,17 @@ describe('Interpolation', () => { }); it('should crash when chaining an interpolation that returns a string', () => { - var interpolation = Interpolation.create({ + var interpolation = AnimatedInterpolation.__createInterpolation({ inputRange: [0, 1], outputRange: [0, 1], }); - expect(() => { interpolation('45rad'); }).toThrow(); + expect(() => { + interpolation('45rad'); + }).toThrow(); }); it('should support a mix of color patterns', () => { - var interpolation = Interpolation.create({ + var interpolation = AnimatedInterpolation.__createInterpolation({ inputRange: [0, 1, 2], outputRange: ['rgba(0, 100, 200, 0)', 'rgb(50, 150, 250)', 'red'], }); @@ -284,14 +294,16 @@ describe('Interpolation', () => { }); it('should crash when defining output range with different pattern', () => { - expect(() => Interpolation.create({ - inputRange: [0, 1], - outputRange: ['20deg', '30rad'], - })).toThrow(); + expect(() => + AnimatedInterpolation.__createInterpolation({ + inputRange: [0, 1], + outputRange: ['20deg', '30rad'], + }), + ).toThrow(); }); it('should round the alpha channel of a color to the nearest thousandth', () => { - var interpolation = Interpolation.create({ + var interpolation = AnimatedInterpolation.__createInterpolation({ inputRange: [0, 1], outputRange: ['rgba(0, 0, 0, 0)', 'rgba(0, 0, 0, 1)'], }); diff --git a/Libraries/Animated/src/animations/Animation.js b/Libraries/Animated/src/animations/Animation.js new file mode 100644 index 00000000000000..a55b3c34322526 --- /dev/null +++ b/Libraries/Animated/src/animations/Animation.js @@ -0,0 +1,72 @@ +/** + * 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. + * + * @flow + * @format + */ +'use strict'; + +const NativeAnimatedHelper = require('NativeAnimatedHelper'); + +import type AnimatedValue from '../nodes/AnimatedValue'; + +export type EndResult = {finished: boolean}; +export type EndCallback = (result: EndResult) => void; + +export type AnimationConfig = { + isInteraction?: boolean, + useNativeDriver?: boolean, + onComplete?: ?EndCallback, + iterations?: number, +}; + +// 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: boolean; + __isInteraction: boolean; + __nativeId: number; + __onEnd: ?EndCallback; + __iterations: number; + start( + fromValue: number, + onUpdate: (value: number) => void, + onEnd: ?EndCallback, + previousAnimation: ?Animation, + animatedValue: AnimatedValue, + ): void {} + stop(): void { + if (this.__nativeId) { + NativeAnimatedHelper.API.stopAnimation(this.__nativeId); + } + } + __getNativeAnimationConfig(): any { + // Subclasses that have corresponding animation implementation done in native + // should override this method + throw new Error('This animation type cannot be offloaded to native'); + } + // Helper function for subclasses to make sure onEnd is only called once. + __debouncedOnEnd(result: EndResult): void { + const onEnd = this.__onEnd; + this.__onEnd = null; + onEnd && onEnd(result); + } + __startNativeAnimation(animatedValue: AnimatedValue): void { + animatedValue.__makeNative(); + this.__nativeId = NativeAnimatedHelper.generateNewAnimationId(); + NativeAnimatedHelper.API.startAnimatingNode( + this.__nativeId, + animatedValue.__getNativeTag(), + this.__getNativeAnimationConfig(), + this.__debouncedOnEnd.bind(this), + ); + } +} + +module.exports = Animation; diff --git a/Libraries/Animated/src/animations/DecayAnimation.js b/Libraries/Animated/src/animations/DecayAnimation.js new file mode 100644 index 00000000000000..3a408fcbaa6303 --- /dev/null +++ b/Libraries/Animated/src/animations/DecayAnimation.js @@ -0,0 +1,111 @@ +/** + * 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. + * + * @flow + * @format + */ +'use strict'; + +const Animation = require('./Animation'); + +const {shouldUseNativeDriver} = require('../NativeAnimatedHelper'); + +import type {AnimationConfig, EndCallback} from './Animation'; +import type AnimatedValue from '../nodes/AnimatedValue'; + +export type DecayAnimationConfig = AnimationConfig & { + velocity: number | {x: number, y: number}, + deceleration?: number, +}; + +export type DecayAnimationConfigSingle = AnimationConfig & { + velocity: number, + deceleration?: number, +}; + +class DecayAnimation extends Animation { + _startTime: number; + _lastValue: number; + _fromValue: number; + _deceleration: number; + _velocity: number; + _onUpdate: (value: number) => void; + _animationFrame: any; + _useNativeDriver: boolean; + + constructor(config: DecayAnimationConfigSingle) { + super(); + this._deceleration = + config.deceleration !== undefined ? config.deceleration : 0.998; + this._velocity = config.velocity; + this._useNativeDriver = shouldUseNativeDriver(config); + this.__isInteraction = + config.isInteraction !== undefined ? config.isInteraction : true; + this.__iterations = config.iterations !== undefined ? config.iterations : 1; + } + + __getNativeAnimationConfig() { + return { + type: 'decay', + deceleration: this._deceleration, + velocity: this._velocity, + iterations: this.__iterations, + }; + } + + start( + fromValue: number, + onUpdate: (value: number) => void, + onEnd: ?EndCallback, + previousAnimation: ?Animation, + animatedValue: AnimatedValue, + ): void { + this.__active = true; + this._lastValue = fromValue; + this._fromValue = fromValue; + this._onUpdate = onUpdate; + this.__onEnd = onEnd; + this._startTime = Date.now(); + if (this._useNativeDriver) { + this.__startNativeAnimation(animatedValue); + } else { + this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this)); + } + } + + onUpdate(): void { + const now = Date.now(); + + const 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 = requestAnimationFrame(this.onUpdate.bind(this)); + } + } + + stop(): void { + super.stop(); + this.__active = false; + global.cancelAnimationFrame(this._animationFrame); + this.__debouncedOnEnd({finished: false}); + } +} + +module.exports = DecayAnimation; diff --git a/Libraries/Animated/src/animations/SpringAnimation.js b/Libraries/Animated/src/animations/SpringAnimation.js new file mode 100644 index 00000000000000..41eaf78359b399 --- /dev/null +++ b/Libraries/Animated/src/animations/SpringAnimation.js @@ -0,0 +1,290 @@ +/** + * 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. + * + * @flow + * @format + */ +'use strict'; + +const AnimatedValue = require('../nodes/AnimatedValue'); +const AnimatedValueXY = require('../nodes/AnimatedValueXY'); +const Animation = require('./Animation'); +const SpringConfig = require('../SpringConfig'); + +const invariant = require('fbjs/lib/invariant'); +const {shouldUseNativeDriver} = require('../NativeAnimatedHelper'); + +import type {AnimationConfig, EndCallback} from './Animation'; + +export type SpringAnimationConfig = AnimationConfig & { + toValue: number | AnimatedValue | {x: number, y: number} | AnimatedValueXY, + overshootClamping?: boolean, + restDisplacementThreshold?: number, + restSpeedThreshold?: number, + velocity?: number | {x: number, y: number}, + bounciness?: number, + speed?: number, + tension?: number, + friction?: number, + delay?: number, +}; + +export type SpringAnimationConfigSingle = AnimationConfig & { + toValue: number | AnimatedValue, + overshootClamping?: boolean, + restDisplacementThreshold?: number, + restSpeedThreshold?: number, + velocity?: number, + bounciness?: number, + speed?: number, + tension?: number, + friction?: number, + delay?: number, +}; + +function withDefault(value: ?T, defaultValue: T): T { + if (value === undefined || value === null) { + return defaultValue; + } + return value; +} + +class SpringAnimation extends Animation { + _overshootClamping: boolean; + _restDisplacementThreshold: number; + _restSpeedThreshold: number; + _initialVelocity: ?number; + _lastVelocity: number; + _startPosition: number; + _lastPosition: number; + _fromValue: number; + _toValue: any; + _tension: number; + _friction: number; + _delay: number; + _timeout: any; + _lastTime: number; + _onUpdate: (value: number) => void; + _animationFrame: any; + _useNativeDriver: boolean; + + 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; + this._delay = withDefault(config.delay, 0); + this._useNativeDriver = shouldUseNativeDriver(config); + this.__isInteraction = + config.isInteraction !== undefined ? config.isInteraction : true; + this.__iterations = config.iterations !== undefined ? config.iterations : 1; + + let 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 = SpringConfig.fromBouncinessAndSpeed( + withDefault(config.bounciness, 8), + withDefault(config.speed, 12), + ); + } else { + springConfig = SpringConfig.fromOrigamiTensionAndFriction( + withDefault(config.tension, 40), + withDefault(config.friction, 7), + ); + } + this._tension = springConfig.tension; + this._friction = springConfig.friction; + } + + __getNativeAnimationConfig() { + return { + type: 'spring', + overshootClamping: this._overshootClamping, + restDisplacementThreshold: this._restDisplacementThreshold, + restSpeedThreshold: this._restSpeedThreshold, + tension: this._tension, + friction: this._friction, + initialVelocity: withDefault(this._initialVelocity, this._lastVelocity), + toValue: this._toValue, + iterations: this.__iterations, + }; + } + + start( + fromValue: number, + onUpdate: (value: number) => void, + onEnd: ?EndCallback, + previousAnimation: ?Animation, + animatedValue: AnimatedValue, + ): 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) { + const 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; + } + + const start = () => { + if (this._useNativeDriver) { + this.__startNativeAnimation(animatedValue); + } else { + this.onUpdate(); + } + }; + + // If this._delay is more than 0, we start after the timeout. + if (this._delay) { + this._timeout = setTimeout(start, this._delay); + } else { + start(); + } + } + + getInternalState(): Object { + return { + lastPosition: this._lastPosition, + lastVelocity: this._lastVelocity, + lastTime: this._lastTime, + }; + } + + onUpdate(): void { + let position = this._lastPosition; + let velocity = this._lastVelocity; + + let tempPosition = this._lastPosition; + let 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. + const MAX_STEPS = 64; + let 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/ + const TIMESTEP_MSEC = 1; + const numSteps = Math.floor((now - this._lastTime) / TIMESTEP_MSEC); + + for (let i = 0; i < numSteps; ++i) { + // Velocity is based on seconds instead of milliseconds + const step = TIMESTEP_MSEC / 1000; + + // This is using RK4. A good blog post to understand how it works: + // http://gafferongames.com/game-physics/integration-basics/ + const aVelocity = velocity; + const aAcceleration = + this._tension * (this._toValue - tempPosition) - + this._friction * tempVelocity; + tempPosition = position + aVelocity * step / 2; + tempVelocity = velocity + aAcceleration * step / 2; + + const bVelocity = tempVelocity; + const bAcceleration = + this._tension * (this._toValue - tempPosition) - + this._friction * tempVelocity; + tempPosition = position + bVelocity * step / 2; + tempVelocity = velocity + bAcceleration * step / 2; + + const cVelocity = tempVelocity; + const cAcceleration = + this._tension * (this._toValue - tempPosition) - + this._friction * tempVelocity; + tempPosition = position + cVelocity * step / 2; + tempVelocity = velocity + cAcceleration * step / 2; + + const dVelocity = tempVelocity; + const dAcceleration = + this._tension * (this._toValue - tempPosition) - + this._friction * tempVelocity; + tempPosition = position + cVelocity * step / 2; + tempVelocity = velocity + cAcceleration * step / 2; + + const dxdt = (aVelocity + 2 * (bVelocity + cVelocity) + dVelocity) / 6; + const 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 + let isOvershooting = false; + if (this._overshootClamping && this._tension !== 0) { + if (this._startPosition < this._toValue) { + isOvershooting = position > this._toValue; + } else { + isOvershooting = position < this._toValue; + } + } + const isVelocity = Math.abs(velocity) <= this._restSpeedThreshold; + let isDisplacement = true; + if (this._tension !== 0) { + isDisplacement = + Math.abs(this._toValue - position) <= this._restDisplacementThreshold; + } + + if (isOvershooting || (isVelocity && isDisplacement)) { + if (this._tension !== 0) { + // Ensure that we end up with a round value + this._onUpdate(this._toValue); + } + + this.__debouncedOnEnd({finished: true}); + return; + } + this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this)); + } + + stop(): void { + super.stop(); + this.__active = false; + clearTimeout(this._timeout); + global.cancelAnimationFrame(this._animationFrame); + this.__debouncedOnEnd({finished: false}); + } +} + +module.exports = SpringAnimation; diff --git a/Libraries/Animated/src/animations/TimingAnimation.js b/Libraries/Animated/src/animations/TimingAnimation.js new file mode 100644 index 00000000000000..39d5939550784f --- /dev/null +++ b/Libraries/Animated/src/animations/TimingAnimation.js @@ -0,0 +1,154 @@ +/** + * 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. + * + * @flow + * @format + */ +'use strict'; + +const AnimatedValue = require('../nodes/AnimatedValue'); +const AnimatedValueXY = require('../nodes/AnimatedValueXY'); +const Animation = require('./Animation'); + +const {shouldUseNativeDriver} = require('../NativeAnimatedHelper'); + +import type {AnimationConfig, EndCallback} from './Animation'; + +export type TimingAnimationConfig = AnimationConfig & { + toValue: number | AnimatedValue | {x: number, y: number} | AnimatedValueXY, + easing?: (value: number) => number, + duration?: number, + delay?: number, +}; + +export type TimingAnimationConfigSingle = AnimationConfig & { + toValue: number | AnimatedValue, + easing?: (value: number) => number, + duration?: number, + delay?: number, +}; + +let _easeInOut; +function easeInOut() { + if (!_easeInOut) { + const Easing = require('Easing'); + _easeInOut = Easing.inOut(Easing.ease); + } + return _easeInOut; +} + +class TimingAnimation extends Animation { + _startTime: number; + _fromValue: number; + _toValue: any; + _duration: number; + _delay: number; + _easing: (value: number) => number; + _onUpdate: (value: number) => void; + _animationFrame: any; + _timeout: any; + _useNativeDriver: boolean; + + constructor(config: TimingAnimationConfigSingle) { + super(); + this._toValue = config.toValue; + this._easing = config.easing !== undefined ? config.easing : easeInOut(); + this._duration = config.duration !== undefined ? config.duration : 500; + this._delay = config.delay !== undefined ? config.delay : 0; + this.__iterations = config.iterations !== undefined ? config.iterations : 1; + this.__isInteraction = + config.isInteraction !== undefined ? config.isInteraction : true; + this._useNativeDriver = shouldUseNativeDriver(config); + } + + __getNativeAnimationConfig(): any { + const frameDuration = 1000.0 / 60.0; + const frames = []; + for (let dt = 0.0; dt < this._duration; dt += frameDuration) { + frames.push(this._easing(dt / this._duration)); + } + frames.push(this._easing(1)); + return { + type: 'frames', + frames, + toValue: this._toValue, + iterations: this.__iterations, + }; + } + + start( + fromValue: number, + onUpdate: (value: number) => void, + onEnd: ?EndCallback, + previousAnimation: ?Animation, + animatedValue: AnimatedValue, + ): void { + this.__active = true; + this._fromValue = fromValue; + this._onUpdate = onUpdate; + this.__onEnd = onEnd; + + const start = () => { + // Animations that sometimes have 0 duration and sometimes do not + // still need to use the native driver when duration is 0 so as to + // not cause intermixed JS and native animations. + if (this._duration === 0 && !this._useNativeDriver) { + this._onUpdate(this._toValue); + this.__debouncedOnEnd({finished: true}); + } else { + this._startTime = Date.now(); + if (this._useNativeDriver) { + this.__startNativeAnimation(animatedValue); + } else { + this._animationFrame = requestAnimationFrame( + this.onUpdate.bind(this), + ); + } + } + }; + if (this._delay) { + this._timeout = setTimeout(start, this._delay); + } else { + start(); + } + } + + onUpdate(): void { + const 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 = requestAnimationFrame(this.onUpdate.bind(this)); + } + } + + stop(): void { + super.stop(); + this.__active = false; + clearTimeout(this._timeout); + global.cancelAnimationFrame(this._animationFrame); + this.__debouncedOnEnd({finished: false}); + } +} + +module.exports = TimingAnimation; diff --git a/Libraries/Animated/src/createAnimatedComponent.js b/Libraries/Animated/src/createAnimatedComponent.js new file mode 100644 index 00000000000000..e90248346000ac --- /dev/null +++ b/Libraries/Animated/src/createAnimatedComponent.js @@ -0,0 +1,192 @@ +/** + * 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. + * + * @flow + * @format + */ +'use strict'; + +const {AnimatedEvent} = require('./AnimatedEvent'); +const AnimatedProps = require('./nodes/AnimatedProps'); +const React = require('React'); +const ViewStylePropTypes = require('ViewStylePropTypes'); + +function createAnimatedComponent(Component: any): any { + class AnimatedComponent extends React.Component { + _component: any; + _prevComponent: any; + _propsAnimated: AnimatedProps; + _eventDetachers: Array = []; + _setComponentRef: Function; + + static __skipSetNativeProps_FOR_TESTS_ONLY = false; + + constructor(props: Object) { + super(props); + this._setComponentRef = this._setComponentRef.bind(this); + } + + componentWillUnmount() { + this._propsAnimated && this._propsAnimated.__detach(); + this._detachNativeEvents(); + } + + setNativeProps(props) { + this._component.setNativeProps(props); + } + + componentWillMount() { + this._attachProps(this.props); + } + + componentDidMount() { + this._propsAnimated.setNativeView(this._component); + this._attachNativeEvents(); + } + + _attachNativeEvents() { + // Make sure to get the scrollable node for components that implement + // `ScrollResponder.Mixin`. + const scrollableNode = this._component.getScrollableNode + ? this._component.getScrollableNode() + : this._component; + + for (const key in this.props) { + const prop = this.props[key]; + if (prop instanceof AnimatedEvent && prop.__isNative) { + prop.__attach(scrollableNode, key); + this._eventDetachers.push(() => prop.__detach(scrollableNode, key)); + } + } + } + + _detachNativeEvents() { + this._eventDetachers.forEach(remove => remove()); + this._eventDetachers = []; + } + + _attachProps(nextProps) { + const 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. + const callback = () => { + if ( + !AnimatedComponent.__skipSetNativeProps_FOR_TESTS_ONLY && + this._component.setNativeProps + ) { + if (!this._propsAnimated.__isNative) { + this._component.setNativeProps( + this._propsAnimated.__getAnimatedValue(), + ); + } else { + throw new Error( + 'Attempting to run JS driven animation on animated ' + + 'node that has been moved to "native" earlier by starting an ' + + 'animation with `useNativeDriver: true`', + ); + } + } 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(newProps) { + this._attachProps(newProps); + } + + componentDidUpdate(prevProps) { + if (this._component !== this._prevComponent) { + this._propsAnimated.setNativeView(this._component); + } + if (this._component !== this._prevComponent || prevProps !== this.props) { + this._detachNativeEvents(); + this._attachNativeEvents(); + } + } + + render() { + const props = this._propsAnimated.__getValue(); + return ( + + ); + } + + _setComponentRef(c) { + this._prevComponent = this._component; + this._component = c; + } + + // A third party library can use getNode() + // to get the node reference of the decorated component + getNode() { + return this._component; + } + } + + // ReactNative `View.propTypes` have been deprecated in favor of + // `ViewPropTypes`. In their place a temporary getter has been added with a + // deprecated warning message. Avoid triggering that warning here by using + // temporary workaround, __propTypesSecretDontUseThesePlease. + // TODO (bvaughn) Revert this particular change any time after April 1 + const propTypes = + Component.__propTypesSecretDontUseThesePlease || Component.propTypes; + + AnimatedComponent.propTypes = { + style: function(props, propName, componentName) { + if (!propTypes) { + return; + } + + for (const key in ViewStylePropTypes) { + if (!propTypes[key] && props[key] !== undefined) { + console.warn( + 'You are setting the style `{ ' + + key + + ': ... }` as a prop. You ' + + 'should nest it in a style object. ' + + 'E.g. `{ style: { ' + + key + + ': ... } }`', + ); + } + } + }, + }; + + return AnimatedComponent; +} + +module.exports = createAnimatedComponent; diff --git a/Libraries/Animated/src/nodes/AnimatedAddition.js b/Libraries/Animated/src/nodes/AnimatedAddition.js new file mode 100644 index 00000000000000..a09279369e23a0 --- /dev/null +++ b/Libraries/Animated/src/nodes/AnimatedAddition.js @@ -0,0 +1,64 @@ +/** + * 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. + * + * @flow + * @format + */ +'use strict'; + +const AnimatedInterpolation = require('./AnimatedInterpolation'); +const AnimatedNode = require('./AnimatedNode'); +const AnimatedValue = require('./AnimatedValue'); +const AnimatedWithChildren = require('./AnimatedWithChildren'); + +import type {InterpolationConfigType} from './AnimatedInterpolation'; + +class AnimatedAddition extends AnimatedWithChildren { + _a: AnimatedNode; + _b: AnimatedNode; + + constructor(a: AnimatedNode | number, b: AnimatedNode | number) { + super(); + this._a = typeof a === 'number' ? new AnimatedValue(a) : a; + this._b = typeof b === 'number' ? new AnimatedValue(b) : b; + } + + __makeNative() { + this._a.__makeNative(); + this._b.__makeNative(); + super.__makeNative(); + } + + __getValue(): number { + return this._a.__getValue() + this._b.__getValue(); + } + + interpolate(config: InterpolationConfigType): AnimatedInterpolation { + return new AnimatedInterpolation(this, config); + } + + __attach(): void { + this._a.__addChild(this); + this._b.__addChild(this); + } + + __detach(): void { + this._a.__removeChild(this); + this._b.__removeChild(this); + super.__detach(); + } + + __getNativeConfig(): any { + return { + type: 'addition', + input: [this._a.__getNativeTag(), this._b.__getNativeTag()], + }; + } +} + +module.exports = AnimatedAddition; diff --git a/Libraries/Animated/src/nodes/AnimatedDiffClamp.js b/Libraries/Animated/src/nodes/AnimatedDiffClamp.js new file mode 100644 index 00000000000000..948c00102159b0 --- /dev/null +++ b/Libraries/Animated/src/nodes/AnimatedDiffClamp.js @@ -0,0 +1,72 @@ +/** + * 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. + * + * @flow + * @format + */ +'use strict'; + +const AnimatedInterpolation = require('./AnimatedInterpolation'); +const AnimatedNode = require('./AnimatedNode'); +const AnimatedWithChildren = require('./AnimatedWithChildren'); + +import type {InterpolationConfigType} from './AnimatedInterpolation'; + +class AnimatedDiffClamp extends AnimatedWithChildren { + _a: AnimatedNode; + _min: number; + _max: number; + _value: number; + _lastValue: number; + + constructor(a: AnimatedNode, min: number, max: number) { + super(); + + this._a = a; + this._min = min; + this._max = max; + this._value = this._lastValue = this._a.__getValue(); + } + + __makeNative() { + this._a.__makeNative(); + super.__makeNative(); + } + + interpolate(config: InterpolationConfigType): AnimatedInterpolation { + return new AnimatedInterpolation(this, config); + } + + __getValue(): number { + const value = this._a.__getValue(); + const diff = value - this._lastValue; + this._lastValue = value; + this._value = Math.min(Math.max(this._value + diff, this._min), this._max); + return this._value; + } + + __attach(): void { + this._a.__addChild(this); + } + + __detach(): void { + this._a.__removeChild(this); + super.__detach(); + } + + __getNativeConfig(): any { + return { + type: 'diffclamp', + input: this._a.__getNativeTag(), + min: this._min, + max: this._max, + }; + } +} + +module.exports = AnimatedDiffClamp; diff --git a/Libraries/Animated/src/nodes/AnimatedDivision.js b/Libraries/Animated/src/nodes/AnimatedDivision.js new file mode 100644 index 00000000000000..8bdd68b6e2e084 --- /dev/null +++ b/Libraries/Animated/src/nodes/AnimatedDivision.js @@ -0,0 +1,69 @@ +/** + * 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. + * + * @flow + * @format + */ +'use strict'; + +const AnimatedInterpolation = require('./AnimatedInterpolation'); +const AnimatedNode = require('./AnimatedNode'); +const AnimatedValue = require('./AnimatedValue'); +const AnimatedWithChildren = require('./AnimatedWithChildren'); + +import type {InterpolationConfigType} from './AnimatedInterpolation'; + +class AnimatedDivision extends AnimatedWithChildren { + _a: AnimatedNode; + _b: AnimatedNode; + + constructor(a: AnimatedNode | number, b: AnimatedNode | number) { + super(); + this._a = typeof a === 'number' ? new AnimatedValue(a) : a; + this._b = typeof b === 'number' ? new AnimatedValue(b) : b; + } + + __makeNative() { + this._a.__makeNative(); + this._b.__makeNative(); + super.__makeNative(); + } + + __getValue(): number { + const a = this._a.__getValue(); + const b = this._b.__getValue(); + if (b === 0) { + console.error('Detected division by zero in AnimatedDivision'); + } + return a / b; + } + + interpolate(config: InterpolationConfigType): AnimatedInterpolation { + return new AnimatedInterpolation(this, config); + } + + __attach(): void { + this._a.__addChild(this); + this._b.__addChild(this); + } + + __detach(): void { + this._a.__removeChild(this); + this._b.__removeChild(this); + super.__detach(); + } + + __getNativeConfig(): any { + return { + type: 'division', + input: [this._a.__getNativeTag(), this._b.__getNativeTag()], + }; + } +} + +module.exports = AnimatedDivision; diff --git a/Libraries/Animated/src/Interpolation.js b/Libraries/Animated/src/nodes/AnimatedInterpolation.js similarity index 51% rename from Libraries/Animated/src/Interpolation.js rename to Libraries/Animated/src/nodes/AnimatedInterpolation.js index 6a28246ca73ff4..69b2912993371c 100644 --- a/Libraries/Animated/src/Interpolation.js +++ b/Libraries/Animated/src/nodes/AnimatedInterpolation.js @@ -6,14 +6,18 @@ * 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 + * @format */ /* eslint no-bitwise: 0 */ 'use strict'; -var invariant = require('fbjs/lib/invariant'); -var normalizeColor = require('normalizeColor'); +const AnimatedNode = require('./AnimatedNode'); +const AnimatedWithChildren = require('./AnimatedWithChildren'); +const NativeAnimatedHelper = require('../NativeAnimatedHelper'); + +const invariant = require('fbjs/lib/invariant'); +const normalizeColor = require('normalizeColor'); type ExtrapolateType = 'extend' | 'identity' | 'clamp'; @@ -23,74 +27,76 @@ export type InterpolationConfigType = { * detected during the deployment of v0.38.0. To see the error, remove this * comment and run flow */ - outputRange: (Array | Array), - easing?: ((input: number) => number), + outputRange: Array | Array, + easing?: (input: number) => number, extrapolate?: ExtrapolateType, extrapolateLeft?: ExtrapolateType, extrapolateRight?: ExtrapolateType, }; -var linear = (t) => t; +const 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 { +function createInterpolation( + config: InterpolationConfigType, +): (input: number) => number | string { + if (config.outputRange && typeof config.outputRange[0] === 'string') { + return createInterpolationFromStringOutputRange(config); + } - if (config.outputRange && typeof config.outputRange[0] === 'string') { - return createInterpolationFromStringOutputRange(config); - } + const outputRange: Array = (config.outputRange: any); + checkInfiniteRange('outputRange', outputRange); - var outputRange: Array = (config.outputRange: any); - checkInfiniteRange('outputRange', outputRange); + const inputRange = config.inputRange; + checkInfiniteRange('inputRange', inputRange); + checkValidInputRange(inputRange); - 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', + ); - invariant( - inputRange.length === outputRange.length, - 'inputRange (' + inputRange.length + ') and outputRange (' + - outputRange.length + ') must have the same length' - ); + const easing = config.easing || linear; - var easing = config.easing || linear; + let extrapolateLeft: ExtrapolateType = 'extend'; + if (config.extrapolateLeft !== undefined) { + extrapolateLeft = config.extrapolateLeft; + } else if (config.extrapolate !== undefined) { + extrapolateLeft = config.extrapolate; + } - var extrapolateLeft: ExtrapolateType = 'extend'; - if (config.extrapolateLeft !== undefined) { - extrapolateLeft = config.extrapolateLeft; - } else if (config.extrapolate !== undefined) { - extrapolateLeft = config.extrapolate; - } + let extrapolateRight: ExtrapolateType = 'extend'; + if (config.extrapolateRight !== undefined) { + extrapolateRight = config.extrapolateRight; + } else if (config.extrapolate !== undefined) { + extrapolateRight = 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', + ); - 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, - ); - }; - } + const range = findRange(input, inputRange); + return interpolate( + input, + inputRange[range], + inputRange[range + 1], + outputRange[range], + outputRange[range + 1], + easing, + extrapolateLeft, + extrapolateRight, + ); + }; } function interpolate( @@ -99,11 +105,11 @@ function interpolate( inputMax: number, outputMin: number, outputMax: number, - easing: ((input: number) => number), + easing: (input: number) => number, extrapolateLeft: ExtrapolateType, extrapolateRight: ExtrapolateType, ) { - var result = input; + let result = input; // Extrapolate if (result < inputMin) { @@ -162,22 +168,22 @@ function interpolate( } function colorToRgba(input: string): string { - var int32Color = normalizeColor(input); + let int32Color = normalizeColor(input); if (int32Color === null) { return input; } int32Color = int32Color || 0; - var r = (int32Color & 0xff000000) >>> 24; - var g = (int32Color & 0x00ff0000) >>> 16; - var b = (int32Color & 0x0000ff00) >>> 8; - var a = (int32Color & 0x000000ff) / 255; + const r = (int32Color & 0xff000000) >>> 24; + const g = (int32Color & 0x00ff0000) >>> 16; + const b = (int32Color & 0x0000ff00) >>> 8; + const a = (int32Color & 0x000000ff) / 255; return `rgba(${r}, ${g}, ${b}, ${a})`; } -var stringShapeRegex = /[0-9\.-]+/g; +const stringShapeRegex = /[0-9\.-]+/g; /** * Supports string shapes by extracting numbers so new values can be computed, @@ -190,7 +196,7 @@ var stringShapeRegex = /[0-9\.-]+/g; function createInterpolationFromStringOutputRange( config: InterpolationConfigType, ): (input: number) => string { - var outputRange: Array = (config.outputRange: any); + let outputRange: Array = (config.outputRange: any); invariant(outputRange.length >= 2, 'Bad output range'); outputRange = outputRange.map(colorToRgba); checkPattern(outputRange); @@ -206,7 +212,7 @@ function createInterpolationFromStringOutputRange( /* $FlowFixMe(>=0.18.0): `outputRange[0].match()` can return `null`. Need to * guard against this possibility. */ - var outputRanges = outputRange[0].match(stringShapeRegex).map(() => []); + const outputRanges = outputRange[0].match(stringShapeRegex).map(() => []); outputRange.forEach(value => { /* $FlowFixMe(>=0.18.0): `value.match()` can return `null`. Need to guard * against this possibility. @@ -219,25 +225,28 @@ function createInterpolationFromStringOutputRange( /* $FlowFixMe(>=0.18.0): `outputRange[0].match()` can return `null`. Need to * guard against this possibility. */ - var interpolations = outputRange[0].match(stringShapeRegex).map((value, i) => { - return Interpolation.create({ - ...config, - outputRange: outputRanges[i], + const interpolations = outputRange[0] + .match(stringShapeRegex) + .map((value, i) => { + return createInterpolation({ + ...config, + outputRange: outputRanges[i], + }); }); - }); // rgba requires that the r,g,b are integers.... so we want to round them, but we *dont* want to // round the opacity (4th column). const shouldRound = isRgbOrRgba(outputRange[0]); - return (input) => { - var i = 0; + return input => { + let i = 0; // 'rgba(0, 100, 200, 0)' // -> // 'rgba(${interpolations[0](input)}, ${interpolations[1](input)}, ...' return outputRange[0].replace(stringShapeRegex, () => { const val = +interpolations[i++](input); - const rounded = shouldRound && i < 4 ? Math.round(val) : Math.round(val * 1000) / 1000; + const rounded = + shouldRound && i < 4 ? Math.round(val) : Math.round(val * 1000) / 1000; return String(rounded); }); }; @@ -248,8 +257,8 @@ function isRgbOrRgba(range) { } function checkPattern(arr: Array) { - var pattern = arr[0].replace(stringShapeRegex, ''); - for (var i = 1; i < arr.length; ++i) { + const pattern = arr[0].replace(stringShapeRegex, ''); + for (let i = 1; i < arr.length; ++i) { invariant( pattern === arr[i].replace(stringShapeRegex, ''), 'invalid pattern ' + arr[0] + ' and ' + arr[i], @@ -258,7 +267,8 @@ function checkPattern(arr: Array) { } function findRange(input: number, inputRange: Array) { - for (var i = 1; i < inputRange.length - 1; ++i) { + let i; + for (i = 1; i < inputRange.length - 1; ++i) { if (inputRange[i] >= input) { break; } @@ -268,7 +278,7 @@ function findRange(input: number, inputRange: Array) { function checkValidInputRange(arr: Array) { invariant(arr.length >= 2, 'inputRange must have at least 2 elements'); - for (var i = 1; i < arr.length; ++i) { + for (let i = 1; i < arr.length; ++i) { invariant( arr[i] >= arr[i - 1], /* $FlowFixMe(>=0.13.0) - In the addition expression below this comment, @@ -277,7 +287,7 @@ function checkValidInputRange(arr: Array) { * mean this implicit string conversion, you can do something like * String(myThing) */ - 'inputRange must be monotonically increasing ' + arr + 'inputRange must be monotonically increasing ' + arr, ); } } @@ -292,8 +302,86 @@ function checkInfiniteRange(name: string, arr: Array) { * this implicit string conversion, you can do something like * String(myThing) */ - name + 'cannot be ]-infinity;+infinity[ ' + arr + name + 'cannot be ]-infinity;+infinity[ ' + arr, ); } -module.exports = Interpolation; +class AnimatedInterpolation extends AnimatedWithChildren { + // Export for testing. + static __createInterpolation = createInterpolation; + + _parent: AnimatedNode; + _config: InterpolationConfigType; + _interpolation: (input: number) => number | string; + + constructor(parent: AnimatedNode, config: InterpolationConfigType) { + super(); + this._parent = parent; + this._config = config; + this._interpolation = createInterpolation(config); + } + + __makeNative() { + this._parent.__makeNative(); + super.__makeNative(); + } + + __getValue(): number | string { + const 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, config); + } + + __attach(): void { + this._parent.__addChild(this); + } + + __detach(): void { + this._parent.__removeChild(this); + super.__detach(); + } + + __transformDataType(range: Array) { + // Change the string array type to number array + // So we can reuse the same logic in iOS and Android platform + return range.map(function(value) { + if (typeof value !== 'string') { + return value; + } + if (/deg$/.test(value)) { + const degrees = parseFloat(value) || 0; + const radians = degrees * Math.PI / 180.0; + return radians; + } else { + // Assume radians + return parseFloat(value) || 0; + } + }); + } + + __getNativeConfig(): any { + if (__DEV__) { + NativeAnimatedHelper.validateInterpolation(this._config); + } + + return { + inputRange: this._config.inputRange, + // Only the `outputRange` can contain strings so we don't need to tranform `inputRange` here + outputRange: this.__transformDataType(this._config.outputRange), + extrapolateLeft: + this._config.extrapolateLeft || this._config.extrapolate || 'extend', + extrapolateRight: + this._config.extrapolateRight || this._config.extrapolate || 'extend', + type: 'interpolation', + }; + } +} + +module.exports = AnimatedInterpolation; diff --git a/Libraries/Animated/src/nodes/AnimatedModulo.js b/Libraries/Animated/src/nodes/AnimatedModulo.js new file mode 100644 index 00000000000000..0d38862073e37b --- /dev/null +++ b/Libraries/Animated/src/nodes/AnimatedModulo.js @@ -0,0 +1,63 @@ +/** + * 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. + * + * @flow + * @format + */ +'use strict'; + +const AnimatedInterpolation = require('./AnimatedInterpolation'); +const AnimatedNode = require('./AnimatedNode'); +const AnimatedWithChildren = require('./AnimatedWithChildren'); + +import type {InterpolationConfigType} from './AnimatedInterpolation'; + +class AnimatedModulo extends AnimatedWithChildren { + _a: AnimatedNode; + _modulus: number; + + constructor(a: AnimatedNode, modulus: number) { + super(); + this._a = a; + this._modulus = modulus; + } + + __makeNative() { + this._a.__makeNative(); + super.__makeNative(); + } + + __getValue(): number { + return ( + (this._a.__getValue() % this._modulus + this._modulus) % this._modulus + ); + } + + interpolate(config: InterpolationConfigType): AnimatedInterpolation { + return new AnimatedInterpolation(this, config); + } + + __attach(): void { + this._a.__addChild(this); + } + + __detach(): void { + this._a.__removeChild(this); + super.__detach(); + } + + __getNativeConfig(): any { + return { + type: 'modulus', + input: this._a.__getNativeTag(), + modulus: this._modulus, + }; + } +} + +module.exports = AnimatedModulo; diff --git a/Libraries/Animated/src/nodes/AnimatedMultiplication.js b/Libraries/Animated/src/nodes/AnimatedMultiplication.js new file mode 100644 index 00000000000000..dbc5eff7f67063 --- /dev/null +++ b/Libraries/Animated/src/nodes/AnimatedMultiplication.js @@ -0,0 +1,64 @@ +/** + * 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. + * + * @flow + * @format + */ +'use strict'; + +const AnimatedInterpolation = require('./AnimatedInterpolation'); +const AnimatedNode = require('./AnimatedNode'); +const AnimatedValue = require('./AnimatedValue'); +const AnimatedWithChildren = require('./AnimatedWithChildren'); + +import type {InterpolationConfigType} from './AnimatedInterpolation'; + +class AnimatedMultiplication extends AnimatedWithChildren { + _a: AnimatedNode; + _b: AnimatedNode; + + constructor(a: AnimatedNode | number, b: AnimatedNode | number) { + super(); + this._a = typeof a === 'number' ? new AnimatedValue(a) : a; + this._b = typeof b === 'number' ? new AnimatedValue(b) : b; + } + + __makeNative() { + this._a.__makeNative(); + this._b.__makeNative(); + super.__makeNative(); + } + + __getValue(): number { + return this._a.__getValue() * this._b.__getValue(); + } + + interpolate(config: InterpolationConfigType): AnimatedInterpolation { + return new AnimatedInterpolation(this, config); + } + + __attach(): void { + this._a.__addChild(this); + this._b.__addChild(this); + } + + __detach(): void { + this._a.__removeChild(this); + this._b.__removeChild(this); + super.__detach(); + } + + __getNativeConfig(): any { + return { + type: 'multiplication', + input: [this._a.__getNativeTag(), this._b.__getNativeTag()], + }; + } +} + +module.exports = AnimatedMultiplication; diff --git a/Libraries/Animated/src/nodes/AnimatedNode.js b/Libraries/Animated/src/nodes/AnimatedNode.js new file mode 100644 index 00000000000000..2c09869ed6d64c --- /dev/null +++ b/Libraries/Animated/src/nodes/AnimatedNode.js @@ -0,0 +1,72 @@ +/** + * 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. + * + * @flow + * @format + */ +'use strict'; + +const NativeAnimatedHelper = require('../NativeAnimatedHelper'); + +const invariant = require('fbjs/lib/invariant'); + +// Note(vjeux): this would be better as an interface but flow doesn't +// support them yet +class AnimatedNode { + __attach(): void {} + __detach(): void { + if (this.__isNative && this.__nativeTag != null) { + NativeAnimatedHelper.API.dropAnimatedNode(this.__nativeTag); + this.__nativeTag = undefined; + } + } + __getValue(): any {} + __getAnimatedValue(): any { + return this.__getValue(); + } + __addChild(child: AnimatedNode) {} + __removeChild(child: AnimatedNode) {} + __getChildren(): Array { + return []; + } + + /* Methods and props used by native Animated impl */ + __isNative: boolean; + __nativeTag: ?number; + __makeNative() { + if (!this.__isNative) { + throw new Error('This node cannot be made a "native" animated node'); + } + } + __getNativeTag(): ?number { + NativeAnimatedHelper.assertNativeAnimatedModule(); + invariant( + this.__isNative, + 'Attempt to get native tag from node not marked as "native"', + ); + if (this.__nativeTag == null) { + const nativeTag: ?number = NativeAnimatedHelper.generateNewNodeTag(); + NativeAnimatedHelper.API.createAnimatedNode( + nativeTag, + this.__getNativeConfig(), + ); + this.__nativeTag = nativeTag; + } + return this.__nativeTag; + } + __getNativeConfig(): Object { + throw new Error( + 'This JS animated node type cannot be used as native animated node', + ); + } + toJSON(): any { + return this.__getValue(); + } +} + +module.exports = AnimatedNode; diff --git a/Libraries/Animated/src/nodes/AnimatedProps.js b/Libraries/Animated/src/nodes/AnimatedProps.js new file mode 100644 index 00000000000000..daf3b12ef57ef2 --- /dev/null +++ b/Libraries/Animated/src/nodes/AnimatedProps.js @@ -0,0 +1,166 @@ +/** + * 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. + * + * @flow + * @format + */ +'use strict'; + +const {AnimatedEvent} = require('../AnimatedEvent'); +const AnimatedNode = require('./AnimatedNode'); +const AnimatedStyle = require('./AnimatedStyle'); +const NativeAnimatedHelper = require('../NativeAnimatedHelper'); +const ReactNative = require('ReactNative'); + +const invariant = require('fbjs/lib/invariant'); + +class AnimatedProps extends AnimatedNode { + _props: Object; + _animatedView: any; + _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 { + const props = {}; + for (const key in this._props) { + const value = this._props[key]; + if (value instanceof AnimatedNode) { + if (!value.__isNative || value instanceof AnimatedStyle) { + // We cannot use value of natively driven nodes this way as the value we have access from + // JS may not be up to date. + props[key] = value.__getValue(); + } + } else if (value instanceof AnimatedEvent) { + props[key] = value.__getHandler(); + } else { + props[key] = value; + } + } + return props; + } + + __getAnimatedValue(): Object { + const props = {}; + for (const key in this._props) { + const value = this._props[key]; + if (value instanceof AnimatedNode) { + props[key] = value.__getAnimatedValue(); + } + } + return props; + } + + __attach(): void { + for (const key in this._props) { + const value = this._props[key]; + if (value instanceof AnimatedNode) { + value.__addChild(this); + } + } + } + + __detach(): void { + if (this.__isNative && this._animatedView) { + this.__disconnectAnimatedView(); + } + for (const key in this._props) { + const value = this._props[key]; + if (value instanceof AnimatedNode) { + value.__removeChild(this); + } + } + super.__detach(); + } + + update(): void { + this._callback(); + } + + __makeNative(): void { + if (!this.__isNative) { + this.__isNative = true; + for (const key in this._props) { + const value = this._props[key]; + if (value instanceof AnimatedNode) { + value.__makeNative(); + } + } + if (this._animatedView) { + this.__connectAnimatedView(); + } + } + } + + setNativeView(animatedView: any): void { + if (this._animatedView === animatedView) { + return; + } + this._animatedView = animatedView; + if (this.__isNative) { + this.__connectAnimatedView(); + } + } + + __connectAnimatedView(): void { + invariant(this.__isNative, 'Expected node to be marked as "native"'); + const nativeViewTag: ?number = ReactNative.findNodeHandle( + this._animatedView, + ); + invariant( + nativeViewTag != null, + 'Unable to locate attached view in the native tree', + ); + NativeAnimatedHelper.API.connectAnimatedNodeToView( + this.__getNativeTag(), + nativeViewTag, + ); + } + + __disconnectAnimatedView(): void { + invariant(this.__isNative, 'Expected node to be marked as "native"'); + const nativeViewTag: ?number = ReactNative.findNodeHandle( + this._animatedView, + ); + invariant( + nativeViewTag != null, + 'Unable to locate attached view in the native tree', + ); + NativeAnimatedHelper.API.disconnectAnimatedNodeFromView( + this.__getNativeTag(), + nativeViewTag, + ); + } + + __getNativeConfig(): Object { + const propsConfig = {}; + for (const propKey in this._props) { + const value = this._props[propKey]; + if (value instanceof AnimatedNode) { + propsConfig[propKey] = value.__getNativeTag(); + } + } + return { + type: 'props', + props: propsConfig, + }; + } +} + +module.exports = AnimatedProps; diff --git a/Libraries/Animated/src/nodes/AnimatedStyle.js b/Libraries/Animated/src/nodes/AnimatedStyle.js new file mode 100644 index 00000000000000..b46ca3e2951ee7 --- /dev/null +++ b/Libraries/Animated/src/nodes/AnimatedStyle.js @@ -0,0 +1,126 @@ +/** + * 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. + * + * @flow + * @format + */ +'use strict'; + +const AnimatedNode = require('./AnimatedNode'); +const AnimatedTransform = require('./AnimatedTransform'); +const AnimatedWithChildren = require('./AnimatedWithChildren'); +const NativeAnimatedHelper = require('../NativeAnimatedHelper'); + +const flattenStyle = require('flattenStyle'); + +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; + } + + // Recursively get values for nested styles (like iOS's shadowOffset) + _walkStyleAndGetValues(style) { + const updatedStyle = {}; + for (const key in style) { + const value = style[key]; + if (value instanceof AnimatedNode) { + if (!value.__isNative) { + // We cannot use value of natively driven nodes this way as the value we have access from + // JS may not be up to date. + updatedStyle[key] = value.__getValue(); + } + } else if (value && !Array.isArray(value) && typeof value === 'object') { + // Support animating nested values (for example: shadowOffset.height) + updatedStyle[key] = this._walkStyleAndGetValues(value); + } else { + updatedStyle[key] = value; + } + } + return updatedStyle; + } + + __getValue(): Object { + return this._walkStyleAndGetValues(this._style); + } + + // Recursively get animated values for nested styles (like iOS's shadowOffset) + _walkStyleAndGetAnimatedValues(style) { + const updatedStyle = {}; + for (const key in style) { + const value = style[key]; + if (value instanceof AnimatedNode) { + updatedStyle[key] = value.__getAnimatedValue(); + } else if (value && !Array.isArray(value) && typeof value === 'object') { + // Support animating nested values (for example: shadowOffset.height) + updatedStyle[key] = this._walkStyleAndGetAnimatedValues(value); + } + } + return updatedStyle; + } + + __getAnimatedValue(): Object { + return this._walkStyleAndGetAnimatedValues(this._style); + } + + __attach(): void { + for (const key in this._style) { + const value = this._style[key]; + if (value instanceof AnimatedNode) { + value.__addChild(this); + } + } + } + + __detach(): void { + for (const key in this._style) { + const value = this._style[key]; + if (value instanceof AnimatedNode) { + value.__removeChild(this); + } + } + super.__detach(); + } + + __makeNative() { + super.__makeNative(); + for (const key in this._style) { + const value = this._style[key]; + if (value instanceof AnimatedNode) { + value.__makeNative(); + } + } + } + + __getNativeConfig(): Object { + const styleConfig = {}; + for (const styleKey in this._style) { + if (this._style[styleKey] instanceof AnimatedNode) { + styleConfig[styleKey] = this._style[styleKey].__getNativeTag(); + } + // Non-animated styles are set using `setNativeProps`, no need + // to pass those as a part of the node config + } + NativeAnimatedHelper.validateStyles(styleConfig); + return { + type: 'style', + style: styleConfig, + }; + } +} + +module.exports = AnimatedStyle; diff --git a/Libraries/Animated/src/nodes/AnimatedTracking.js b/Libraries/Animated/src/nodes/AnimatedTracking.js new file mode 100644 index 00000000000000..637252700c87d1 --- /dev/null +++ b/Libraries/Animated/src/nodes/AnimatedTracking.js @@ -0,0 +1,66 @@ +/** + * 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. + * + * @flow + * @format + */ +'use strict'; + +const AnimatedValue = require('./AnimatedValue'); +const AnimatedNode = require('./AnimatedNode'); + +import type {EndCallback} from '../animations/Animation'; + +class AnimatedTracking extends AnimatedNode { + _value: AnimatedValue; + _parent: AnimatedNode; + _callback: ?EndCallback; + _animationConfig: Object; + _animationClass: any; + + constructor( + value: AnimatedValue, + parent: AnimatedNode, + 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); + super.__detach(); + } + + update(): void { + this._value.animate( + new this._animationClass({ + ...this._animationConfig, + toValue: (this._animationConfig.toValue: any).__getValue(), + }), + this._callback, + ); + } +} + +module.exports = AnimatedTracking; diff --git a/Libraries/Animated/src/nodes/AnimatedTransform.js b/Libraries/Animated/src/nodes/AnimatedTransform.js new file mode 100644 index 00000000000000..befdbe4b2b50b7 --- /dev/null +++ b/Libraries/Animated/src/nodes/AnimatedTransform.js @@ -0,0 +1,122 @@ +/** + * 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. + * + * @flow + * @format + */ +'use strict'; + +const AnimatedNode = require('./AnimatedNode'); +const AnimatedWithChildren = require('./AnimatedWithChildren'); +const NativeAnimatedHelper = require('../NativeAnimatedHelper'); + +class AnimatedTransform extends AnimatedWithChildren { + _transforms: Array; + + constructor(transforms: Array) { + super(); + this._transforms = transforms; + } + + __makeNative() { + super.__makeNative(); + this._transforms.forEach(transform => { + for (const key in transform) { + const value = transform[key]; + if (value instanceof AnimatedNode) { + value.__makeNative(); + } + } + }); + } + + __getValue(): Array { + return this._transforms.map(transform => { + const result = {}; + for (const key in transform) { + const value = transform[key]; + if (value instanceof AnimatedNode) { + result[key] = value.__getValue(); + } else { + result[key] = value; + } + } + return result; + }); + } + + __getAnimatedValue(): Array { + return this._transforms.map(transform => { + const result = {}; + for (const key in transform) { + const value = transform[key]; + if (value instanceof AnimatedNode) { + result[key] = value.__getAnimatedValue(); + } else { + // All transform components needed to recompose matrix + result[key] = value; + } + } + return result; + }); + } + + __attach(): void { + this._transforms.forEach(transform => { + for (const key in transform) { + const value = transform[key]; + if (value instanceof AnimatedNode) { + value.__addChild(this); + } + } + }); + } + + __detach(): void { + this._transforms.forEach(transform => { + for (const key in transform) { + const value = transform[key]; + if (value instanceof AnimatedNode) { + value.__removeChild(this); + } + } + }); + super.__detach(); + } + + __getNativeConfig(): any { + const transConfigs = []; + + this._transforms.forEach(transform => { + for (const key in transform) { + const value = transform[key]; + if (value instanceof AnimatedNode) { + transConfigs.push({ + type: 'animated', + property: key, + nodeTag: value.__getNativeTag(), + }); + } else { + transConfigs.push({ + type: 'static', + property: key, + value, + }); + } + } + }); + + NativeAnimatedHelper.validateTransform(transConfigs); + return { + type: 'transform', + transforms: transConfigs, + }; + } +} + +module.exports = AnimatedTransform; diff --git a/Libraries/Animated/src/nodes/AnimatedValue.js b/Libraries/Animated/src/nodes/AnimatedValue.js new file mode 100644 index 00000000000000..3ca4829180c729 --- /dev/null +++ b/Libraries/Animated/src/nodes/AnimatedValue.js @@ -0,0 +1,308 @@ +/** + * 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. + * + * @flow + * @format + */ +'use strict'; + +const AnimatedInterpolation = require('./AnimatedInterpolation'); +const AnimatedNode = require('./AnimatedNode'); +const AnimatedWithChildren = require('./AnimatedWithChildren'); +const InteractionManager = require('InteractionManager'); +const NativeAnimatedHelper = require('../NativeAnimatedHelper'); + +import type Animation, {EndCallback} from '../animations/Animation'; +import type {InterpolationConfigType} from './AnimatedInterpolation'; + +const NativeAnimatedAPI = NativeAnimatedHelper.API; + +type ValueListenerCallback = (state: {value: number}) => void; + +let _uniqueId = 1; + +/** + * 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 { + const animatedStyles = new Set(); + function findAnimatedStyles(node) { + if (typeof node.update === 'function') { + animatedStyles.add(node); + } else { + node.__getChildren().forEach(findAnimatedStyles); + } + } + findAnimatedStyles(rootNode); + /* $FlowFixMe */ + animatedStyles.forEach(animatedStyle => animatedStyle.update()); +} + +/** + * Standard value for driving animations. One `Animated.Value` can drive + * multiple properties in a synchronized fashion, but can only be driven by one + * mechanism at a time. Using a new mechanism (e.g. starting a new animation, + * or calling `setValue`) will stop any previous ones. + */ +class AnimatedValue extends AnimatedWithChildren { + _value: number; + _startingValue: number; + _offset: number; + _animation: ?Animation; + _tracking: ?AnimatedNode; + _listeners: {[key: string]: ValueListenerCallback}; + __nativeAnimatedValueListener: ?any; + + constructor(value: number) { + super(); + this._startingValue = this._value = value; + this._offset = 0; + this._animation = null; + this._listeners = {}; + } + + __detach() { + this.stopAnimation(); + super.__detach(); + } + + __getValue(): number { + return this._value + this._offset; + } + + __makeNative() { + super.__makeNative(); + + if (Object.keys(this._listeners).length) { + this._startListeningToNativeValueUpdates(); + } + } + + /** + * Directly set the value. This will stop any animations running on the value + * and update all the bound properties. + */ + setValue(value: number): void { + if (this._animation) { + this._animation.stop(); + this._animation = null; + } + this._updateValue( + value, + !this.__isNative /* don't perform a flush for natively driven values */, + ); + if (this.__isNative) { + NativeAnimatedAPI.setAnimatedNodeValue(this.__getNativeTag(), value); + } + } + + /** + * Sets an offset that is applied on top of whatever value is set, whether via + * `setValue`, an animation, or `Animated.event`. Useful for compensating + * things like the start of a pan gesture. + */ + setOffset(offset: number): void { + this._offset = offset; + if (this.__isNative) { + NativeAnimatedAPI.setAnimatedNodeOffset(this.__getNativeTag(), offset); + } + } + + /** + * Merges the offset value into the base value and resets the offset to zero. + * The final output of the value is unchanged. + */ + flattenOffset(): void { + this._value += this._offset; + this._offset = 0; + if (this.__isNative) { + NativeAnimatedAPI.flattenAnimatedNodeOffset(this.__getNativeTag()); + } + } + + /** + * Sets the offset value to the base value, and resets the base value to zero. + * The final output of the value is unchanged. + */ + extractOffset(): void { + this._offset += this._value; + this._value = 0; + if (this.__isNative) { + NativeAnimatedAPI.extractAnimatedNodeOffset(this.__getNativeTag()); + } + } + + /** + * Adds an asynchronous listener to the value so you can observe updates from + * animations. This is useful because there is no way to + * synchronously read the value because it might be driven natively. + */ + addListener(callback: ValueListenerCallback): string { + const id = String(_uniqueId++); + this._listeners[id] = callback; + if (this.__isNative) { + this._startListeningToNativeValueUpdates(); + } + return id; + } + + removeListener(id: string): void { + delete this._listeners[id]; + if (this.__isNative && Object.keys(this._listeners).length === 0) { + this._stopListeningForNativeValueUpdates(); + } + } + + removeAllListeners(): void { + this._listeners = {}; + if (this.__isNative) { + this._stopListeningForNativeValueUpdates(); + } + } + + _startListeningToNativeValueUpdates() { + if (this.__nativeAnimatedValueListener) { + return; + } + + NativeAnimatedAPI.startListeningToAnimatedNodeValue(this.__getNativeTag()); + this.__nativeAnimatedValueListener = NativeAnimatedHelper.nativeEventEmitter.addListener( + 'onAnimatedValueUpdate', + data => { + if (data.tag !== this.__getNativeTag()) { + return; + } + this._updateValue(data.value, false /* flush */); + }, + ); + } + + _stopListeningForNativeValueUpdates() { + if (!this.__nativeAnimatedValueListener) { + return; + } + + this.__nativeAnimatedValueListener.remove(); + this.__nativeAnimatedValueListener = null; + NativeAnimatedAPI.stopListeningToAnimatedNodeValue(this.__getNativeTag()); + } + + /** + * Stops any running animation or tracking. `callback` is invoked with the + * final value after stopping the animation, which is useful for updating + * state to match the animation position with layout. + */ + stopAnimation(callback?: ?(value: number) => void): void { + this.stopTracking(); + this._animation && this._animation.stop(); + this._animation = null; + callback && callback(this.__getValue()); + } + + /** + * Stops any animation and resets the value to its original + */ + resetAnimation(callback?: ?(value: number) => void): void { + this.stopAnimation(callback); + this._value = this._startingValue; + } + + /** + * Interpolates the value before updating the property, e.g. mapping 0-1 to + * 0-10. + */ + interpolate(config: InterpolationConfigType): AnimatedInterpolation { + return new AnimatedInterpolation(this, config); + } + + /** + * Typically only used internally, but could be used by a custom Animation + * class. + */ + animate(animation: Animation, callback: ?EndCallback): void { + let handle = null; + if (animation.__isInteraction) { + handle = InteractionManager.createInteractionHandle(); + } + const previousAnimation = this._animation; + this._animation && this._animation.stop(); + this._animation = animation; + animation.start( + this._value, + value => { + // Natively driven animations will never call into that callback, therefore we can always + // pass flush = true to allow the updated value to propagate to native with setNativeProps + this._updateValue(value, true /* flush */); + }, + result => { + this._animation = null; + if (handle !== null) { + InteractionManager.clearInteractionHandle(handle); + } + callback && callback(result); + }, + previousAnimation, + this, + ); + } + + /** + * Typically only used internally. + */ + stopTracking(): void { + this._tracking && this._tracking.__detach(); + this._tracking = null; + } + + /** + * Typically only used internally. + */ + track(tracking: AnimatedNode): void { + this.stopTracking(); + this._tracking = tracking; + } + + _updateValue(value: number, flush: boolean): void { + this._value = value; + if (flush) { + _flush(this); + } + for (const key in this._listeners) { + this._listeners[key]({value: this.__getValue()}); + } + } + + __getNativeConfig(): Object { + return { + type: 'value', + value: this._value, + offset: this._offset, + }; + } +} + +module.exports = AnimatedValue; diff --git a/Libraries/Animated/src/nodes/AnimatedValueXY.js b/Libraries/Animated/src/nodes/AnimatedValueXY.js new file mode 100644 index 00000000000000..71d3817ee70d96 --- /dev/null +++ b/Libraries/Animated/src/nodes/AnimatedValueXY.js @@ -0,0 +1,179 @@ +/** + * 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. + * + * @flow + * @format + */ +'use strict'; + +const AnimatedValue = require('./AnimatedValue'); +const AnimatedWithChildren = require('./AnimatedWithChildren'); + +const invariant = require('fbjs/lib/invariant'); + +type ValueXYListenerCallback = (value: {x: number, y: number}) => void; + +let _uniqueId = 1; + +/** + * 2D Value for driving 2D animations, such as pan gestures. Almost identical + * API to normal `Animated.Value`, but multiplexed. Contains two regular + * `Animated.Value`s under the hood. + * + * #### Example + * + *```javascript + * class DraggableView extends React.Component { + * constructor(props) { + * super(props); + * this.state = { + * pan: new Animated.ValueXY(), // inits to zero + * }; + * this.state.panResponder = PanResponder.create({ + * onStartShouldSetPanResponder: () => true, + * onPanResponderMove: Animated.event([null, { + * dx: this.state.pan.x, // x,y are Animated.Value + * dy: this.state.pan.y, + * }]), + * onPanResponderRelease: () => { + * Animated.spring( + * this.state.pan, // Auto-multiplexed + * {toValue: {x: 0, y: 0}} // Back to zero + * ).start(); + * }, + * }); + * } + * render() { + * return ( + * + * {this.props.children} + * + * ); + * } + * } + *``` + */ +class AnimatedValueXY extends AnimatedWithChildren { + x: AnimatedValue; + y: AnimatedValue; + _listeners: {[key: string]: {x: string, y: string}}; + + constructor( + valueIn?: ?{x: number | AnimatedValue, y: number | AnimatedValue}, + ) { + super(); + const 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(); + } + + extractOffset(): void { + this.x.extractOffset(); + this.y.extractOffset(); + } + + __getValue(): {x: number, y: number} { + return { + x: this.x.__getValue(), + y: this.y.__getValue(), + }; + } + + resetAnimation(callback?: (value: {x: number, y: number}) => void): void { + this.x.resetAnimation(); + this.y.resetAnimation(); + callback && callback(this.__getValue()); + } + + stopAnimation(callback?: (value: {x: number, y: number}) => void): void { + this.x.stopAnimation(); + this.y.stopAnimation(); + callback && callback(this.__getValue()); + } + + addListener(callback: ValueXYListenerCallback): string { + const id = String(_uniqueId++); + const 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]; + } + + removeAllListeners(): void { + this.x.removeAllListeners(); + this.y.removeAllListeners(); + this._listeners = {}; + } + + /** + * Converts `{x, y}` into `{left, top}` for use in style, e.g. + * + *```javascript + * style={this.state.anim.getLayout()} + *``` + */ + getLayout(): {[key: string]: AnimatedValue} { + return { + left: this.x, + top: this.y, + }; + } + + /** + * Converts `{x, y}` into a useable translation transform, e.g. + * + *```javascript + * style={{ + * transform: this.state.anim.getTranslateTransform() + * }} + *``` + */ + getTranslateTransform(): Array<{[key: string]: AnimatedValue}> { + return [{translateX: this.x}, {translateY: this.y}]; + } +} + +module.exports = AnimatedValueXY; diff --git a/Libraries/Animated/src/nodes/AnimatedWithChildren.js b/Libraries/Animated/src/nodes/AnimatedWithChildren.js new file mode 100644 index 00000000000000..0a3b72be4b8a1c --- /dev/null +++ b/Libraries/Animated/src/nodes/AnimatedWithChildren.js @@ -0,0 +1,76 @@ +/** + * 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. + * + * @flow + * @format + */ +'use strict'; + +const AnimatedNode = require('./AnimatedNode'); +const NativeAnimatedHelper = require('../NativeAnimatedHelper'); + +class AnimatedWithChildren extends AnimatedNode { + _children: Array; + + constructor() { + super(); + this._children = []; + } + + __makeNative() { + if (!this.__isNative) { + this.__isNative = true; + for (const child of this._children) { + child.__makeNative(); + NativeAnimatedHelper.API.connectAnimatedNodes( + this.__getNativeTag(), + child.__getNativeTag(), + ); + } + } + } + + __addChild(child: AnimatedNode): void { + if (this._children.length === 0) { + this.__attach(); + } + this._children.push(child); + if (this.__isNative) { + // Only accept "native" animated nodes as children + child.__makeNative(); + NativeAnimatedHelper.API.connectAnimatedNodes( + this.__getNativeTag(), + child.__getNativeTag(), + ); + } + } + + __removeChild(child: AnimatedNode): void { + const index = this._children.indexOf(child); + if (index === -1) { + console.warn("Trying to remove a child that doesn't exist"); + return; + } + if (this.__isNative && child.__isNative) { + NativeAnimatedHelper.API.disconnectAnimatedNodes( + this.__getNativeTag(), + child.__getNativeTag(), + ); + } + this._children.splice(index, 1); + if (this._children.length === 0) { + this.__detach(); + } + } + + __getChildren(): Array { + return this._children; + } +} + +module.exports = AnimatedWithChildren;