diff --git a/change/react-native-windows-2ce4c28a-fe80-49e9-81f1-ef400c97469f.json b/change/react-native-windows-2ce4c28a-fe80-49e9-81f1-ef400c97469f.json new file mode 100644 index 00000000000..4a74841f157 --- /dev/null +++ b/change/react-native-windows-2ce4c28a-fe80-49e9-81f1-ef400c97469f.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Animated JS changes to support platform config for animation drivers and nodes", + "packageName": "react-native-windows", + "email": "erozell@outlook.com", + "dependentChangeType": "patch" +} diff --git a/vnext/.flowconfig b/vnext/.flowconfig index fe782d97f3b..af93421caa8 100644 --- a/vnext/.flowconfig +++ b/vnext/.flowconfig @@ -32,6 +32,29 @@ /Libraries/Pressability/Pressability.js /Libraries/Text/TextNativeComponent.js /Libraries/Types/CoreEventTypes.js +/Libraries/Animated/AnimatedPlatformConfig.js + +; These Animated overrides will be upstreamed +/Libraries/Animated/animations/Animation.js +/Libraries/Animated/animations/DecayAnimation.js +/Libraries/Animated/animations/SpringAnimation.js +/Libraries/Animated/animations/TimingAnimation.js +/Libraries/Animated/nodes/AnimatedAddition.js +/Libraries/Animated/nodes/AnimatedDiffClamp.js +/Libraries/Animated/nodes/AnimatedDivision.js +/Libraries/Animated/nodes/AnimatedInterpolation.js +/Libraries/Animated/nodes/AnimatedModulo.js +/Libraries/Animated/nodes/AnimatedMultiplication.js +/Libraries/Animated/nodes/AnimatedNode.js +/Libraries/Animated/nodes/AnimatedProps.js +/Libraries/Animated/nodes/AnimatedStyle.js +/Libraries/Animated/nodes/AnimatedSubtraction.js +/Libraries/Animated/nodes/AnimatedTracking.js +/Libraries/Animated/nodes/AnimatedTransform.js +/Libraries/Animated/nodes/AnimatedWithChildren.js +/Libraries/Animated/AnimatedEvent.js +/Libraries/Animated/AnimatedImplementation.js +/Libraries/Animated/AnimatedMock.js ; Ignore react-native files in node_modules since they are copied into project root .*/node_modules/react-native/.* diff --git a/vnext/src/Libraries/Animated/AnimatedEvent.windows.js b/vnext/src/Libraries/Animated/AnimatedEvent.windows.js new file mode 100644 index 00000000000..71d0a0d1598 --- /dev/null +++ b/vnext/src/Libraries/Animated/AnimatedEvent.windows.js @@ -0,0 +1,263 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +const {unstable_getDefaultPlatformConfig} = require('./AnimatedPlatformConfig'); +const AnimatedValue = require('./nodes/AnimatedValue'); +const AnimatedValueXY = require('./nodes/AnimatedValueXY'); +const NativeAnimatedHelper = require('./NativeAnimatedHelper'); +const ReactNative = require('../Renderer/shims/ReactNative'); + +const invariant = require('invariant'); + +const {shouldUseNativeDriver} = require('./NativeAnimatedHelper'); + +import type {PlatformConfig} from './AnimatedPlatformConfig'; + +export type Mapping = + | {[key: string]: Mapping, ...} + | AnimatedValue + | AnimatedValueXY; +export type EventConfig = { + listener?: ?Function, + useNativeDriver: boolean, + platformConfig?: PlatformConfig, +}; + +function attachNativeEvent( + viewRef: any, + eventName: string, + argMapping: $ReadOnlyArray, + platformConfig: ?PlatformConfig, +): {detach: () => void} { + // 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(platformConfig ?? unstable_getDefaultPlatformConfig()); + + eventMappings.push({ + nativeEventPath: path, + animatedValueTag: value.__getNativeTag(), + }); + } else if (value instanceof AnimatedValueXY) { + traverse(value.x, path.concat('x')); + traverse(value.y, path.concat('y')); + } 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); + if (viewTag != null) { + eventMappings.forEach((mapping) => { + NativeAnimatedHelper.API.addAnimatedEventToView( + viewTag, + eventName, + mapping, + ); + }); + } + + return { + detach() { + if (viewTag != null) { + eventMappings.forEach((mapping) => { + NativeAnimatedHelper.API.removeAnimatedEventFromView( + viewTag, + eventName, + // $FlowFixMe[incompatible-call] + mapping.animatedValueTag, + ); + }); + } + }, + }; +} + +function validateMapping(argMapping, args) { + const validate = (recMapping, recEvt, key) => { + if (recMapping instanceof AnimatedValue) { + invariant( + typeof recEvt === 'number', + 'Bad mapping of event key ' + + key + + ', should be number but got ' + + typeof recEvt, + ); + return; + } + if (recMapping instanceof AnimatedValueXY) { + invariant( + typeof recEvt.x === 'number' && typeof recEvt.y === 'number', + 'Bad mapping of event key ' + key + ', should be XY but got ' + recEvt, + ); + return; + } + 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) { + validate(recMapping[mappingKey], recEvt[mappingKey], mappingKey); + } + }; + + invariant( + args.length >= argMapping.length, + 'Event has less arguments than mapping', + ); + argMapping.forEach((mapping, idx) => { + validate(mapping, args[idx], 'arg' + idx); + }); +} + +class AnimatedEvent { + _argMapping: $ReadOnlyArray; + _listeners: Array = []; + _callListeners: Function; + _attachedEvent: ?{detach: () => void, ...}; + __isNative: boolean; + __platformConfig: ?PlatformConfig; + + constructor(argMapping: $ReadOnlyArray, config: EventConfig) { + this._argMapping = argMapping; + + if (config == null) { + console.warn('Animated.event now requires a second argument for options'); + config = {useNativeDriver: false}; + } + + if (config.listener) { + this.__addListener(config.listener); + } + this._callListeners = this._callListeners.bind(this); + this._attachedEvent = null; + this.__isNative = shouldUseNativeDriver(config); + this.__platformConfig = config.platformConfig; + } + + __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, + this.__platformConfig, + ); + } + + __detach(viewTag: any, eventName: string) { + invariant( + this.__isNative, + 'Only native driven events need to be detached.', + ); + + this._attachedEvent && this._attachedEvent.detach(); + } + + __getHandler(): any | ((...args: any) => void) { + if (this.__isNative) { + if (__DEV__) { + let validatedMapping = false; + return (...args: any) => { + if (!validatedMapping) { + validateMapping(this._argMapping, args); + validatedMapping = true; + } + this._callListeners(...args); + }; + } else { + return this._callListeners; + } + } + + let validatedMapping = false; + return (...args: any) => { + if (__DEV__ && !validatedMapping) { + validateMapping(this._argMapping, args); + validatedMapping = true; + } + + const traverse = (recMapping, recEvt) => { + if (recMapping instanceof AnimatedValue) { + if (typeof recEvt === 'number') { + recMapping.setValue(recEvt); + } + } else if (recMapping instanceof AnimatedValueXY) { + if (typeof recEvt === 'object') { + traverse(recMapping.x, recEvt.x); + traverse(recMapping.y, recEvt.y); + } + } else if (typeof recMapping === 'object') { + for (const mappingKey in recMapping) { + /* $FlowFixMe[prop-missing] (>=0.120.0) This comment suppresses an + * error found when Flow v0.120 was deployed. To see the error, + * delete this comment and run Flow. */ + traverse(recMapping[mappingKey], recEvt[mappingKey]); + } + } + }; + this._argMapping.forEach((mapping, idx) => { + traverse(mapping, args[idx]); + }); + + this._callListeners(...args); + }; + } + + _callListeners(...args: any) { + this._listeners.forEach((listener) => listener(...args)); + } +} + +module.exports = {AnimatedEvent, attachNativeEvent}; diff --git a/vnext/src/Libraries/Animated/AnimatedImplementation.windows.js b/vnext/src/Libraries/Animated/AnimatedImplementation.windows.js new file mode 100644 index 00000000000..c0f94ce768b --- /dev/null +++ b/vnext/src/Libraries/Animated/AnimatedImplementation.windows.js @@ -0,0 +1,719 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +const {AnimatedEvent, attachNativeEvent} = require('./AnimatedEvent'); +const { + unstable_getDefaultPlatformConfig, + unstable_setDefaultPlatformConfig, +} = require('./AnimatedPlatformConfig'); +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 AnimatedSubtraction = require('./nodes/AnimatedSubtraction'); +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'; + +export type CompositeAnimation = { + start: (callback?: ?EndCallback) => void, + stop: () => void, + reset: () => void, + _startNativeLoop: (iterations?: number) => void, + _isUsingNativeDriver: () => boolean, + ... +}; + +const add = function ( + a: AnimatedNode | number, + b: AnimatedNode | number, +): AnimatedAddition { + return new AnimatedAddition(a, b); +}; + +const subtract = function ( + a: AnimatedNode | number, + b: AnimatedNode | number, +): AnimatedSubtraction { + return new AnimatedSubtraction(a, b); +}; + +const divide = function ( + a: AnimatedNode | number, + b: AnimatedNode | number, +): AnimatedDivision { + return new AnimatedDivision(a, b); +}; + +const multiply = function ( + a: AnimatedNode | number, + b: AnimatedNode | number, +): AnimatedMultiplication { + return new AnimatedMultiplication(a, b); +}; + +const modulo = function (a: AnimatedNode, modulus: number): AnimatedModulo { + return new AnimatedModulo(a, modulus); +}; + +const diffClamp = function ( + a: AnimatedNode, + min: number, + max: number, +): AnimatedDiffClamp { + return new AnimatedDiffClamp(a, min, max); +}; + +const _combineCallbacks = function ( + callback: ?EndCallback, + config: {...AnimationConfig, ...}, +) { + if (callback && config.onComplete) { + return (...args) => { + config.onComplete && config.onComplete(...args); + callback && callback(...args); + }; + } else { + return callback || config.onComplete; + } +}; + +const maybeVectorAnim = function ( + value: AnimatedValue | AnimatedValueXY, + config: Object, + anim: (value: AnimatedValue, config: Object) => CompositeAnimation, +): ?CompositeAnimation { + if (value instanceof AnimatedValueXY) { + 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; + } + } + 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}); + } + return null; +}; + +const spring = function ( + value: AnimatedValue | AnimatedValueXY, + config: SpringAnimationConfig, +): CompositeAnimation { + const start = function ( + animatedValue: AnimatedValue | AnimatedValueXY, + configuration: SpringAnimationConfig, + 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); + } + }; + return ( + maybeVectorAnim(value, config, spring) || { + start: function (callback?: ?EndCallback): void { + start(value, config, callback); + }, + + stop: function (): void { + value.stopAnimation(); + }, + + reset: function (): void { + value.resetAnimation(); + }, + + _startNativeLoop: function (iterations?: number): void { + const singleConfig = {...config, iterations}; + start(value, singleConfig); + }, + + _isUsingNativeDriver: function (): boolean { + return config.useNativeDriver || false; + }, + } + ); +}; + +const timing = function ( + value: AnimatedValue | AnimatedValueXY, + config: TimingAnimationConfig, +): CompositeAnimation { + const start = function ( + animatedValue: AnimatedValue | AnimatedValueXY, + configuration: TimingAnimationConfig, + 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); + } + }; + + return ( + maybeVectorAnim(value, config, timing) || { + start: function (callback?: ?EndCallback): void { + start(value, config, callback); + }, + + stop: function (): void { + value.stopAnimation(); + }, + + reset: function (): void { + value.resetAnimation(); + }, + + _startNativeLoop: function (iterations?: number): void { + const singleConfig = {...config, iterations}; + start(value, singleConfig); + }, + + _isUsingNativeDriver: function (): boolean { + return config.useNativeDriver || false; + }, + } + ); +}; + +const decay = function ( + value: AnimatedValue | AnimatedValueXY, + config: DecayAnimationConfig, +): CompositeAnimation { + const start = function ( + animatedValue: AnimatedValue | AnimatedValueXY, + configuration: DecayAnimationConfig, + 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); + }, + + stop: function (): void { + value.stopAnimation(); + }, + + reset: function (): void { + value.resetAnimation(); + }, + + _startNativeLoop: function (iterations?: number): void { + const singleConfig = {...config, iterations}; + start(value, singleConfig); + }, + + _isUsingNativeDriver: function (): boolean { + return config.useNativeDriver || false; + }, + } + ); +}; + +const sequence = function ( + animations: Array, +): CompositeAnimation { + let current = 0; + return { + start: function (callback?: ?EndCallback) { + const onComplete = function (result) { + if (!result.finished) { + callback && callback(result); + return; + } + + current++; + + if (current === animations.length) { + callback && callback(result); + return; + } + + animations[current].start(onComplete); + }; + + if (animations.length === 0) { + callback && callback({finished: true}); + } else { + animations[current].start(onComplete); + } + }, + + stop: function () { + if (current < animations.length) { + animations[current].stop(); + } + }, + + reset: function () { + animations.forEach((animation, idx) => { + if (idx <= current) { + animation.reset(); + } + }); + current = 0; + }, + + _startNativeLoop: function () { + throw new Error( + 'Loops run using the native driver cannot contain Animated.sequence animations', + ); + }, + + _isUsingNativeDriver: function (): boolean { + return false; + }, + }; +}; + +type ParallelConfig = { + // If one is stopped, stop all. default: true + stopTogether?: boolean, + ... +}; +const parallel = function ( + animations: Array, + config?: ?ParallelConfig, +): CompositeAnimation { + let doneCount = 0; + // Make sure we only call stop() at most once for each animation + const hasEnded = {}; + const stopTogether = !(config && config.stopTogether === false); + + const result = { + start: function (callback?: ?EndCallback) { + if (doneCount === animations.length) { + callback && callback({finished: true}); + return; + } + + animations.forEach((animation, idx) => { + const cb = function (endResult) { + hasEnded[idx] = true; + doneCount++; + if (doneCount === animations.length) { + doneCount = 0; + callback && callback(endResult); + return; + } + + if (!endResult.finished && stopTogether) { + result.stop(); + } + }; + + if (!animation) { + cb({finished: true}); + } else { + animation.start(cb); + } + }); + }, + + stop: function (): void { + animations.forEach((animation, idx) => { + !hasEnded[idx] && animation.stop(); + hasEnded[idx] = true; + }); + }, + + reset: function (): void { + animations.forEach((animation, idx) => { + animation.reset(); + hasEnded[idx] = false; + doneCount = 0; + }); + }, + + _startNativeLoop: function () { + throw new Error( + 'Loops run using the native driver cannot contain Animated.parallel animations', + ); + }, + + _isUsingNativeDriver: function (): boolean { + return false; + }, + }; + + return result; +}; + +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, + useNativeDriver: false, + }); +}; + +const stagger = function ( + time: number, + animations: Array, +): CompositeAnimation { + return parallel( + animations.map((animation, i) => { + return sequence([delay(time * i), animation]); + }), + ); +}; + +type LoopAnimationConfig = { + iterations: number, + resetBeforeIteration?: boolean, + ... +}; + +const loop = function ( + animation: CompositeAnimation, + {iterations = -1, resetBeforeIteration = true}: LoopAnimationConfig = {}, +): CompositeAnimation { + let isFinished = false; + let iterationsSoFar = 0; + return { + start: function (callback?: ?EndCallback) { + const restart = function (result: EndResult = {finished: true}): void { + if ( + isFinished || + iterationsSoFar === iterations || + result.finished === false + ) { + callback && callback(result); + } else { + iterationsSoFar++; + resetBeforeIteration && animation.reset(); + animation.start(restart); + } + }; + if (!animation || iterations === 0) { + callback && callback({finished: true}); + } else { + if (animation._isUsingNativeDriver()) { + animation._startNativeLoop(iterations); + } else { + restart(); // Start looping recursively on the js thread + } + } + }, + + stop: function (): void { + isFinished = true; + animation.stop(); + }, + + reset: function (): void { + iterationsSoFar = 0; + isFinished = false; + animation.reset(); + }, + + _startNativeLoop: function () { + throw new Error( + 'Loops run using the native driver cannot contain Animated.loop animations', + ); + }, + + _isUsingNativeDriver: function (): boolean { + return animation._isUsingNativeDriver(); + }, + }; +}; + +function forkEvent( + event: ?AnimatedEvent | ?Function, + listener: Function, +): AnimatedEvent | Function { + if (!event) { + return listener; + } else if (event instanceof AnimatedEvent) { + event.__addListener(listener); + return event; + } else { + return (...args) => { + typeof event === 'function' && event(...args); + listener(...args); + }; + } +} + +function unforkEvent( + event: ?AnimatedEvent | ?Function, + listener: Function, +): void { + if (event && event instanceof AnimatedEvent) { + event.__removeListener(listener); + } +} + +const event = function ( + argMapping: $ReadOnlyArray, + config: EventConfig, +): any { + const animatedEvent = new AnimatedEvent(argMapping, config); + if (animatedEvent.__isNative) { + return animatedEvent; + } else { + return animatedEvent.__getHandler(); + } +}; + +/** + * The `Animated` library is designed to make animations fluid, powerful, and + * easy to build and maintain. `Animated` focuses on declarative relationships + * between inputs and outputs, with configurable transforms in between, and + * simple `start`/`stop` methods to control time-based animation execution. + * If additional transforms are added, be sure to include them in + * AnimatedMock.js as well. + * + * See https://reactnative.dev/docs/animated + */ +module.exports = { + /** + * Standard value class for driving animations. Typically initialized with + * `new Animated.Value(0);` + * + * See https://reactnative.dev/docs/animated#value + */ + Value: AnimatedValue, + /** + * 2D value class for driving 2D animations, such as pan gestures. + * + * See https://reactnative.dev/docs/animatedvaluexy + */ + ValueXY: AnimatedValueXY, + /** + * Exported to use the Interpolation type in flow. + * + * See https://reactnative.dev/docs/animated#interpolation + */ + Interpolation: AnimatedInterpolation, + /** + * Exported for ease of type checking. All animated values derive from this + * class. + * + * See https://reactnative.dev/docs/animated#node + */ + Node: AnimatedNode, + + /** + * Animates a value from an initial velocity to zero based on a decay + * coefficient. + * + * See https://reactnative.dev/docs/animated#decay + */ + decay, + /** + * Animates a value along a timed easing curve. The Easing module has tons of + * predefined curves, or you can use your own function. + * + * See https://reactnative.dev/docs/animated#timing + */ + timing, + /** + * Animates a value according to an analytical spring model based on + * damped harmonic oscillation. + * + * See https://reactnative.dev/docs/animated#spring + */ + spring, + + /** + * Creates a new Animated value composed from two Animated values added + * together. + * + * See https://reactnative.dev/docs/animated#add + */ + add, + + /** + * Creates a new Animated value composed by subtracting the second Animated + * value from the first Animated value. + * + * See https://reactnative.dev/docs/animated#subtract + */ + subtract, + + /** + * Creates a new Animated value composed by dividing the first Animated value + * by the second Animated value. + * + * See https://reactnative.dev/docs/animated#divide + */ + divide, + + /** + * Creates a new Animated value composed from two Animated values multiplied + * together. + * + * See https://reactnative.dev/docs/animated#multiply + */ + multiply, + + /** + * Creates a new Animated value that is the (non-negative) modulo of the + * provided Animated value. + * + * See https://reactnative.dev/docs/animated#modulo + */ + modulo, + + /** + * Create a new Animated value that is limited between 2 values. It uses the + * difference between the last value so even if the value is far from the + * bounds it will start changing when the value starts getting closer again. + * + * See https://reactnative.dev/docs/animated#diffclamp + */ + diffClamp, + + /** + * Starts an animation after the given delay. + * + * See https://reactnative.dev/docs/animated#delay + */ + delay, + /** + * Starts an array of animations in order, waiting for each to complete + * before starting the next. If the current running animation is stopped, no + * following animations will be started. + * + * See https://reactnative.dev/docs/animated#sequence + */ + sequence, + /** + * Starts an array of animations all at the same time. By default, if one + * of the animations is stopped, they will all be stopped. You can override + * this with the `stopTogether` flag. + * + * See https://reactnative.dev/docs/animated#parallel + */ + parallel, + /** + * Array of animations may run in parallel (overlap), but are started in + * sequence with successive delays. Nice for doing trailing effects. + * + * See https://reactnative.dev/docs/animated#stagger + */ + stagger, + /** + * Loops a given animation continuously, so that each time it reaches the + * end, it resets and begins again from the start. + * + * See https://reactnative.dev/docs/animated#loop + */ + loop, + + /** + * Takes an array of mappings and extracts values from each arg accordingly, + * then calls `setValue` on the mapped outputs. + * + * See https://reactnative.dev/docs/animated#event + */ + event, + + /** + * Make any React component Animatable. Used to create `Animated.View`, etc. + * + * See https://reactnative.dev/docs/animated#createanimatedcomponent + */ + createAnimatedComponent, + + /** + * Imperative API to attach an animated value to an event on a view. Prefer + * using `Animated.event` with `useNativeDrive: true` if possible. + * + * See https://reactnative.dev/docs/animated#attachnativeevent + */ + attachNativeEvent, + + /** + * Advanced imperative API for snooping on animated events that are passed in + * through props. Use values directly where possible. + * + * See https://reactnative.dev/docs/animated#forkevent + */ + forkEvent, + unforkEvent, + + /** + * Get or set default platformConfig value to use with animations. This value + * is used when a platformConfig option is not provided or is null. + */ + unstable_getDefaultPlatformConfig, + unstable_setDefaultPlatformConfig, + + /** + * Expose Event class, so it can be used as a type for type checkers. + */ + Event: AnimatedEvent, +}; diff --git a/vnext/src/Libraries/Animated/AnimatedMock.windows.js b/vnext/src/Libraries/Animated/AnimatedMock.windows.js new file mode 100644 index 00000000000..cd44ff53cb8 --- /dev/null +++ b/vnext/src/Libraries/Animated/AnimatedMock.windows.js @@ -0,0 +1,195 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +const {AnimatedEvent, attachNativeEvent} = require('./AnimatedEvent'); +const { + unstable_getDefaultPlatformConfig, + unstable_setDefaultPlatformConfig, +} = require('./AnimatedPlatformConfig'); +const AnimatedImplementation = require('./AnimatedImplementation'); +const AnimatedInterpolation = require('./nodes/AnimatedInterpolation'); +const AnimatedNode = require('./nodes/AnimatedNode'); +const AnimatedValue = require('./nodes/AnimatedValue'); +const AnimatedValueXY = require('./nodes/AnimatedValueXY'); + +const createAnimatedComponent = require('./createAnimatedComponent'); + +import type {EndCallback} from './animations/Animation'; +import type {TimingAnimationConfig} from './animations/TimingAnimation'; +import type {DecayAnimationConfig} from './animations/DecayAnimation'; +import type {SpringAnimationConfig} from './animations/SpringAnimation'; + +/** + * Animations are a source of flakiness in snapshot testing. This mock replaces + * animation functions from AnimatedImplementation with empty animations for + * predictability in tests. When possible the animation will run immediately + * to the final state. + */ + +// Prevent any callback invocation from recursively triggering another +// callback, which may trigger another animation +let inAnimationCallback = false; +function mockAnimationStart( + start: (callback?: ?EndCallback) => void, +): (callback?: ?EndCallback) => void { + return (callback) => { + const guardedCallback = + callback == null + ? callback + : (...args) => { + if (inAnimationCallback) { + console.warn( + 'Ignoring recursive animation callback when running mock animations', + ); + return; + } + inAnimationCallback = true; + try { + callback(...args); + } finally { + inAnimationCallback = false; + } + }; + start(guardedCallback); + }; +} + +export type CompositeAnimation = { + start: (callback?: ?EndCallback) => void, + stop: () => void, + reset: () => void, + _startNativeLoop: (iterations?: number) => void, + _isUsingNativeDriver: () => boolean, + ... +}; + +const emptyAnimation = { + start: () => {}, + stop: () => {}, + reset: () => {}, + _startNativeLoop: () => {}, + _isUsingNativeDriver: () => { + return false; + }, +}; + +const mockCompositeAnimation = ( + animations: Array, +): CompositeAnimation => ({ + ...emptyAnimation, + start: mockAnimationStart((callback?: ?EndCallback): void => { + animations.forEach((animation) => animation.start()); + callback?.({finished: true}); + }), +}); + +const spring = function ( + value: AnimatedValue | AnimatedValueXY, + config: SpringAnimationConfig, +): CompositeAnimation { + const anyValue: any = value; + return { + ...emptyAnimation, + start: mockAnimationStart((callback?: ?EndCallback): void => { + anyValue.setValue(config.toValue); + callback?.({finished: true}); + }), + }; +}; + +const timing = function ( + value: AnimatedValue | AnimatedValueXY, + config: TimingAnimationConfig, +): CompositeAnimation { + const anyValue: any = value; + return { + ...emptyAnimation, + start: mockAnimationStart((callback?: ?EndCallback): void => { + anyValue.setValue(config.toValue); + callback?.({finished: true}); + }), + }; +}; + +const decay = function ( + value: AnimatedValue | AnimatedValueXY, + config: DecayAnimationConfig, +): CompositeAnimation { + return emptyAnimation; +}; + +const sequence = function ( + animations: Array, +): CompositeAnimation { + return mockCompositeAnimation(animations); +}; + +type ParallelConfig = {stopTogether?: boolean, ...}; +const parallel = function ( + animations: Array, + config?: ?ParallelConfig, +): CompositeAnimation { + return mockCompositeAnimation(animations); +}; + +const delay = function (time: number): CompositeAnimation { + return emptyAnimation; +}; + +const stagger = function ( + time: number, + animations: Array, +): CompositeAnimation { + return mockCompositeAnimation(animations); +}; + +type LoopAnimationConfig = { + iterations: number, + resetBeforeIteration?: boolean, + ... +}; + +const loop = function ( + animation: CompositeAnimation, + {iterations = -1}: LoopAnimationConfig = {}, +): CompositeAnimation { + return emptyAnimation; +}; + +module.exports = { + Value: AnimatedValue, + ValueXY: AnimatedValueXY, + Interpolation: AnimatedInterpolation, + Node: AnimatedNode, + decay, + timing, + spring, + add: AnimatedImplementation.add, + subtract: AnimatedImplementation.subtract, + divide: AnimatedImplementation.divide, + multiply: AnimatedImplementation.multiply, + modulo: AnimatedImplementation.modulo, + diffClamp: AnimatedImplementation.diffClamp, + delay, + sequence, + parallel, + stagger, + loop, + event: AnimatedImplementation.event, + createAnimatedComponent, + attachNativeEvent, + forkEvent: AnimatedImplementation.forkEvent, + unforkEvent: AnimatedImplementation.unforkEvent, + unstable_getDefaultPlatformConfig, + unstable_setDefaultPlatformConfig, + Event: AnimatedEvent, +}; diff --git a/vnext/src/Libraries/Animated/AnimatedPlatformConfig.windows.js b/vnext/src/Libraries/Animated/AnimatedPlatformConfig.windows.js new file mode 100644 index 00000000000..a8bb419c15e --- /dev/null +++ b/vnext/src/Libraries/Animated/AnimatedPlatformConfig.windows.js @@ -0,0 +1,25 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +export type PlatformConfig = {useComposition: boolean}; + +let defaultPlatformConfig: ?PlatformConfig = {useComposition: true}; + +export function unstable_getDefaultPlatformConfig(): ?PlatformConfig { + return defaultPlatformConfig; +} + +export function unstable_setDefaultPlatformConfig( + platformConfig: ?PlatformConfig, +) { + defaultPlatformConfig = platformConfig; +} diff --git a/vnext/src/Libraries/Animated/animations/Animation.windows.js b/vnext/src/Libraries/Animated/animations/Animation.windows.js new file mode 100644 index 00000000000..bf2da37cf66 --- /dev/null +++ b/vnext/src/Libraries/Animated/animations/Animation.windows.js @@ -0,0 +1,94 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +const NativeAnimatedHelper = require('../NativeAnimatedHelper'); +const { + unstable_getDefaultPlatformConfig, +} = require('../AnimatedPlatformConfig'); +import type {PlatformConfig} from '../AnimatedPlatformConfig'; +import type AnimatedValue from '../nodes/AnimatedValue'; + +export type EndResult = {finished: boolean, ...}; +export type EndCallback = (result: EndResult) => void; + +export type AnimationConfig = { + isInteraction?: boolean, + useNativeDriver: boolean, + platformConfig?: PlatformConfig, + onComplete?: ?EndCallback, + iterations?: number, +}; + +let startNativeAnimationNextId = 1; + +// 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 { + const startNativeAnimationWaitId = `${startNativeAnimationNextId}:startAnimation`; + startNativeAnimationNextId += 1; + NativeAnimatedHelper.API.setWaitingForIdentifier( + startNativeAnimationWaitId, + ); + try { + const config = this.__getNativeAnimationConfig(); + config.platformConfig = + config.platformConfig ?? unstable_getDefaultPlatformConfig(); + animatedValue.__makeNative(config.platformConfig); + this.__nativeId = NativeAnimatedHelper.generateNewAnimationId(); + NativeAnimatedHelper.API.startAnimatingNode( + this.__nativeId, + animatedValue.__getNativeTag(), + config, + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + this.__debouncedOnEnd.bind(this), + ); + } catch (e) { + throw e; + } finally { + NativeAnimatedHelper.API.unsetWaitingForIdentifier( + startNativeAnimationWaitId, + ); + } + } +} + +module.exports = Animation; diff --git a/vnext/src/Libraries/Animated/animations/DecayAnimation.windows.js b/vnext/src/Libraries/Animated/animations/DecayAnimation.windows.js new file mode 100644 index 00000000000..40d854122f6 --- /dev/null +++ b/vnext/src/Libraries/Animated/animations/DecayAnimation.windows.js @@ -0,0 +1,127 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +const Animation = require('./Animation'); + +const {shouldUseNativeDriver} = require('../NativeAnimatedHelper'); + +import type {PlatformConfig} from '../AnimatedPlatformConfig'; +import type AnimatedValue from '../nodes/AnimatedValue'; +import type {AnimationConfig, EndCallback} from './Animation'; + +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; + _platformConfig: ?PlatformConfig; + + constructor(config: DecayAnimationConfigSingle) { + super(); + this._deceleration = config.deceleration ?? 0.998; + this._velocity = config.velocity; + this._useNativeDriver = shouldUseNativeDriver(config); + this._platformConfig = config.platformConfig; + this.__isInteraction = config.isInteraction ?? !this._useNativeDriver; + this.__iterations = config.iterations ?? 1; + } + + __getNativeAnimationConfig(): {| + deceleration: number, + iterations: number, + platformConfig: ?PlatformConfig, + type: $TEMPORARY$string<'decay'>, + velocity: number, + |} { + return { + type: 'decay', + deceleration: this._deceleration, + velocity: this._velocity, + iterations: this.__iterations, + platformConfig: this._platformConfig, + }; + } + + 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 { + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + 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) { + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + 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/vnext/src/Libraries/Animated/animations/SpringAnimation.windows.js b/vnext/src/Libraries/Animated/animations/SpringAnimation.windows.js new file mode 100644 index 00000000000..96721681fd5 --- /dev/null +++ b/vnext/src/Libraries/Animated/animations/SpringAnimation.windows.js @@ -0,0 +1,365 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +const AnimatedValue = require('../nodes/AnimatedValue'); +const AnimatedValueXY = require('../nodes/AnimatedValueXY'); +const AnimatedInterpolation = require('../nodes/AnimatedInterpolation'); +const Animation = require('./Animation'); +const SpringConfig = require('../SpringConfig'); + +const invariant = require('invariant'); + +const {shouldUseNativeDriver} = require('../NativeAnimatedHelper'); + +import type {PlatformConfig} from '../AnimatedPlatformConfig'; +import type {AnimationConfig, EndCallback} from './Animation'; + +export type SpringAnimationConfig = { + ...AnimationConfig, + toValue: + | number + | AnimatedValue + | { + x: number, + y: number, + ... + } + | AnimatedValueXY + | AnimatedInterpolation, + overshootClamping?: boolean, + restDisplacementThreshold?: number, + restSpeedThreshold?: number, + velocity?: + | number + | { + x: number, + y: number, + ... + }, + bounciness?: number, + speed?: number, + tension?: number, + friction?: number, + stiffness?: number, + damping?: number, + mass?: number, + delay?: number, +}; + +export type SpringAnimationConfigSingle = { + ...AnimationConfig, + toValue: number | AnimatedValue | AnimatedInterpolation, + overshootClamping?: boolean, + restDisplacementThreshold?: number, + restSpeedThreshold?: number, + velocity?: number, + bounciness?: number, + speed?: number, + tension?: number, + friction?: number, + stiffness?: number, + damping?: number, + mass?: number, + delay?: number, +}; + +class SpringAnimation extends Animation { + _overshootClamping: boolean; + _restDisplacementThreshold: number; + _restSpeedThreshold: number; + _lastVelocity: number; + _startPosition: number; + _lastPosition: number; + _fromValue: number; + _toValue: any; + _stiffness: number; + _damping: number; + _mass: number; + _initialVelocity: number; + _delay: number; + _timeout: any; + _startTime: number; + _lastTime: number; + _frameTime: number; + _onUpdate: (value: number) => void; + _animationFrame: any; + _useNativeDriver: boolean; + _platformConfig: ?PlatformConfig; + + constructor(config: SpringAnimationConfigSingle) { + super(); + + this._overshootClamping = config.overshootClamping ?? false; + this._restDisplacementThreshold = config.restDisplacementThreshold ?? 0.001; + this._restSpeedThreshold = config.restSpeedThreshold ?? 0.001; + this._initialVelocity = config.velocity ?? 0; + this._lastVelocity = config.velocity ?? 0; + this._toValue = config.toValue; + this._delay = config.delay ?? 0; + this._useNativeDriver = shouldUseNativeDriver(config); + this._platformConfig = config.platformConfig; + this.__isInteraction = config.isInteraction ?? !this._useNativeDriver; + this.__iterations = config.iterations ?? 1; + + if ( + config.stiffness !== undefined || + config.damping !== undefined || + config.mass !== undefined + ) { + invariant( + config.bounciness === undefined && + config.speed === undefined && + config.tension === undefined && + config.friction === undefined, + 'You can define one of bounciness/speed, tension/friction, or stiffness/damping/mass, but not more than one', + ); + this._stiffness = config.stiffness ?? 100; + this._damping = config.damping ?? 10; + this._mass = config.mass ?? 1; + } else if (config.bounciness !== undefined || config.speed !== undefined) { + // Convert the origami bounciness/speed values to stiffness/damping + // We assume mass is 1. + invariant( + config.tension === undefined && + config.friction === undefined && + config.stiffness === undefined && + config.damping === undefined && + config.mass === undefined, + 'You can define one of bounciness/speed, tension/friction, or stiffness/damping/mass, but not more than one', + ); + const springConfig = SpringConfig.fromBouncinessAndSpeed( + config.bounciness ?? 8, + config.speed ?? 12, + ); + this._stiffness = springConfig.stiffness; + this._damping = springConfig.damping; + this._mass = 1; + } else { + // Convert the origami tension/friction values to stiffness/damping + // We assume mass is 1. + const springConfig = SpringConfig.fromOrigamiTensionAndFriction( + config.tension ?? 40, + config.friction ?? 7, + ); + this._stiffness = springConfig.stiffness; + this._damping = springConfig.damping; + this._mass = 1; + } + + invariant(this._stiffness > 0, 'Stiffness value must be greater than 0'); + invariant(this._damping > 0, 'Damping value must be greater than 0'); + invariant(this._mass > 0, 'Mass value must be greater than 0'); + } + + __getNativeAnimationConfig(): {| + damping: number, + initialVelocity: number, + iterations: number, + mass: number, + platformConfig: ?PlatformConfig, + overshootClamping: boolean, + restDisplacementThreshold: number, + restSpeedThreshold: number, + stiffness: number, + toValue: any, + type: $TEMPORARY$string<'spring'>, + |} { + return { + type: 'spring', + overshootClamping: this._overshootClamping, + restDisplacementThreshold: this._restDisplacementThreshold, + restSpeedThreshold: this._restSpeedThreshold, + stiffness: this._stiffness, + damping: this._damping, + mass: this._mass, + initialVelocity: this._initialVelocity ?? this._lastVelocity, + toValue: this._toValue, + iterations: this.__iterations, + platformConfig: this._platformConfig, + }; + } + + 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(); + this._frameTime = 0.0; + + if (previousAnimation instanceof SpringAnimation) { + const internalState = previousAnimation.getInternalState(); + this._lastPosition = internalState.lastPosition; + this._lastVelocity = internalState.lastVelocity; + // Set the initial velocity to the last velocity + this._initialVelocity = this._lastVelocity; + this._lastTime = internalState.lastTime; + } + + 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, + }; + } + + /** + * This spring model is based off of a damped harmonic oscillator + * (https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator). + * + * We use the closed form of the second order differential equation: + * + * x'' + (2ζ⍵_0)x' + ⍵^2x = 0 + * + * where + * ⍵_0 = √(k / m) (undamped angular frequency of the oscillator), + * ζ = c / 2√mk (damping ratio), + * c = damping constant + * k = stiffness + * m = mass + * + * The derivation of the closed form is described in detail here: + * http://planetmath.org/sites/default/files/texpdf/39745.pdf + * + * This algorithm happens to match the algorithm used by CASpringAnimation, + * a QuartzCore (iOS) API that creates spring animations. + */ + onUpdate(): void { + // 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; + } + + const deltaTime = (now - this._lastTime) / 1000; + this._frameTime += deltaTime; + + const c: number = this._damping; + const m: number = this._mass; + const k: number = this._stiffness; + const v0: number = -this._initialVelocity; + + const zeta = c / (2 * Math.sqrt(k * m)); // damping ratio + const omega0 = Math.sqrt(k / m); // undamped angular frequency of the oscillator (rad/ms) + const omega1 = omega0 * Math.sqrt(1.0 - zeta * zeta); // exponential decay + const x0 = this._toValue - this._startPosition; // calculate the oscillation from x0 = 1 to x = 0 + + let position = 0.0; + let velocity = 0.0; + const t = this._frameTime; + if (zeta < 1) { + // Under damped + const envelope = Math.exp(-zeta * omega0 * t); + position = + this._toValue - + envelope * + (((v0 + zeta * omega0 * x0) / omega1) * Math.sin(omega1 * t) + + x0 * Math.cos(omega1 * t)); + // This looks crazy -- it's actually just the derivative of the + // oscillation function + velocity = + zeta * + omega0 * + envelope * + ((Math.sin(omega1 * t) * (v0 + zeta * omega0 * x0)) / omega1 + + x0 * Math.cos(omega1 * t)) - + envelope * + (Math.cos(omega1 * t) * (v0 + zeta * omega0 * x0) - + omega1 * x0 * Math.sin(omega1 * t)); + } else { + // Critically damped + const envelope = Math.exp(-omega0 * t); + position = this._toValue - envelope * (x0 + (v0 + omega0 * x0) * t); + velocity = + envelope * (v0 * (t * omega0 - 1) + t * x0 * (omega0 * omega0)); + } + + 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._stiffness !== 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._stiffness !== 0) { + isDisplacement = + Math.abs(this._toValue - position) <= this._restDisplacementThreshold; + } + + if (isOvershooting || (isVelocity && isDisplacement)) { + if (this._stiffness !== 0) { + // Ensure that we end up with a round value + this._lastPosition = this._toValue; + this._lastVelocity = 0; + this._onUpdate(this._toValue); + } + + this.__debouncedOnEnd({finished: true}); + return; + } + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + 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/vnext/src/Libraries/Animated/animations/TimingAnimation.windows.js b/vnext/src/Libraries/Animated/animations/TimingAnimation.windows.js new file mode 100644 index 00000000000..60cfc6a5181 --- /dev/null +++ b/vnext/src/Libraries/Animated/animations/TimingAnimation.windows.js @@ -0,0 +1,172 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +const AnimatedValue = require('../nodes/AnimatedValue'); +const AnimatedValueXY = require('../nodes/AnimatedValueXY'); +const AnimatedInterpolation = require('../nodes/AnimatedInterpolation'); +const Animation = require('./Animation'); + +const {shouldUseNativeDriver} = require('../NativeAnimatedHelper'); + +import type {PlatformConfig} from '../AnimatedPlatformConfig'; +import type {AnimationConfig, EndCallback} from './Animation'; + +export type TimingAnimationConfig = { + ...AnimationConfig, + toValue: + | number + | AnimatedValue + | { + x: number, + y: number, + ... + } + | AnimatedValueXY + | AnimatedInterpolation, + easing?: (value: number) => number, + duration?: number, + delay?: number, +}; + +export type TimingAnimationConfigSingle = { + ...AnimationConfig, + toValue: number | AnimatedValue | AnimatedInterpolation, + easing?: (value: number) => number, + duration?: number, + delay?: number, +}; + +let _easeInOut; +function easeInOut() { + if (!_easeInOut) { + const Easing = require('../Easing'); + // $FlowFixMe[method-unbinding] + _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; + _platformConfig: ?PlatformConfig; + + constructor(config: TimingAnimationConfigSingle) { + super(); + this._toValue = config.toValue; + this._easing = config.easing ?? easeInOut(); + this._duration = config.duration ?? 500; + this._delay = config.delay ?? 0; + this.__iterations = config.iterations ?? 1; + this._useNativeDriver = shouldUseNativeDriver(config); + this._platformConfig = config.platformConfig; + this.__isInteraction = config.isInteraction ?? !this._useNativeDriver; + } + + __getNativeAnimationConfig(): any { + const frameDuration = 1000.0 / 60.0; + const frames = []; + const numFrames = Math.round(this._duration / frameDuration); + for (let frame = 0; frame < numFrames; frame++) { + frames.push(this._easing(frame / numFrames)); + } + frames.push(this._easing(1)); + return { + type: 'frames', + frames, + toValue: this._toValue, + iterations: this.__iterations, + platformConfig: this._platformConfig, + }; + } + + 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( + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + 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) { + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + 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/vnext/src/Libraries/Animated/nodes/AnimatedAddition.windows.js b/vnext/src/Libraries/Animated/nodes/AnimatedAddition.windows.js new file mode 100644 index 00000000000..6736cf07c8a --- /dev/null +++ b/vnext/src/Libraries/Animated/nodes/AnimatedAddition.windows.js @@ -0,0 +1,64 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +const AnimatedInterpolation = require('./AnimatedInterpolation'); +const AnimatedNode = require('./AnimatedNode'); +const AnimatedValue = require('./AnimatedValue'); +const AnimatedWithChildren = require('./AnimatedWithChildren'); + +import type {PlatformConfig} from '../AnimatedPlatformConfig'; +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(platformConfig: ?PlatformConfig) { + this._a.__makeNative(platformConfig); + this._b.__makeNative(platformConfig); + super.__makeNative(platformConfig); + } + + __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/vnext/src/Libraries/Animated/nodes/AnimatedDiffClamp.windows.js b/vnext/src/Libraries/Animated/nodes/AnimatedDiffClamp.windows.js new file mode 100644 index 00000000000..013a8d26047 --- /dev/null +++ b/vnext/src/Libraries/Animated/nodes/AnimatedDiffClamp.windows.js @@ -0,0 +1,72 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +const AnimatedInterpolation = require('./AnimatedInterpolation'); +const AnimatedNode = require('./AnimatedNode'); +const AnimatedWithChildren = require('./AnimatedWithChildren'); + +import type {InterpolationConfigType} from './AnimatedInterpolation'; +import type {PlatformConfig} from '../AnimatedPlatformConfig'; + +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(platformConfig: ?PlatformConfig) { + this._a.__makeNative(platformConfig); + super.__makeNative(platformConfig); + } + + 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/vnext/src/Libraries/Animated/nodes/AnimatedDivision.windows.js b/vnext/src/Libraries/Animated/nodes/AnimatedDivision.windows.js new file mode 100644 index 00000000000..2530642e626 --- /dev/null +++ b/vnext/src/Libraries/Animated/nodes/AnimatedDivision.windows.js @@ -0,0 +1,80 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @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'; +import type {PlatformConfig} from '../AnimatedPlatformConfig'; + +class AnimatedDivision extends AnimatedWithChildren { + _a: AnimatedNode; + _b: AnimatedNode; + _warnedAboutDivideByZero: boolean = false; + + constructor(a: AnimatedNode | number, b: AnimatedNode | number) { + super(); + if (b === 0 || (b instanceof AnimatedNode && b.__getValue() === 0)) { + console.error('Detected potential division by zero in AnimatedDivision'); + } + this._a = typeof a === 'number' ? new AnimatedValue(a) : a; + this._b = typeof b === 'number' ? new AnimatedValue(b) : b; + } + + __makeNative(platformConfig: ?PlatformConfig) { + this._a.__makeNative(platformConfig); + this._b.__makeNative(platformConfig); + super.__makeNative(platformConfig); + } + + __getValue(): number { + const a = this._a.__getValue(); + const b = this._b.__getValue(); + if (b === 0) { + // Prevent spamming the console/LogBox + if (!this._warnedAboutDivideByZero) { + console.error('Detected division by zero in AnimatedDivision'); + this._warnedAboutDivideByZero = true; + } + // Passing infinity/NaN to Fabric will cause a native crash + return 0; + } + this._warnedAboutDivideByZero = false; + 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/vnext/src/Libraries/Animated/nodes/AnimatedInterpolation.windows.js b/vnext/src/Libraries/Animated/nodes/AnimatedInterpolation.windows.js new file mode 100644 index 00000000000..d25c24fb7a9 --- /dev/null +++ b/vnext/src/Libraries/Animated/nodes/AnimatedInterpolation.windows.js @@ -0,0 +1,374 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +/* eslint no-bitwise: 0 */ + +'use strict'; + +const AnimatedNode = require('./AnimatedNode'); +const AnimatedWithChildren = require('./AnimatedWithChildren'); +const NativeAnimatedHelper = require('../NativeAnimatedHelper'); + +const invariant = require('invariant'); +const normalizeColor = require('../../StyleSheet/normalizeColor'); + +import type {PlatformConfig} from '../AnimatedPlatformConfig'; + +type ExtrapolateType = 'extend' | 'identity' | 'clamp'; + +export type InterpolationConfigType = { + inputRange: $ReadOnlyArray, + outputRange: $ReadOnlyArray | $ReadOnlyArray, + easing?: (input: number) => number, + extrapolate?: ExtrapolateType, + extrapolateLeft?: ExtrapolateType, + extrapolateRight?: ExtrapolateType, +}; + +const linear = (t: number) => t; + +/** + * Very handy helper to map input ranges to output ranges with an easing + * function and custom behavior outside of the ranges. + */ +function createInterpolation( + config: InterpolationConfigType, +): (input: number) => number | string { + if (config.outputRange && typeof config.outputRange[0] === 'string') { + return createInterpolationFromStringOutputRange(config); + } + + const outputRange: Array = (config.outputRange: any); + checkInfiniteRange('outputRange', outputRange); + + const 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', + ); + + const easing = config.easing || linear; + + let 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; + } + + return (input) => { + invariant( + typeof input === 'number', + 'Cannot interpolation an input which is not a number', + ); + + const range = findRange(input, inputRange); + return interpolate( + input, + inputRange[range], + inputRange[range + 1], + outputRange[range], + outputRange[range + 1], + easing, + extrapolateLeft, + extrapolateRight, + ); + }; +} + +function interpolate( + input: number, + inputMin: number, + inputMax: number, + outputMin: number, + outputMax: number, + easing: (input: number) => number, + extrapolateLeft: ExtrapolateType, + extrapolateRight: ExtrapolateType, +) { + let result = input; + + // Extrapolate + if (result < inputMin) { + if (extrapolateLeft === 'identity') { + return result; + } else if (extrapolateLeft === 'clamp') { + result = inputMin; + } else if (extrapolateLeft === 'extend') { + // noop + } + } + + if (result > inputMax) { + if (extrapolateRight === 'identity') { + return result; + } else if (extrapolateRight === 'clamp') { + result = inputMax; + } else if (extrapolateRight === 'extend') { + // noop + } + } + + if (outputMin === outputMax) { + return outputMin; + } + + if (inputMin === inputMax) { + if (input <= inputMin) { + return outputMin; + } + return outputMax; + } + + // Input Range + if (inputMin === -Infinity) { + result = -result; + } else if (inputMax === Infinity) { + result = result - inputMin; + } else { + result = (result - inputMin) / (inputMax - inputMin); + } + + // Easing + result = easing(result); + + // Output Range + if (outputMin === -Infinity) { + result = -result; + } else if (outputMax === Infinity) { + result = result + outputMin; + } else { + result = result * (outputMax - outputMin) + outputMin; + } + + return result; +} + +function colorToRgba(input: string): string { + let normalizedColor = normalizeColor(input); + if (normalizedColor === null || typeof normalizedColor !== 'number') { + return input; + } + + normalizedColor = normalizedColor || 0; + + const r = (normalizedColor & 0xff000000) >>> 24; + const g = (normalizedColor & 0x00ff0000) >>> 16; + const b = (normalizedColor & 0x0000ff00) >>> 8; + const a = (normalizedColor & 0x000000ff) / 255; + + return `rgba(${r}, ${g}, ${b}, ${a})`; +} + +const stringShapeRegex = /[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g; + +/** + * Supports string shapes by extracting numbers so new values can be computed, + * and recombines those values into new strings of the same shape. Supports + * things like: + * + * rgba(123, 42, 99, 0.36) // colors + * -45deg // values with units + */ +function createInterpolationFromStringOutputRange( + config: InterpolationConfigType, +): (input: number) => string { + let outputRange: Array = (config.outputRange: any); + invariant(outputRange.length >= 2, 'Bad output range'); + outputRange = outputRange.map(colorToRgba); + checkPattern(outputRange); + + // ['rgba(0, 100, 200, 0)', 'rgba(50, 150, 250, 0.5)'] + // -> + // [ + // [0, 50], + // [100, 150], + // [200, 250], + // [0, 0.5], + // ] + /* $FlowFixMe[incompatible-use] (>=0.18.0): `outputRange[0].match()` can + * return `null`. Need to guard against this possibility. */ + const outputRanges = outputRange[0].match(stringShapeRegex).map(() => []); + outputRange.forEach((value) => { + /* $FlowFixMe[incompatible-use] (>=0.18.0): `value.match()` can return + * `null`. Need to guard against this possibility. */ + value.match(stringShapeRegex).forEach((number, i) => { + outputRanges[i].push(+number); + }); + }); + + const interpolations = outputRange[0] + .match(stringShapeRegex) + /* $FlowFixMe[incompatible-use] (>=0.18.0): `outputRange[0].match()` can + * return `null`. Need to guard against this possibility. */ + /* $FlowFixMe[incompatible-call] (>=0.18.0): `outputRange[0].match()` can + * return `null`. Need to guard against this possibility. */ + .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) => { + let i = 0; + // 'rgba(0, 100, 200, 0)' + // -> + // 'rgba(${interpolations[0](input)}, ${interpolations[1](input)}, ...' + return outputRange[0].replace(stringShapeRegex, () => { + let val = +interpolations[i++](input); + if (shouldRound) { + val = i < 4 ? Math.round(val) : Math.round(val * 1000) / 1000; + } + return String(val); + }); + }; +} + +function isRgbOrRgba(range: string) { + return typeof range === 'string' && range.startsWith('rgb'); +} + +function checkPattern(arr: $ReadOnlyArray) { + 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], + ); + } +} + +function findRange(input: number, inputRange: $ReadOnlyArray) { + let i; + for (i = 1; i < inputRange.length - 1; ++i) { + if (inputRange[i] >= input) { + break; + } + } + return i - 1; +} + +function checkValidInputRange(arr: $ReadOnlyArray) { + invariant(arr.length >= 2, 'inputRange must have at least 2 elements'); + for (let i = 1; i < arr.length; ++i) { + invariant( + arr[i] >= arr[i - 1], + /* $FlowFixMe[incompatible-type] (>=0.13.0) - In the addition expression + * below this comment, one or both of the operands may be something that + * doesn't cleanly convert to a string, like undefined, null, and object, + * etc. If you really mean this implicit string conversion, you can do + * something like String(myThing) */ + 'inputRange must be monotonically non-decreasing ' + arr, + ); + } +} + +function checkInfiniteRange(name: string, arr: $ReadOnlyArray) { + invariant(arr.length >= 2, name + ' must have at least 2 elements'); + invariant( + arr.length !== 2 || arr[0] !== -Infinity || arr[1] !== Infinity, + /* $FlowFixMe[incompatible-type] (>=0.13.0) - In the addition expression + * below this comment, one or both of the operands may be something that + * doesn't cleanly convert to a string, like undefined, null, and object, + * etc. If you really mean this implicit string conversion, you can do + * something like String(myThing) */ + name + 'cannot be ]-infinity;+infinity[ ' + arr, + ); +} + +class AnimatedInterpolation extends AnimatedWithChildren { + // Export for testing. + static __createInterpolation: ( + config: InterpolationConfigType, + ) => (input: number) => number | string = 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(platformConfig: ?PlatformConfig) { + this._parent.__makeNative(platformConfig); + super.__makeNative(platformConfig); + } + + __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): Array { + return range.map(NativeAnimatedHelper.transformDataType); + } + + __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 transform `inputRange` here + /* $FlowFixMe[incompatible-call] (>=0.38.0) - Flow error detected during + * the deployment of v0.38.0. To see the error, remove this comment and + * run flow */ + 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/vnext/src/Libraries/Animated/nodes/AnimatedModulo.windows.js b/vnext/src/Libraries/Animated/nodes/AnimatedModulo.windows.js new file mode 100644 index 00000000000..12920ade500 --- /dev/null +++ b/vnext/src/Libraries/Animated/nodes/AnimatedModulo.windows.js @@ -0,0 +1,63 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +const AnimatedInterpolation = require('./AnimatedInterpolation'); +const AnimatedNode = require('./AnimatedNode'); +const AnimatedWithChildren = require('./AnimatedWithChildren'); + +import type {InterpolationConfigType} from './AnimatedInterpolation'; +import type {PlatformConfig} from '../AnimatedPlatformConfig'; + +class AnimatedModulo extends AnimatedWithChildren { + _a: AnimatedNode; + _modulus: number; + + constructor(a: AnimatedNode, modulus: number) { + super(); + this._a = a; + this._modulus = modulus; + } + + __makeNative(platformConfig: ?PlatformConfig) { + this._a.__makeNative(platformConfig); + super.__makeNative(platformConfig); + } + + __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/vnext/src/Libraries/Animated/nodes/AnimatedMultiplication.windows.js b/vnext/src/Libraries/Animated/nodes/AnimatedMultiplication.windows.js new file mode 100644 index 00000000000..add8ff5f33f --- /dev/null +++ b/vnext/src/Libraries/Animated/nodes/AnimatedMultiplication.windows.js @@ -0,0 +1,64 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @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'; +import type {PlatformConfig} from '../AnimatedPlatformConfig'; + +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(platformConfig: ?PlatformConfig) { + this._a.__makeNative(platformConfig); + this._b.__makeNative(platformConfig); + super.__makeNative(platformConfig); + } + + __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/vnext/src/Libraries/Animated/nodes/AnimatedNode.windows.js b/vnext/src/Libraries/Animated/nodes/AnimatedNode.windows.js new file mode 100644 index 00000000000..2098e1733f7 --- /dev/null +++ b/vnext/src/Libraries/Animated/nodes/AnimatedNode.windows.js @@ -0,0 +1,197 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +const NativeAnimatedHelper = require('../NativeAnimatedHelper'); + +const NativeAnimatedAPI = NativeAnimatedHelper.API; +const invariant = require('invariant'); + +import type {PlatformConfig} from '../AnimatedPlatformConfig'; + +type ValueListenerCallback = (state: {value: number, ...}) => mixed; + +let _uniqueId = 1; + +// Note(vjeux): this would be better as an interface but flow doesn't +// support them yet +class AnimatedNode { + _listeners: {[key: string]: ValueListenerCallback, ...}; + _platformConfig: ?PlatformConfig; + __nativeAnimatedValueListener: ?any; + __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; + __shouldUpdateListenersForNewNativeTag: boolean; + + constructor() { + this._listeners = {}; + } + + __makeNative(platformConfig: ?PlatformConfig) { + if (!this.__isNative) { + throw new Error('This node cannot be made a "native" animated node'); + } + + this._platformConfig = platformConfig; + if (this.hasListeners()) { + this._startListeningToNativeValueUpdates(); + } + } + + /** + * 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. + * + * See https://reactnative.dev/docs/animatedvalue#addlistener + */ + addListener(callback: (value: any) => mixed): string { + const id = String(_uniqueId++); + this._listeners[id] = callback; + if (this.__isNative) { + this._startListeningToNativeValueUpdates(); + } + return id; + } + + /** + * Unregister a listener. The `id` param shall match the identifier + * previously returned by `addListener()`. + * + * See https://reactnative.dev/docs/animatedvalue#removelistener + */ + removeListener(id: string): void { + delete this._listeners[id]; + if (this.__isNative && !this.hasListeners()) { + this._stopListeningForNativeValueUpdates(); + } + } + + /** + * Remove all registered listeners. + * + * See https://reactnative.dev/docs/animatedvalue#removealllisteners + */ + removeAllListeners(): void { + this._listeners = {}; + if (this.__isNative) { + this._stopListeningForNativeValueUpdates(); + } + } + + hasListeners(): boolean { + return !!Object.keys(this._listeners).length; + } + + _startListeningToNativeValueUpdates() { + if ( + this.__nativeAnimatedValueListener && + !this.__shouldUpdateListenersForNewNativeTag + ) { + return; + } + + if (this.__shouldUpdateListenersForNewNativeTag) { + this.__shouldUpdateListenersForNewNativeTag = false; + this._stopListeningForNativeValueUpdates(); + } + + NativeAnimatedAPI.startListeningToAnimatedNodeValue(this.__getNativeTag()); + this.__nativeAnimatedValueListener = + NativeAnimatedHelper.nativeEventEmitter.addListener( + 'onAnimatedValueUpdate', + (data) => { + if (data.tag !== this.__getNativeTag()) { + return; + } + this._onAnimatedValueUpdateReceived(data.value); + }, + ); + } + + _onAnimatedValueUpdateReceived(value: number) { + this.__callListeners(value); + } + + __callListeners(value: number): void { + for (const key in this._listeners) { + this._listeners[key]({value}); + } + } + + _stopListeningForNativeValueUpdates() { + if (!this.__nativeAnimatedValueListener) { + return; + } + + this.__nativeAnimatedValueListener.remove(); + this.__nativeAnimatedValueListener = null; + NativeAnimatedAPI.stopListeningToAnimatedNodeValue(this.__getNativeTag()); + } + + __getNativeTag(): number { + NativeAnimatedHelper.assertNativeAnimatedModule(); + invariant( + this.__isNative, + 'Attempt to get native tag from node not marked as "native"', + ); + + const nativeTag = + this.__nativeTag ?? NativeAnimatedHelper.generateNewNodeTag(); + + if (this.__nativeTag == null) { + this.__nativeTag = nativeTag; + const config = this.__getNativeConfig(); + if (this._platformConfig) { + config.platformConfig = this._platformConfig; + } + NativeAnimatedHelper.API.createAnimatedNode(nativeTag, config); + this.__shouldUpdateListenersForNewNativeTag = true; + } + + return nativeTag; + } + __getNativeConfig(): Object { + throw new Error( + 'This JS animated node type cannot be used as native animated node', + ); + } + toJSON(): any { + return this.__getValue(); + } + + __getPlatformConfig(): ?PlatformConfig { + return this._platformConfig; + } + __setPlatformConfig(platformConfig: ?PlatformConfig) { + this._platformConfig = platformConfig; + } +} + +module.exports = AnimatedNode; diff --git a/vnext/src/Libraries/Animated/nodes/AnimatedProps.windows.js b/vnext/src/Libraries/Animated/nodes/AnimatedProps.windows.js new file mode 100644 index 00000000000..e6efc31331a --- /dev/null +++ b/vnext/src/Libraries/Animated/nodes/AnimatedProps.windows.js @@ -0,0 +1,183 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +const {AnimatedEvent} = require('../AnimatedEvent'); +const AnimatedNode = require('./AnimatedNode'); +const AnimatedStyle = require('./AnimatedStyle'); +const NativeAnimatedHelper = require('../NativeAnimatedHelper'); +const ReactNative = require('../../Renderer/shims/ReactNative'); + +const invariant = require('invariant'); + +import type {PlatformConfig} from '../AnimatedPlatformConfig'; + +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; + } + + __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(platformConfig: ?PlatformConfig): void { + if (!this.__isNative) { + this.__isNative = true; + for (const key in this._props) { + const value = this._props[key]; + if (value instanceof AnimatedNode) { + value.__makeNative(platformConfig); + } + } + + // Since this does not call the super.__makeNative, we need to store the + // supplied platformConfig here, before calling __connectAnimatedView + // where it will be needed to traverse the graph of attached values. + super.__setPlatformConfig(platformConfig); + + 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, + ); + } + + __restoreDefaultValues(): void { + // When using the native driver, view properties need to be restored to + // their default values manually since react no longer tracks them. This + // is needed to handle cases where a prop driven by native animated is removed + // after having been changed natively by an animation. + if (this.__isNative) { + NativeAnimatedHelper.API.restoreDefaultValues(this.__getNativeTag()); + } + } + + __getNativeConfig(): Object { + const propsConfig = {}; + for (const propKey in this._props) { + const value = this._props[propKey]; + if (value instanceof AnimatedNode) { + value.__makeNative(this.__getPlatformConfig()); + propsConfig[propKey] = value.__getNativeTag(); + } + } + return { + type: 'props', + props: propsConfig, + }; + } +} + +module.exports = AnimatedProps; diff --git a/vnext/src/Libraries/Animated/nodes/AnimatedStyle.windows.js b/vnext/src/Libraries/Animated/nodes/AnimatedStyle.windows.js new file mode 100644 index 00000000000..c8ea2f2dfb0 --- /dev/null +++ b/vnext/src/Libraries/Animated/nodes/AnimatedStyle.windows.js @@ -0,0 +1,129 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +const AnimatedNode = require('./AnimatedNode'); +const AnimatedTransform = require('./AnimatedTransform'); +const AnimatedWithChildren = require('./AnimatedWithChildren'); +const NativeAnimatedHelper = require('../NativeAnimatedHelper'); + +const flattenStyle = require('../../StyleSheet/flattenStyle'); + +import type {PlatformConfig} from '../AnimatedPlatformConfig'; + +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: any) { + 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: any) { + 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(platformConfig: ?PlatformConfig) { + for (const key in this._style) { + const value = this._style[key]; + if (value instanceof AnimatedNode) { + value.__makeNative(platformConfig); + } + } + super.__makeNative(platformConfig); + } + + __getNativeConfig(): Object { + const styleConfig = {}; + for (const styleKey in this._style) { + if (this._style[styleKey] instanceof AnimatedNode) { + const style = this._style[styleKey]; + style.__makeNative(this.__getPlatformConfig()); + styleConfig[styleKey] = style.__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/vnext/src/Libraries/Animated/nodes/AnimatedSubtraction.windows.js b/vnext/src/Libraries/Animated/nodes/AnimatedSubtraction.windows.js new file mode 100644 index 00000000000..918deac1076 --- /dev/null +++ b/vnext/src/Libraries/Animated/nodes/AnimatedSubtraction.windows.js @@ -0,0 +1,64 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @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'; +import type {PlatformConfig} from '../AnimatedPlatformConfig'; + +class AnimatedSubtraction 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(platformConfig: ?PlatformConfig) { + this._a.__makeNative(platformConfig); + this._b.__makeNative(platformConfig); + super.__makeNative(platformConfig); + } + + __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: 'subtraction', + input: [this._a.__getNativeTag(), this._b.__getNativeTag()], + }; + } +} + +module.exports = AnimatedSubtraction; diff --git a/vnext/src/Libraries/Animated/nodes/AnimatedTracking.windows.js b/vnext/src/Libraries/Animated/nodes/AnimatedTracking.windows.js new file mode 100644 index 00000000000..e3894293028 --- /dev/null +++ b/vnext/src/Libraries/Animated/nodes/AnimatedTracking.windows.js @@ -0,0 +1,107 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +const { + unstable_getDefaultPlatformConfig, +} = require('../AnimatedPlatformConfig'); +const AnimatedValue = require('./AnimatedValue'); +const AnimatedNode = require('./AnimatedNode'); +const { + generateNewAnimationId, + shouldUseNativeDriver, +} = require('../NativeAnimatedHelper'); + +import type {PlatformConfig} from '../AnimatedPlatformConfig'; +import type {EndCallback} from '../animations/Animation'; + +class AnimatedTracking extends AnimatedNode { + _value: AnimatedValue; + _parent: AnimatedNode; + _callback: ?EndCallback; + _animationConfig: Object; + _animationClass: any; + _useNativeDriver: boolean; + + 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._useNativeDriver = shouldUseNativeDriver(animationConfig); + this._callback = callback; + this.__attach(); + } + + __makeNative(platformConfig: ?PlatformConfig) { + this.__isNative = true; + this._parent.__makeNative(platformConfig); + super.__makeNative(platformConfig); + this._value.__makeNative(platformConfig); + } + + __getValue(): Object { + return this._parent.__getValue(); + } + + __attach(): void { + this._parent.__addChild(this); + if (this._useNativeDriver) { + // when the tracking starts we need to convert this node to a "native node" + // so that the parent node will be made "native" too. This is necessary as + // if we don't do this `update` method will get called. At that point it + // may be too late as it would mean the JS driver has already started + // updating node values + let {platformConfig} = this._animationConfig; + this.__makeNative(platformConfig ?? unstable_getDefaultPlatformConfig()); + } + } + + __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, + ); + } + + __getNativeConfig(): any { + const animation = new this._animationClass({ + ...this._animationConfig, + // remove toValue from the config as it's a ref to Animated.Value + toValue: undefined, + }); + const animationConfig = animation.__getNativeAnimationConfig(); + return { + type: 'tracking', + animationId: generateNewAnimationId(), + animationConfig, + toValue: this._parent.__getNativeTag(), + value: this._value.__getNativeTag(), + }; + } +} + +module.exports = AnimatedTracking; diff --git a/vnext/src/Libraries/Animated/nodes/AnimatedTransform.windows.js b/vnext/src/Libraries/Animated/nodes/AnimatedTransform.windows.js new file mode 100644 index 00000000000..d934fbcc566 --- /dev/null +++ b/vnext/src/Libraries/Animated/nodes/AnimatedTransform.windows.js @@ -0,0 +1,123 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +const AnimatedNode = require('./AnimatedNode'); +const AnimatedWithChildren = require('./AnimatedWithChildren'); +const NativeAnimatedHelper = require('../NativeAnimatedHelper'); + +import type {PlatformConfig} from '../AnimatedPlatformConfig'; + +class AnimatedTransform extends AnimatedWithChildren { + _transforms: $ReadOnlyArray; + + constructor(transforms: $ReadOnlyArray) { + super(); + this._transforms = transforms; + } + + __makeNative(platformConfig: ?PlatformConfig) { + this._transforms.forEach((transform) => { + for (const key in transform) { + const value = transform[key]; + if (value instanceof AnimatedNode) { + value.__makeNative(platformConfig); + } + } + }); + super.__makeNative(platformConfig); + } + + __getValue(): $ReadOnlyArray { + 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(): $ReadOnlyArray { + 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.transformDataType(value), + }); + } + } + }); + + NativeAnimatedHelper.validateTransform(transConfigs); + return { + type: 'transform', + transforms: transConfigs, + }; + } +} + +module.exports = AnimatedTransform; diff --git a/vnext/src/Libraries/Animated/nodes/AnimatedWithChildren.windows.js b/vnext/src/Libraries/Animated/nodes/AnimatedWithChildren.windows.js new file mode 100644 index 00000000000..a0737dfa6a1 --- /dev/null +++ b/vnext/src/Libraries/Animated/nodes/AnimatedWithChildren.windows.js @@ -0,0 +1,89 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import type {PlatformConfig} from '../AnimatedPlatformConfig'; +const AnimatedNode = require('./AnimatedNode'); +const NativeAnimatedHelper = require('../NativeAnimatedHelper'); + +class AnimatedWithChildren extends AnimatedNode { + _children: Array; + + constructor() { + super(); + this._children = []; + } + + __makeNative(platformConfig: ?PlatformConfig) { + if (!this.__isNative) { + this.__isNative = true; + for (const child of this._children) { + child.__makeNative(platformConfig); + NativeAnimatedHelper.API.connectAnimatedNodes( + this.__getNativeTag(), + child.__getNativeTag(), + ); + } + } + super.__makeNative(platformConfig); + } + + __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(this.__getPlatformConfig()); + 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; + } + + __callListeners(value: number): void { + super.__callListeners(value); + if (!this.__isNative) { + for (const child of this._children) { + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + if (child.__getValue) { + child.__callListeners(child.__getValue()); + } + } + } + } +} + +module.exports = AnimatedWithChildren;