From a9a1df150ee2ffb515c0aba83c46102e5789e040 Mon Sep 17 00:00:00 2001 From: Gabriel Donadel Dall'Agnol Date: Thu, 24 Nov 2022 20:33:29 -0300 Subject: [PATCH] feat: Move Animated to @react-native/animated --- Libraries/Animated/AnimatedMock.js | 186 +---------- Libraries/Animated/createAnimatedComponent.js | 54 +--- Libraries/Animated/nodes/AnimatedValue.js | 293 +---------------- Libraries/Components/ScrollView/ScrollView.js | 2 +- .../ScrollView/ScrollViewStickyHeader.js | 2 +- .../Components/Touchable/TouchableBounce.js | 2 +- .../Components/Touchable/TouchableOpacity.js | 3 +- .../UI/LogBoxInspectorSourceMapStatus.js | 3 +- Libraries/StyleSheet/StyleSheetTypes.js | 2 +- .../StyleSheet/private/_TransformStyle.js | 2 +- index.js | 14 +- package.json | 1 + .../animated}/Animated.d.ts | 19 +- .../animated}/Animated.js | 2 +- .../animated}/AnimatedEvent.js | 2 +- .../animated}/AnimatedImplementation.js | 0 packages/animated/AnimatedMock.js | 195 ++++++++++++ .../animated}/AnimatedPlatformConfig.js | 0 .../animated}/AnimatedWeb.js | 0 .../animated}/Easing.d.ts | 0 .../Animated => packages/animated}/Easing.js | 0 .../animated}/NativeAnimatedHelper.js | 10 +- .../animated}/NativeAnimatedModule.js | 4 +- .../animated}/NativeAnimatedTurboModule.js | 4 +- .../animated}/SpringConfig.js | 0 .../Utilities/__tests__/useMergeRefs-test.js | 256 +++++++++++++++ .../Utilities/__tests__/useRefEffect-test.js | 4 +- packages/animated/Utilities/useMergeRefs.js | 44 +++ .../animated}/Utilities/useRefEffect.js | 0 .../animated}/__tests__/Animated-test.js | 15 +- .../animated}/__tests__/Animated-web-test.js | 4 +- .../animated}/__tests__/AnimatedMock-test.js | 0 .../__tests__/AnimatedNative-test.js | 8 +- .../animated}/__tests__/Easing-test.js | 0 .../animated}/__tests__/Interpolation-test.js | 0 .../__tests__/TimingAnimation-test.js | 0 .../animated}/__tests__/bezier-test.js | 0 .../animated}/animations/Animation.js | 0 .../animated}/animations/DecayAnimation.js | 0 .../animated}/animations/SpringAnimation.js | 0 .../animated}/animations/TimingAnimation.js | 0 .../Animated => packages/animated}/bezier.js | 0 .../animated}/components/AnimatedFlatList.js | 2 +- .../animated}/components/AnimatedImage.js | 2 +- .../components/AnimatedScrollView.js | 16 +- .../components/AnimatedSectionList.js | 2 +- .../animated}/components/AnimatedText.js | 2 +- .../animated}/components/AnimatedView.js | 2 +- packages/animated/createAnimatedComponent.js | 61 ++++ packages/animated/index.js | 44 +++ .../animated}/nodes/AnimatedAddition.js | 0 .../animated}/nodes/AnimatedColor.js | 10 +- .../animated}/nodes/AnimatedDiffClamp.js | 0 .../animated}/nodes/AnimatedDivision.js | 0 .../animated}/nodes/AnimatedInterpolation.js | 2 +- .../animated}/nodes/AnimatedModulo.js | 0 .../animated}/nodes/AnimatedMultiplication.js | 0 .../animated}/nodes/AnimatedNode.js | 1 - .../animated}/nodes/AnimatedProps.js | 2 +- .../animated}/nodes/AnimatedStyle.js | 7 +- .../animated}/nodes/AnimatedSubtraction.js | 0 .../animated}/nodes/AnimatedTracking.js | 0 .../animated}/nodes/AnimatedTransform.js | 0 packages/animated/nodes/AnimatedValue.js | 294 ++++++++++++++++++ .../animated}/nodes/AnimatedValueXY.js | 0 .../animated}/nodes/AnimatedWithChildren.js | 0 packages/animated/package.json | 16 + .../animated}/useAnimatedProps.js | 2 +- .../animated}/useAnimatedValue.d.ts | 0 .../animated}/useAnimatedValue.js | 0 types/index.d.ts | 6 +- 71 files changed, 997 insertions(+), 605 deletions(-) rename {Libraries/Animated => packages/animated}/Animated.d.ts (97%) rename {Libraries/Animated => packages/animated}/Animated.js (97%) rename {Libraries/Animated => packages/animated}/AnimatedEvent.js (99%) rename {Libraries/Animated => packages/animated}/AnimatedImplementation.js (100%) create mode 100644 packages/animated/AnimatedMock.js rename {Libraries/Animated => packages/animated}/AnimatedPlatformConfig.js (100%) rename {Libraries/Animated => packages/animated}/AnimatedWeb.js (100%) rename {Libraries/Animated => packages/animated}/Easing.d.ts (100%) rename {Libraries/Animated => packages/animated}/Easing.js (100%) rename {Libraries/Animated => packages/animated}/NativeAnimatedHelper.js (97%) rename {Libraries/Animated => packages/animated}/NativeAnimatedModule.js (93%) rename {Libraries/Animated => packages/animated}/NativeAnimatedTurboModule.js (93%) rename {Libraries/Animated => packages/animated}/SpringConfig.js (100%) create mode 100644 packages/animated/Utilities/__tests__/useMergeRefs-test.js rename {Libraries => packages/animated}/Utilities/__tests__/useRefEffect-test.js (98%) create mode 100644 packages/animated/Utilities/useMergeRefs.js rename {Libraries => packages/animated}/Utilities/useRefEffect.js (100%) rename {Libraries/Animated => packages/animated}/__tests__/Animated-test.js (98%) rename {Libraries/Animated => packages/animated}/__tests__/Animated-web-test.js (97%) rename {Libraries/Animated => packages/animated}/__tests__/AnimatedMock-test.js (100%) rename {Libraries/Animated => packages/animated}/__tests__/AnimatedNative-test.js (99%) rename {Libraries/Animated => packages/animated}/__tests__/Easing-test.js (100%) rename {Libraries/Animated => packages/animated}/__tests__/Interpolation-test.js (100%) rename {Libraries/Animated => packages/animated}/__tests__/TimingAnimation-test.js (100%) rename {Libraries/Animated => packages/animated}/__tests__/bezier-test.js (100%) rename {Libraries/Animated => packages/animated}/animations/Animation.js (100%) rename {Libraries/Animated => packages/animated}/animations/DecayAnimation.js (100%) rename {Libraries/Animated => packages/animated}/animations/SpringAnimation.js (100%) rename {Libraries/Animated => packages/animated}/animations/TimingAnimation.js (100%) rename {Libraries/Animated => packages/animated}/bezier.js (100%) rename {Libraries/Animated => packages/animated}/components/AnimatedFlatList.js (94%) rename {Libraries/Animated => packages/animated}/components/AnimatedImage.js (93%) rename {Libraries/Animated => packages/animated}/components/AnimatedScrollView.js (87%) rename {Libraries/Animated => packages/animated}/components/AnimatedSectionList.js (94%) rename {Libraries/Animated => packages/animated}/components/AnimatedText.js (93%) rename {Libraries/Animated => packages/animated}/components/AnimatedView.js (92%) create mode 100644 packages/animated/createAnimatedComponent.js create mode 100644 packages/animated/index.js rename {Libraries/Animated => packages/animated}/nodes/AnimatedAddition.js (100%) rename {Libraries/Animated => packages/animated}/nodes/AnimatedColor.js (95%) rename {Libraries/Animated => packages/animated}/nodes/AnimatedDiffClamp.js (100%) rename {Libraries/Animated => packages/animated}/nodes/AnimatedDivision.js (100%) rename {Libraries/Animated => packages/animated}/nodes/AnimatedInterpolation.js (99%) rename {Libraries/Animated => packages/animated}/nodes/AnimatedModulo.js (100%) rename {Libraries/Animated => packages/animated}/nodes/AnimatedMultiplication.js (100%) rename {Libraries/Animated => packages/animated}/nodes/AnimatedNode.js (98%) rename {Libraries/Animated => packages/animated}/nodes/AnimatedProps.js (98%) rename {Libraries/Animated => packages/animated}/nodes/AnimatedStyle.js (95%) rename {Libraries/Animated => packages/animated}/nodes/AnimatedSubtraction.js (100%) rename {Libraries/Animated => packages/animated}/nodes/AnimatedTracking.js (100%) rename {Libraries/Animated => packages/animated}/nodes/AnimatedTransform.js (100%) create mode 100644 packages/animated/nodes/AnimatedValue.js rename {Libraries/Animated => packages/animated}/nodes/AnimatedValueXY.js (100%) rename {Libraries/Animated => packages/animated}/nodes/AnimatedWithChildren.js (100%) create mode 100644 packages/animated/package.json rename {Libraries/Animated => packages/animated}/useAnimatedProps.js (99%) rename {Libraries/Animated => packages/animated}/useAnimatedValue.d.ts (100%) rename {Libraries/Animated => packages/animated}/useAnimatedValue.js (100%) diff --git a/Libraries/Animated/AnimatedMock.js b/Libraries/Animated/AnimatedMock.js index c509c830f7d075..ccf4d21c005211 100644 --- a/Libraries/Animated/AnimatedMock.js +++ b/Libraries/Animated/AnimatedMock.js @@ -4,192 +4,14 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @flow * @format + * @flow */ 'use strict'; -import type {Numeric as AnimatedNumeric} from './AnimatedImplementation'; -import type {EndResult} from './animations/Animation'; -import type {EndCallback} from './animations/Animation'; -import type {DecayAnimationConfig} from './animations/DecayAnimation'; -import type {SpringAnimationConfig} from './animations/SpringAnimation'; -import type {TimingAnimationConfig} from './animations/TimingAnimation'; - -import {AnimatedEvent, attachNativeEvent} from './AnimatedEvent'; -import AnimatedImplementation from './AnimatedImplementation'; -import createAnimatedComponent from './createAnimatedComponent'; -import AnimatedColor from './nodes/AnimatedColor'; -import AnimatedInterpolation from './nodes/AnimatedInterpolation'; -import AnimatedNode from './nodes/AnimatedNode'; -import AnimatedValue from './nodes/AnimatedValue'; -import AnimatedValueXY from './nodes/AnimatedValueXY'; - -/** - * 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: Array) => { - 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 | AnimatedColor, - 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 | AnimatedColor, - 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 | AnimatedColor, - 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, - // $FlowFixMe[prop-missing] - {iterations = -1}: LoopAnimationConfig = {}, -): CompositeAnimation { - return emptyAnimation; -}; +import {AnimatedMock} from '@react-native/animated'; -export type {AnimatedNumeric as Numeric}; +export type * from '@react-native/animated/AnimatedMock'; -export default { - Value: AnimatedValue, - ValueXY: AnimatedValueXY, - Color: AnimatedColor, - 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, - Event: AnimatedEvent, -}; +export default AnimatedMock; diff --git a/Libraries/Animated/createAnimatedComponent.js b/Libraries/Animated/createAnimatedComponent.js index 09d749c8ca85e0..09d60d5e3e8e74 100644 --- a/Libraries/Animated/createAnimatedComponent.js +++ b/Libraries/Animated/createAnimatedComponent.js @@ -4,58 +4,14 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @flow * @format + * @flow */ -import View from '../Components/View/View'; -import useMergeRefs from '../Utilities/useMergeRefs'; -import useAnimatedProps from './useAnimatedProps'; -import * as React from 'react'; - -export type AnimatedComponentType< - -Props: {+[string]: mixed, ...}, - +Instance = mixed, -> = React.AbstractComponent< - $ObjMap< - Props & - $ReadOnly<{ - passthroughAnimatedPropExplicitValues?: React.ElementConfig< - typeof View, - >, - }>, - () => any, - >, - Instance, ->; +'use strict'; -export default function createAnimatedComponent( - Component: React.AbstractComponent, -): AnimatedComponentType { - return React.forwardRef((props, forwardedRef) => { - const [reducedProps, callbackRef] = useAnimatedProps( - // $FlowFixMe[incompatible-call] - props, - ); - const ref = useMergeRefs(callbackRef, forwardedRef); +import {createAnimatedComponent} from '@react-native/animated'; - // Some components require explicit passthrough values for animation - // to work properly. For example, if an animated component is - // transformed and Pressable, onPress will not work after transform - // without these passthrough values. - // $FlowFixMe[prop-missing] - const {passthroughAnimatedPropExplicitValues, style} = reducedProps; - const {style: passthroughStyle, ...passthroughProps} = - passthroughAnimatedPropExplicitValues ?? {}; - const mergedStyle = {...style, ...passthroughStyle}; +export type * from '@react-native/animated/createAnimatedComponent'; - return ( - - ); - }); -} +export default createAnimatedComponent; diff --git a/Libraries/Animated/nodes/AnimatedValue.js b/Libraries/Animated/nodes/AnimatedValue.js index 207dbf69c6e12b..27f5e6615e20f5 100644 --- a/Libraries/Animated/nodes/AnimatedValue.js +++ b/Libraries/Animated/nodes/AnimatedValue.js @@ -4,299 +4,12 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @flow * @format + * @flow */ 'use strict'; -import type Animation, {EndCallback} from '../animations/Animation'; -import type {InterpolationConfigType} from './AnimatedInterpolation'; -import type AnimatedNode from './AnimatedNode'; -import type AnimatedTracking from './AnimatedTracking'; - -import InteractionManager from '../../Interaction/InteractionManager'; -import NativeAnimatedHelper from '../NativeAnimatedHelper'; -import AnimatedInterpolation from './AnimatedInterpolation'; -import AnimatedWithChildren from './AnimatedWithChildren'; - -export type AnimatedValueConfig = $ReadOnly<{ - useNativeDriver: boolean, -}>; - -const NativeAnimatedAPI = NativeAnimatedHelper.API; - -/** - * Animated works by building a directed acyclic graph of dependencies - * transparently when you render your Animated components. - * - * new Animated.Value(0) - * .interpolate() .interpolate() new Animated.Value(1) - * opacity translateY scale - * style transform - * View#234 style - * View#123 - * - * A) Top Down phase - * When an Animated.Value is updated, we recursively go down through this - * graph in order to find leaf nodes: the views that we flag as needing - * an update. - * - * B) Bottom Up phase - * When a view is flagged as needing an update, we recursively go back up - * in order to build the new value that it needs. The reason why we need - * this two-phases process is to deal with composite props such as - * transform which can receive values from multiple parents. - */ -export function flushValue(rootNode: AnimatedNode): void { - const leaves = new Set<{update: () => void, ...}>(); - function findAnimatedStyles(node: AnimatedNode) { - // $FlowFixMe[prop-missing] - if (typeof node.update === 'function') { - leaves.add((node: any)); - } else { - node.__getChildren().forEach(findAnimatedStyles); - } - } - findAnimatedStyles(rootNode); - leaves.forEach(leaf => leaf.update()); -} - -/** - * Some operations are executed only on batch end, which is _mostly_ scheduled when - * Animated component props change. For some of the changes which require immediate execution - * (e.g. setValue), we create a separate batch in case none is scheduled. - */ -function _executeAsAnimatedBatch(id: string, operation: () => void) { - NativeAnimatedAPI.setWaitingForIdentifier(id); - operation(); - NativeAnimatedAPI.unsetWaitingForIdentifier(id); -} - -/** - * Standard value for driving animations. One `Animated.Value` can drive - * multiple properties in a synchronized fashion, but can only be driven by one - * mechanism at a time. Using a new mechanism (e.g. starting a new animation, - * or calling `setValue`) will stop any previous ones. - * - * See https://reactnative.dev/docs/animatedvalue - */ -export default class AnimatedValue extends AnimatedWithChildren { - _value: number; - _startingValue: number; - _offset: number; - _animation: ?Animation; - _tracking: ?AnimatedTracking; - - constructor(value: number, config?: ?AnimatedValueConfig) { - super(); - if (typeof value !== 'number') { - throw new Error('AnimatedValue: Attempting to set value to undefined'); - } - this._startingValue = this._value = value; - this._offset = 0; - this._animation = null; - if (config && config.useNativeDriver) { - this.__makeNative(); - } - } - - __detach() { - if (this.__isNative) { - NativeAnimatedAPI.getValue(this.__getNativeTag(), value => { - this._value = value - this._offset; - }); - } - this.stopAnimation(); - super.__detach(); - } - - __getValue(): number { - return this._value + this._offset; - } - - /** - * Directly set the value. This will stop any animations running on the value - * and update all the bound properties. - * - * See https://reactnative.dev/docs/animatedvalue#setvalue - */ - setValue(value: number): void { - if (this._animation) { - this._animation.stop(); - this._animation = null; - } - this._updateValue( - value, - !this.__isNative /* don't perform a flush for natively driven values */, - ); - if (this.__isNative) { - _executeAsAnimatedBatch(this.__getNativeTag().toString(), () => - NativeAnimatedAPI.setAnimatedNodeValue(this.__getNativeTag(), value), - ); - } - } - - /** - * Sets an offset that is applied on top of whatever value is set, whether via - * `setValue`, an animation, or `Animated.event`. Useful for compensating - * things like the start of a pan gesture. - * - * See https://reactnative.dev/docs/animatedvalue#setoffset - */ - setOffset(offset: number): void { - this._offset = offset; - if (this.__isNative) { - NativeAnimatedAPI.setAnimatedNodeOffset(this.__getNativeTag(), offset); - } - } - - /** - * Merges the offset value into the base value and resets the offset to zero. - * The final output of the value is unchanged. - * - * See https://reactnative.dev/docs/animatedvalue#flattenoffset - */ - flattenOffset(): void { - this._value += this._offset; - this._offset = 0; - if (this.__isNative) { - NativeAnimatedAPI.flattenAnimatedNodeOffset(this.__getNativeTag()); - } - } - - /** - * Sets the offset value to the base value, and resets the base value to zero. - * The final output of the value is unchanged. - * - * See https://reactnative.dev/docs/animatedvalue#extractoffset - */ - extractOffset(): void { - this._offset += this._value; - this._value = 0; - if (this.__isNative) { - NativeAnimatedAPI.extractAnimatedNodeOffset(this.__getNativeTag()); - } - } - - /** - * Stops any running animation or tracking. `callback` is invoked with the - * final value after stopping the animation, which is useful for updating - * state to match the animation position with layout. - * - * See https://reactnative.dev/docs/animatedvalue#stopanimation - */ - stopAnimation(callback?: ?(value: number) => void): void { - this.stopTracking(); - this._animation && this._animation.stop(); - this._animation = null; - if (callback) { - if (this.__isNative) { - NativeAnimatedAPI.getValue(this.__getNativeTag(), callback); - } else { - callback(this.__getValue()); - } - } - } - - /** - * Stops any animation and resets the value to its original. - * - * See https://reactnative.dev/docs/animatedvalue#resetanimation - */ - resetAnimation(callback?: ?(value: number) => void): void { - this.stopAnimation(callback); - this._value = this._startingValue; - if (this.__isNative) { - NativeAnimatedAPI.setAnimatedNodeValue( - this.__getNativeTag(), - this._startingValue, - ); - } - } - - __onAnimatedValueUpdateReceived(value: number): void { - this._updateValue(value, false /*flush*/); - } - - /** - * Interpolates the value before updating the property, e.g. mapping 0-1 to - * 0-10. - */ - interpolate( - config: InterpolationConfigType, - ): AnimatedInterpolation { - return new AnimatedInterpolation(this, config); - } - - /** - * Typically only used internally, but could be used by a custom Animation - * class. - * - * See https://reactnative.dev/docs/animatedvalue#animate - */ - animate(animation: Animation, callback: ?EndCallback): void { - let handle = null; - if (animation.__isInteraction) { - handle = InteractionManager.createInteractionHandle(); - } - const previousAnimation = this._animation; - this._animation && this._animation.stop(); - this._animation = animation; - animation.start( - this._value, - value => { - // Natively driven animations will never call into that callback, therefore we can always - // pass flush = true to allow the updated value to propagate to native with setNativeProps - this._updateValue(value, true /* flush */); - }, - result => { - this._animation = null; - if (handle !== null) { - InteractionManager.clearInteractionHandle(handle); - } - callback && callback(result); - }, - previousAnimation, - this, - ); - } - - /** - * Typically only used internally. - */ - stopTracking(): void { - this._tracking && this._tracking.__detach(); - this._tracking = null; - } - - /** - * Typically only used internally. - */ - track(tracking: AnimatedTracking): void { - this.stopTracking(); - this._tracking = tracking; - // Make sure that the tracking animation starts executing - this._tracking && this._tracking.update(); - } - - _updateValue(value: number, flush: boolean): void { - if (value === undefined) { - throw new Error('AnimatedValue: Attempting to set value to undefined'); - } - - this._value = value; - if (flush) { - flushValue(this); - } - this.__callListeners(this.__getValue()); - } +import {AnimatedValue} from '@react-native/animated'; - __getNativeConfig(): Object { - return { - type: 'value', - value: this._value, - offset: this._offset, - }; - } -} +export default AnimatedValue; diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index eab0ece03c6692..b21f41aa41bb73 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -23,7 +23,6 @@ import type {KeyboardEvent, KeyboardMetrics} from '../Keyboard/Keyboard'; import type {ViewProps} from '../View/ViewPropTypes'; import type {Props as ScrollViewStickyHeaderProps} from './ScrollViewStickyHeader'; -import AnimatedImplementation from '../../Animated/AnimatedImplementation'; import FrameRateLogger from '../../Interaction/FrameRateLogger'; import {findNodeHandle} from '../../ReactNative/RendererProxy'; import UIManager from '../../ReactNative/UIManager'; @@ -44,6 +43,7 @@ import Commands from './ScrollViewCommands'; import ScrollViewContext, {HORIZONTAL, VERTICAL} from './ScrollViewContext'; import ScrollViewNativeComponent from './ScrollViewNativeComponent'; import ScrollViewStickyHeader from './ScrollViewStickyHeader'; +import {AnimatedImplementation} from '@react-native/animated'; import invariant from 'invariant'; import memoize from 'memoize-one'; import * as React from 'react'; diff --git a/Libraries/Components/ScrollView/ScrollViewStickyHeader.js b/Libraries/Components/ScrollView/ScrollViewStickyHeader.js index 9d59cf527f1ddd..1bd342277421f8 100644 --- a/Libraries/Components/ScrollView/ScrollViewStickyHeader.js +++ b/Libraries/Components/ScrollView/ScrollViewStickyHeader.js @@ -10,10 +10,10 @@ import type {LayoutEvent} from '../../Types/CoreEventTypes'; -import Animated from '../../Animated/Animated'; import StyleSheet from '../../StyleSheet/StyleSheet'; import Platform from '../../Utilities/Platform'; import useMergeRefs from '../../Utilities/useMergeRefs'; +import {Animated} from '@react-native/animated'; import * as React from 'react'; import {useCallback, useEffect, useMemo, useRef, useState} from 'react'; diff --git a/Libraries/Components/Touchable/TouchableBounce.js b/Libraries/Components/Touchable/TouchableBounce.js index 5a8d97e85c0f86..232cd65604704e 100644 --- a/Libraries/Components/Touchable/TouchableBounce.js +++ b/Libraries/Components/Touchable/TouchableBounce.js @@ -11,12 +11,12 @@ import type {ViewStyleProp} from '../../StyleSheet/StyleSheet'; import typeof TouchableWithoutFeedback from './TouchableWithoutFeedback'; -import Animated from '../../Animated/Animated'; import Pressability, { type PressabilityConfig, } from '../../Pressability/Pressability'; import {PressabilityDebugView} from '../../Pressability/PressabilityDebug'; import Platform from '../../Utilities/Platform'; +import {Animated} from '@react-native/animated'; import * as React from 'react'; type Props = $ReadOnly<{| diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/Libraries/Components/Touchable/TouchableOpacity.js index cc18e8ecd0db23..fde8c134ccbd71 100644 --- a/Libraries/Components/Touchable/TouchableOpacity.js +++ b/Libraries/Components/Touchable/TouchableOpacity.js @@ -11,14 +11,13 @@ import type {ViewStyleProp} from '../../StyleSheet/StyleSheet'; import typeof TouchableWithoutFeedback from './TouchableWithoutFeedback'; -import Animated from '../../Animated/Animated'; -import Easing from '../../Animated/Easing'; import Pressability, { type PressabilityConfig, } from '../../Pressability/Pressability'; import {PressabilityDebugView} from '../../Pressability/PressabilityDebug'; import flattenStyle from '../../StyleSheet/flattenStyle'; import Platform from '../../Utilities/Platform'; +import {Animated, Easing} from '@react-native/animated'; import * as React from 'react'; type TVProps = $ReadOnly<{| diff --git a/Libraries/LogBox/UI/LogBoxInspectorSourceMapStatus.js b/Libraries/LogBox/UI/LogBoxInspectorSourceMapStatus.js index c5eb157d180b78..1d88566edee4e5 100644 --- a/Libraries/LogBox/UI/LogBoxInspectorSourceMapStatus.js +++ b/Libraries/LogBox/UI/LogBoxInspectorSourceMapStatus.js @@ -10,12 +10,11 @@ import type {PressEvent} from '../../Types/CoreEventTypes'; -import Animated from '../../Animated/Animated'; -import Easing from '../../Animated/Easing'; import StyleSheet from '../../StyleSheet/StyleSheet'; import Text from '../../Text/Text'; import LogBoxButton from './LogBoxButton'; import * as LogBoxStyle from './LogBoxStyle'; +import {Animated, Easing} from '@react-native/animated'; import * as React from 'react'; type Props = $ReadOnly<{| diff --git a/Libraries/StyleSheet/StyleSheetTypes.js b/Libraries/StyleSheet/StyleSheetTypes.js index 6fd7ee822d1b26..eb854923c9a2d8 100644 --- a/Libraries/StyleSheet/StyleSheetTypes.js +++ b/Libraries/StyleSheet/StyleSheetTypes.js @@ -10,7 +10,6 @@ 'use strict'; -import type AnimatedNode from '../Animated/nodes/AnimatedNode'; import type {NativeColorValue} from './PlatformColorValueTypes'; import type { ____DangerouslyImpreciseStyle_InternalOverrides, @@ -20,6 +19,7 @@ import type { ____ViewStyle_InternalOverrides, } from './private/_StyleSheetTypesOverrides'; import type {____TransformStyle_Internal} from './private/_TransformStyle'; +import type {AnimatedNode} from '@react-native/animated'; export type ____ColorValue_Internal = null | string | number | NativeColorValue; export type ColorArrayValue = null | $ReadOnlyArray<____ColorValue_Internal>; diff --git a/Libraries/StyleSheet/private/_TransformStyle.js b/Libraries/StyleSheet/private/_TransformStyle.js index 0a264a393e57b5..bcf4ee5de1b686 100644 --- a/Libraries/StyleSheet/private/_TransformStyle.js +++ b/Libraries/StyleSheet/private/_TransformStyle.js @@ -8,7 +8,7 @@ * @format */ -import type AnimatedNode from '../../Animated/nodes/AnimatedNode'; +import type {AnimatedNode} from '@react-native/animated'; export type ____TransformStyle_Internal = $ReadOnly<{| /** diff --git a/index.js b/index.js index fd55a216d1fba5..d51c00f693d20d 100644 --- a/index.js +++ b/index.js @@ -45,8 +45,8 @@ import typeof VirtualizedSectionList from './Libraries/Lists/VirtualizedSectionL // APIs import typeof ActionSheetIOS from './Libraries/ActionSheetIOS/ActionSheetIOS'; import typeof Alert from './Libraries/Alert/Alert'; -import typeof Animated from './Libraries/Animated/Animated'; -import typeof * as AnimatedModule from './Libraries/Animated/Animated'; +import typeof Animated from '@react-native/animated/Animated'; +import typeof * as AnimatedModule from '@react-native/animated/Animated'; import typeof Appearance from './Libraries/Utilities/Appearance'; import typeof AppRegistry from './Libraries/ReactNative/AppRegistry'; import typeof AppState from './Libraries/AppState/AppState'; @@ -55,7 +55,7 @@ import typeof Clipboard from './Libraries/Components/Clipboard/Clipboard'; import typeof DeviceInfo from './Libraries/Utilities/DeviceInfo'; import typeof DevSettings from './Libraries/Utilities/DevSettings'; import typeof Dimensions from './Libraries/Utilities/Dimensions'; -import typeof Easing from './Libraries/Animated/Easing'; +import typeof Easing from '@react-native/animated/Easing'; import typeof ReactNative from './Libraries/Renderer/shims/ReactNative'; import typeof I18nManager from './Libraries/ReactNative/I18nManager'; import typeof InteractionManager from './Libraries/Interaction/InteractionManager'; @@ -77,7 +77,7 @@ import typeof * as Systrace from './Libraries/Performance/Systrace'; import typeof ToastAndroid from './Libraries/Components/ToastAndroid/ToastAndroid'; import typeof * as TurboModuleRegistry from './Libraries/TurboModule/TurboModuleRegistry'; import typeof UIManager from './Libraries/ReactNative/UIManager'; -import typeof useAnimatedValue from './Libraries/Animated/useAnimatedValue'; +import typeof useAnimatedValue from '@react-native/animated/useAnimatedValue'; import typeof useColorScheme from './Libraries/Utilities/useColorScheme'; import typeof useWindowDimensions from './Libraries/Utilities/useWindowDimensions'; import typeof UTFSequence from './Libraries/UTFSequence'; @@ -228,7 +228,7 @@ module.exports = { // you can references types such as Animated.Numeric get Animated(): {...$Diff, ...Animated} { // $FlowExpectedError[prop-missing]: we only return the default export, all other exports are types - return require('./Libraries/Animated/Animated').default; + return require('@react-native/animated').Animated; }, get Appearance(): Appearance { return require('./Libraries/Utilities/Appearance'); @@ -261,7 +261,7 @@ module.exports = { return require('./Libraries/Utilities/Dimensions'); }, get Easing(): Easing { - return require('./Libraries/Animated/Easing').default; + return require('@react-native/animated').Easing; }, get findNodeHandle(): $PropertyType { return require('./Libraries/ReactNative/RendererProxy').findNodeHandle; @@ -342,7 +342,7 @@ module.exports = { .unstable_batchedUpdates; }, get useAnimatedValue(): useAnimatedValue { - return require('./Libraries/Animated/useAnimatedValue').default; + return require('@react-native/animated').useAnimatedValue; }, get useColorScheme(): useColorScheme { return require('./Libraries/Utilities/useColorScheme').default; diff --git a/package.json b/package.json index c8efaf8ab7c538..3ab13aa81e6c72 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,7 @@ "@react-native-community/cli": "10.0.0-alpha.5", "@react-native-community/cli-platform-android": "10.0.0-alpha.4", "@react-native-community/cli-platform-ios": "10.0.0-alpha.3", + "@react-native/animated": "0.72.0", "@react-native/assets": "1.0.0", "@react-native/gradle-plugin": "^0.72.1", "@react-native/normalize-color": "2.1.0", diff --git a/Libraries/Animated/Animated.d.ts b/packages/animated/Animated.d.ts similarity index 97% rename from Libraries/Animated/Animated.d.ts rename to packages/animated/Animated.d.ts index 215f773cac2411..77a4359f1d73b0 100644 --- a/Libraries/Animated/Animated.d.ts +++ b/packages/animated/Animated.d.ts @@ -8,14 +8,17 @@ */ import type * as React from 'react'; -import {ScrollView} from '../Components/ScrollView/ScrollView'; -import {View} from '../Components/View/View'; -import {Image} from '../Image/Image'; -import {FlatListProps} from '../Lists/FlatList'; -import {DefaultSectionT, SectionListProps} from '../Lists/SectionList'; -import {ColorValue} from '../StyleSheet/StyleSheet'; -import {Text} from '../Text/Text'; -import {NativeSyntheticEvent} from '../Types/CoreEventTypes'; +import { + Image, + Text, + ScrollView, + ColorValue, + View, + FlatListProps, + DefaultSectionT, + SectionListProps, + NativeSyntheticEvent, +} from 'react-native'; export namespace Animated { type AnimatedValue = Value; diff --git a/Libraries/Animated/Animated.js b/packages/animated/Animated.js similarity index 97% rename from Libraries/Animated/Animated.js rename to packages/animated/Animated.js index ec17ef764543ee..a84f0c7b5a0b4c 100644 --- a/Libraries/Animated/Animated.js +++ b/packages/animated/Animated.js @@ -17,7 +17,7 @@ import typeof AnimatedSectionList from './components/AnimatedSectionList'; import typeof AnimatedText from './components/AnimatedText'; import typeof AnimatedView from './components/AnimatedView'; -import Platform from '../Utilities/Platform'; +import {Platform} from 'react-native'; import AnimatedImplementation from './AnimatedImplementation'; import AnimatedMock from './AnimatedMock'; diff --git a/Libraries/Animated/AnimatedEvent.js b/packages/animated/AnimatedEvent.js similarity index 99% rename from Libraries/Animated/AnimatedEvent.js rename to packages/animated/AnimatedEvent.js index 7279c2af4deaa2..9319b744e0dacb 100644 --- a/Libraries/Animated/AnimatedEvent.js +++ b/packages/animated/AnimatedEvent.js @@ -12,7 +12,7 @@ import type {PlatformConfig} from './AnimatedPlatformConfig'; -import {findNodeHandle} from '../ReactNative/RendererProxy'; +import {findNodeHandle} from 'react-native'; import NativeAnimatedHelper from './NativeAnimatedHelper'; import AnimatedValue from './nodes/AnimatedValue'; import AnimatedValueXY from './nodes/AnimatedValueXY'; diff --git a/Libraries/Animated/AnimatedImplementation.js b/packages/animated/AnimatedImplementation.js similarity index 100% rename from Libraries/Animated/AnimatedImplementation.js rename to packages/animated/AnimatedImplementation.js diff --git a/packages/animated/AnimatedMock.js b/packages/animated/AnimatedMock.js new file mode 100644 index 00000000000000..c509c830f7d075 --- /dev/null +++ b/packages/animated/AnimatedMock.js @@ -0,0 +1,195 @@ +/** + * Copyright (c) Meta Platforms, Inc. and 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'; + +import type {Numeric as AnimatedNumeric} from './AnimatedImplementation'; +import type {EndResult} from './animations/Animation'; +import type {EndCallback} from './animations/Animation'; +import type {DecayAnimationConfig} from './animations/DecayAnimation'; +import type {SpringAnimationConfig} from './animations/SpringAnimation'; +import type {TimingAnimationConfig} from './animations/TimingAnimation'; + +import {AnimatedEvent, attachNativeEvent} from './AnimatedEvent'; +import AnimatedImplementation from './AnimatedImplementation'; +import createAnimatedComponent from './createAnimatedComponent'; +import AnimatedColor from './nodes/AnimatedColor'; +import AnimatedInterpolation from './nodes/AnimatedInterpolation'; +import AnimatedNode from './nodes/AnimatedNode'; +import AnimatedValue from './nodes/AnimatedValue'; +import AnimatedValueXY from './nodes/AnimatedValueXY'; + +/** + * 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: Array) => { + 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 | AnimatedColor, + 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 | AnimatedColor, + 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 | AnimatedColor, + 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, + // $FlowFixMe[prop-missing] + {iterations = -1}: LoopAnimationConfig = {}, +): CompositeAnimation { + return emptyAnimation; +}; + +export type {AnimatedNumeric as Numeric}; + +export default { + Value: AnimatedValue, + ValueXY: AnimatedValueXY, + Color: AnimatedColor, + 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, + Event: AnimatedEvent, +}; diff --git a/Libraries/Animated/AnimatedPlatformConfig.js b/packages/animated/AnimatedPlatformConfig.js similarity index 100% rename from Libraries/Animated/AnimatedPlatformConfig.js rename to packages/animated/AnimatedPlatformConfig.js diff --git a/Libraries/Animated/AnimatedWeb.js b/packages/animated/AnimatedWeb.js similarity index 100% rename from Libraries/Animated/AnimatedWeb.js rename to packages/animated/AnimatedWeb.js diff --git a/Libraries/Animated/Easing.d.ts b/packages/animated/Easing.d.ts similarity index 100% rename from Libraries/Animated/Easing.d.ts rename to packages/animated/Easing.d.ts diff --git a/Libraries/Animated/Easing.js b/packages/animated/Easing.js similarity index 100% rename from Libraries/Animated/Easing.js rename to packages/animated/Easing.js diff --git a/Libraries/Animated/NativeAnimatedHelper.js b/packages/animated/NativeAnimatedHelper.js similarity index 97% rename from Libraries/Animated/NativeAnimatedHelper.js rename to packages/animated/NativeAnimatedHelper.js index 7685f22999ff3b..0a69b3cb21e87f 100644 --- a/Libraries/Animated/NativeAnimatedHelper.js +++ b/packages/animated/NativeAnimatedHelper.js @@ -8,7 +8,7 @@ * @format */ -import type {EventSubscription} from '../vendor/emitter/EventEmitter'; +import type {EventSubscription} from 'react-native/Libraries/vendor/emitter/EventEmitter'; import type {EventConfig} from './AnimatedEvent'; import type {AnimationConfig, EndCallback} from './animations/Animation'; import type { @@ -18,10 +18,10 @@ import type { } from './NativeAnimatedModule'; import type {InterpolationConfigType} from './nodes/AnimatedInterpolation'; -import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'; -import RCTDeviceEventEmitter from '../EventEmitter/RCTDeviceEventEmitter'; -import ReactNativeFeatureFlags from '../ReactNative/ReactNativeFeatureFlags'; -import Platform from '../Utilities/Platform'; +import NativeEventEmitter from 'react-native/Libraries/EventEmitter/NativeEventEmitter'; +import RCTDeviceEventEmitter from 'react-native/Libraries/EventEmitter/RCTDeviceEventEmitter'; +import ReactNativeFeatureFlags from 'react-native/Libraries/ReactNative/ReactNativeFeatureFlags'; +import {Platform} from 'react-native'; import NativeAnimatedNonTurboModule from './NativeAnimatedModule'; import NativeAnimatedTurboModule from './NativeAnimatedTurboModule'; import invariant from 'invariant'; diff --git a/Libraries/Animated/NativeAnimatedModule.js b/packages/animated/NativeAnimatedModule.js similarity index 93% rename from Libraries/Animated/NativeAnimatedModule.js rename to packages/animated/NativeAnimatedModule.js index 9fc932e6b509cc..a1a9ca2a1583ce 100644 --- a/Libraries/Animated/NativeAnimatedModule.js +++ b/packages/animated/NativeAnimatedModule.js @@ -8,9 +8,9 @@ * @format */ -import type {TurboModule} from '../TurboModule/RCTExport'; +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; -import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; type EndResult = {finished: boolean, ...}; type EndCallback = (result: EndResult) => void; diff --git a/Libraries/Animated/NativeAnimatedTurboModule.js b/packages/animated/NativeAnimatedTurboModule.js similarity index 93% rename from Libraries/Animated/NativeAnimatedTurboModule.js rename to packages/animated/NativeAnimatedTurboModule.js index 58664ca8742173..37b94ea9955743 100644 --- a/Libraries/Animated/NativeAnimatedTurboModule.js +++ b/packages/animated/NativeAnimatedTurboModule.js @@ -8,9 +8,9 @@ * @format */ -import type {TurboModule} from '../TurboModule/RCTExport'; +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; -import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; type EndResult = {finished: boolean, ...}; type EndCallback = (result: EndResult) => void; diff --git a/Libraries/Animated/SpringConfig.js b/packages/animated/SpringConfig.js similarity index 100% rename from Libraries/Animated/SpringConfig.js rename to packages/animated/SpringConfig.js diff --git a/packages/animated/Utilities/__tests__/useMergeRefs-test.js b/packages/animated/Utilities/__tests__/useMergeRefs-test.js new file mode 100644 index 00000000000000..915884190c57bf --- /dev/null +++ b/packages/animated/Utilities/__tests__/useMergeRefs-test.js @@ -0,0 +1,256 @@ +/** + * Copyright (c) Meta Platforms, Inc. and 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 + * @oncall react_native + */ + +import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes'; +import type {HostComponent} from 'react-native/Libraries/Renderer/shims/ReactNativeTypes'; + +import {View} from 'react-native'; +import useMergeRefs from '../useMergeRefs'; +import * as React from 'react'; +import {act, create} from 'react-test-renderer'; + +/** + * TestView provide a component execution environment to test hooks. + */ +/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's + * LTI update could not be added via codemod */ +function TestView({name, refs}) { + const mergeRef = useMergeRefs(...refs); + return ; +} + +/** + * TestViewInstance provides a pretty-printable replacement for React instances. + */ +class TestViewInstance { + name: string; + + constructor(name: string) { + this.name = name; + } + + // $FlowIgnore[unclear-type] - Intentional. + static fromValue(value: any): ?TestViewInstance { + const testID = value?.props?.testID; + return testID == null ? null : new TestViewInstance(testID); + } + + static named(name: string): $FlowFixMe { + // $FlowIssue[prop-missing] - Flow does not support type augmentation. + return expect.testViewInstance(name); + } +} + +/** + * extend.testViewInstance makes it easier to assert expected values. But use + * TestViewInstance.named instead of extend.testViewInstance because of Flow. + */ +expect.extend({ + testViewInstance(received, name) { + const pass = received instanceof TestViewInstance && received.name === name; + return {pass}; + }, +}); + +/** + * Creates a registry that records the values assigned to the mock refs created + * by either of the two returned callbacks. + */ +function mockRefRegistry(): { + mockCallbackRef: (name: string) => T => mixed, + mockObjectRef: (name: string) => {current: T, ...}, + registry: $ReadOnlyArray<{[string]: T}>, +} { + const registry = []; + return { + mockCallbackRef: + (name: string): (T => mixed) => + current => { + registry.push({[name]: TestViewInstance.fromValue(current)}); + }, + mockObjectRef: (name: string): {current: T, ...} => ({ + // $FlowIgnore[unsafe-getters-setters] - Intentional. + set current(current) { + registry.push({[name]: TestViewInstance.fromValue(current)}); + }, + }), + registry, + }; +} + +test('accepts a callback ref', () => { + let root; + + const {mockCallbackRef, registry} = mockRefRegistry>, + >, + > | null>(); + const refA = mockCallbackRef('refA'); + + act(() => { + root = create(); + }); + + expect(registry).toEqual([{refA: TestViewInstance.named('foo')}]); + + act(() => { + root = create(); + }); + + expect(registry).toEqual([ + {refA: TestViewInstance.named('foo')}, + {refA: TestViewInstance.named('bar')}, + ]); + + act(() => { + root.unmount(); + }); + + expect(registry).toEqual([ + {refA: TestViewInstance.named('foo')}, + {refA: TestViewInstance.named('bar')}, + {refA: null}, + ]); +}); + +test('accepts an object ref', () => { + let root; + + const {mockObjectRef, registry} = mockRefRegistry>, + >, + > | null>(); + const refA = mockObjectRef('refA'); + + act(() => { + root = create(); + }); + + expect(registry).toEqual([{refA: TestViewInstance.named('foo')}]); + + act(() => { + root = create(); + }); + + expect(registry).toEqual([ + {refA: TestViewInstance.named('foo')}, + {refA: TestViewInstance.named('bar')}, + ]); + + act(() => { + root.unmount(); + }); + + expect(registry).toEqual([ + {refA: TestViewInstance.named('foo')}, + {refA: TestViewInstance.named('bar')}, + {refA: null}, + ]); +}); + +test('invokes refs in order', () => { + let root; + + const {mockCallbackRef, mockObjectRef, registry} = + mockRefRegistry>, + >, + > | null>(); + const refA = mockCallbackRef('refA'); + const refB = mockObjectRef('refB'); + const refC = mockCallbackRef('refC'); + const refD = mockObjectRef('refD'); + + act(() => { + root = create(); + }); + + expect(registry).toEqual([ + {refA: TestViewInstance.named('foo')}, + {refB: TestViewInstance.named('foo')}, + {refC: TestViewInstance.named('foo')}, + {refD: TestViewInstance.named('foo')}, + ]); + + act(() => { + root.unmount(); + }); + + expect(registry).toEqual([ + {refA: TestViewInstance.named('foo')}, + {refB: TestViewInstance.named('foo')}, + {refC: TestViewInstance.named('foo')}, + {refD: TestViewInstance.named('foo')}, + {refA: null}, + {refB: null}, + {refC: null}, + {refD: null}, + ]); +}); + +// This is actually undesirable behavior, but it's what we have so let's make +// sure it does not change unexpectedly. +test('invokes all refs if any ref changes', () => { + let root; + + const {mockCallbackRef, registry} = mockRefRegistry>, + >, + > | null>(); + const refA = mockCallbackRef('refA'); + const refB = mockCallbackRef('refB'); + + act(() => { + root = create(); + }); + + expect(registry).toEqual([ + {refA: TestViewInstance.named('foo')}, + {refB: TestViewInstance.named('foo')}, + ]); + + const refAPrime = mockCallbackRef('refAPrime'); + act(() => { + root.update(); + }); + + expect(registry).toEqual([ + {refA: TestViewInstance.named('foo')}, + {refB: TestViewInstance.named('foo')}, + {refA: null}, + {refB: null}, + {refAPrime: TestViewInstance.named('foo')}, + {refB: TestViewInstance.named('foo')}, + ]); + + act(() => { + root.unmount(); + }); + + expect(registry).toEqual([ + {refA: TestViewInstance.named('foo')}, + {refB: TestViewInstance.named('foo')}, + {refA: null}, + {refB: null}, + {refAPrime: TestViewInstance.named('foo')}, + {refB: TestViewInstance.named('foo')}, + {refAPrime: null}, + {refB: null}, + ]); +}); diff --git a/Libraries/Utilities/__tests__/useRefEffect-test.js b/packages/animated/Utilities/__tests__/useRefEffect-test.js similarity index 98% rename from Libraries/Utilities/__tests__/useRefEffect-test.js rename to packages/animated/Utilities/__tests__/useRefEffect-test.js index 5d03e9ae803d92..7d792c3dda2558 100644 --- a/Libraries/Utilities/__tests__/useRefEffect-test.js +++ b/packages/animated/Utilities/__tests__/useRefEffect-test.js @@ -14,9 +14,9 @@ import type { MeasureInWindowOnSuccessCallback, MeasureLayoutOnSuccessCallback, MeasureOnSuccessCallback, -} from '../../Renderer/shims/ReactNativeTypes.js'; +} from '../../../../Libraries/Renderer/shims/ReactNativeTypes.js'; -import View from '../../Components/View/View'; +import View from '../../../../Libraries/Components/View/View'; import useRefEffect from '../useRefEffect'; import * as React from 'react'; import {act, create} from 'react-test-renderer'; diff --git a/packages/animated/Utilities/useMergeRefs.js b/packages/animated/Utilities/useMergeRefs.js new file mode 100644 index 00000000000000..15bd982d036896 --- /dev/null +++ b/packages/animated/Utilities/useMergeRefs.js @@ -0,0 +1,44 @@ +/** + * Copyright (c) Meta Platforms, Inc. and 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 + * @format + */ + +import {useCallback} from 'react'; + +type CallbackRef = T => mixed; +type ObjectRef = {current: T, ...}; + +type Ref = CallbackRef | ObjectRef; + +/** + * Constructs a new ref that forwards new values to each of the given refs. The + * given refs will always be invoked in the order that they are supplied. + * + * WARNING: A known problem of merging refs using this approach is that if any + * of the given refs change, the returned callback ref will also be changed. If + * the returned callback ref is supplied as a `ref` to a React element, this may + * lead to problems with the given refs being invoked more times than desired. + */ +export default function useMergeRefs( + ...refs: $ReadOnlyArray> +): CallbackRef { + return useCallback( + (current: T) => { + for (const ref of refs) { + if (ref != null) { + if (typeof ref === 'function') { + ref(current); + } else { + ref.current = current; + } + } + } + }, + [...refs], // eslint-disable-line react-hooks/exhaustive-deps + ); +} diff --git a/Libraries/Utilities/useRefEffect.js b/packages/animated/Utilities/useRefEffect.js similarity index 100% rename from Libraries/Utilities/useRefEffect.js rename to packages/animated/Utilities/useRefEffect.js diff --git a/Libraries/Animated/__tests__/Animated-test.js b/packages/animated/__tests__/Animated-test.js similarity index 98% rename from Libraries/Animated/__tests__/Animated-test.js rename to packages/animated/__tests__/Animated-test.js index eca451af1ba4dc..8c0f4b164b915b 100644 --- a/Libraries/Animated/__tests__/Animated-test.js +++ b/packages/animated/__tests__/Animated-test.js @@ -15,15 +15,6 @@ let Animated = require('../Animated').default; const AnimatedProps = require('../nodes/AnimatedProps').default; const TestRenderer = require('react-test-renderer'); -jest.mock('../../BatchedBridge/NativeModules', () => ({ - NativeAnimatedModule: {}, - PlatformConstants: { - getConstants() { - return {}; - }, - }, -})); - describe('Animated tests', () => { beforeEach(() => { jest.resetModules(); @@ -698,13 +689,13 @@ describe('Animated tests', () => { let InteractionManager; beforeEach(() => { - jest.mock('../../Interaction/InteractionManager'); + jest.mock('react-native/Libraries/Interaction/InteractionManager'); Animated = require('../Animated').default; - InteractionManager = require('../../Interaction/InteractionManager'); + InteractionManager = require('react-native/Libraries/Interaction/InteractionManager'); }); afterEach(() => { - jest.unmock('../../Interaction/InteractionManager'); + jest.unmock('react-native/Libraries/Interaction/InteractionManager'); }); it('registers an interaction by default', () => { diff --git a/Libraries/Animated/__tests__/Animated-web-test.js b/packages/animated/__tests__/Animated-web-test.js similarity index 97% rename from Libraries/Animated/__tests__/Animated-web-test.js rename to packages/animated/__tests__/Animated-web-test.js index 2fccade525c107..e45546b99c51a6 100644 --- a/Libraries/Animated/__tests__/Animated-web-test.js +++ b/packages/animated/__tests__/Animated-web-test.js @@ -8,11 +8,11 @@ * @oncall react_native */ -const StyleSheet = require('../../StyleSheet/StyleSheet'); +const {StyleSheet} = require('react-native'); let Animated = require('../Animated').default; let AnimatedProps = require('../nodes/AnimatedProps').default; -jest.mock('../../Utilities/Platform', () => { +jest.mock('react-native/Libraries/Utilities/Platform', () => { return {OS: 'web'}; }); diff --git a/Libraries/Animated/__tests__/AnimatedMock-test.js b/packages/animated/__tests__/AnimatedMock-test.js similarity index 100% rename from Libraries/Animated/__tests__/AnimatedMock-test.js rename to packages/animated/__tests__/AnimatedMock-test.js diff --git a/Libraries/Animated/__tests__/AnimatedNative-test.js b/packages/animated/__tests__/AnimatedNative-test.js similarity index 99% rename from Libraries/Animated/__tests__/AnimatedNative-test.js rename to packages/animated/__tests__/AnimatedNative-test.js index 2f7b10af052629..65ff4b2a3b52f0 100644 --- a/Libraries/Animated/__tests__/AnimatedNative-test.js +++ b/packages/animated/__tests__/AnimatedNative-test.js @@ -10,7 +10,7 @@ jest .clearAllMocks() - .mock('../../BatchedBridge/NativeModules', () => ({ + .mock('react-native/Libraries/BatchedBridge/NativeModules', () => ({ NativeAnimatedModule: {}, PlatformConstants: { getConstants() { @@ -19,9 +19,11 @@ jest }, })) .mock('../NativeAnimatedModule') - .mock('../../EventEmitter/NativeEventEmitter') + .mock('react-native/Libraries/EventEmitter/NativeEventEmitter') // findNodeHandle is imported from RendererProxy so mock that whole module. - .setMock('../../ReactNative/RendererProxy', {findNodeHandle: () => 1}); + .setMock('react-native/Libraries/ReactNative/RendererProxy', { + findNodeHandle: () => 1, + }); import * as React from 'react'; import TestRenderer from 'react-test-renderer'; diff --git a/Libraries/Animated/__tests__/Easing-test.js b/packages/animated/__tests__/Easing-test.js similarity index 100% rename from Libraries/Animated/__tests__/Easing-test.js rename to packages/animated/__tests__/Easing-test.js diff --git a/Libraries/Animated/__tests__/Interpolation-test.js b/packages/animated/__tests__/Interpolation-test.js similarity index 100% rename from Libraries/Animated/__tests__/Interpolation-test.js rename to packages/animated/__tests__/Interpolation-test.js diff --git a/Libraries/Animated/__tests__/TimingAnimation-test.js b/packages/animated/__tests__/TimingAnimation-test.js similarity index 100% rename from Libraries/Animated/__tests__/TimingAnimation-test.js rename to packages/animated/__tests__/TimingAnimation-test.js diff --git a/Libraries/Animated/__tests__/bezier-test.js b/packages/animated/__tests__/bezier-test.js similarity index 100% rename from Libraries/Animated/__tests__/bezier-test.js rename to packages/animated/__tests__/bezier-test.js diff --git a/Libraries/Animated/animations/Animation.js b/packages/animated/animations/Animation.js similarity index 100% rename from Libraries/Animated/animations/Animation.js rename to packages/animated/animations/Animation.js diff --git a/Libraries/Animated/animations/DecayAnimation.js b/packages/animated/animations/DecayAnimation.js similarity index 100% rename from Libraries/Animated/animations/DecayAnimation.js rename to packages/animated/animations/DecayAnimation.js diff --git a/Libraries/Animated/animations/SpringAnimation.js b/packages/animated/animations/SpringAnimation.js similarity index 100% rename from Libraries/Animated/animations/SpringAnimation.js rename to packages/animated/animations/SpringAnimation.js diff --git a/Libraries/Animated/animations/TimingAnimation.js b/packages/animated/animations/TimingAnimation.js similarity index 100% rename from Libraries/Animated/animations/TimingAnimation.js rename to packages/animated/animations/TimingAnimation.js diff --git a/Libraries/Animated/bezier.js b/packages/animated/bezier.js similarity index 100% rename from Libraries/Animated/bezier.js rename to packages/animated/bezier.js diff --git a/Libraries/Animated/components/AnimatedFlatList.js b/packages/animated/components/AnimatedFlatList.js similarity index 94% rename from Libraries/Animated/components/AnimatedFlatList.js rename to packages/animated/components/AnimatedFlatList.js index afe8bd88061cec..e121d40645d13d 100644 --- a/Libraries/Animated/components/AnimatedFlatList.js +++ b/packages/animated/components/AnimatedFlatList.js @@ -10,7 +10,7 @@ import type {AnimatedComponentType} from '../createAnimatedComponent'; -import FlatList from '../../Lists/FlatList'; +import {FlatList} from 'react-native'; import createAnimatedComponent from '../createAnimatedComponent'; import * as React from 'react'; diff --git a/Libraries/Animated/components/AnimatedImage.js b/packages/animated/components/AnimatedImage.js similarity index 93% rename from Libraries/Animated/components/AnimatedImage.js rename to packages/animated/components/AnimatedImage.js index c178480fe73a41..f709a7f2df1856 100644 --- a/Libraries/Animated/components/AnimatedImage.js +++ b/packages/animated/components/AnimatedImage.js @@ -10,7 +10,7 @@ import type {AnimatedComponentType} from '../createAnimatedComponent'; -import Image from '../../Image/Image'; +import {Image} from 'react-native'; import createAnimatedComponent from '../createAnimatedComponent'; import * as React from 'react'; diff --git a/Libraries/Animated/components/AnimatedScrollView.js b/packages/animated/components/AnimatedScrollView.js similarity index 87% rename from Libraries/Animated/components/AnimatedScrollView.js rename to packages/animated/components/AnimatedScrollView.js index a3ba2d272caed9..59b38f926084d7 100644 --- a/Libraries/Animated/components/AnimatedScrollView.js +++ b/packages/animated/components/AnimatedScrollView.js @@ -8,16 +8,12 @@ * @format */ -import type {____ViewStyle_Internal} from '../../StyleSheet/StyleSheetTypes'; +import type {____ViewStyle_Internal} from 'react-native/Libraries/StyleSheet/StyleSheetTypes'; import type {AnimatedComponentType} from '../createAnimatedComponent'; -import RefreshControl from '../../Components/RefreshControl/RefreshControl'; -import ScrollView from '../../Components/ScrollView/ScrollView'; -import flattenStyle from '../../StyleSheet/flattenStyle'; -import splitLayoutProps from '../../StyleSheet/splitLayoutProps'; -import StyleSheet from '../../StyleSheet/StyleSheet'; -import Platform from '../../Utilities/Platform'; -import useMergeRefs from '../../Utilities/useMergeRefs'; +import splitLayoutProps from 'react-native/Libraries/StyleSheet/splitLayoutProps'; +import {Platform, RefreshControl, ScrollView, StyleSheet} from 'react-native'; +import useMergeRefs from '../Utilities/useMergeRefs'; import createAnimatedComponent from '../createAnimatedComponent'; import useAnimatedProps from '../useAnimatedProps'; import * as React from 'react'; @@ -73,7 +69,9 @@ const AnimatedScrollViewWithInvertedRefreshControl = React.forwardRef( // and child (ScrollView). const {intermediatePropsForRefreshControl, intermediatePropsForScrollView} = useMemo(() => { - const {outer, inner} = splitLayoutProps(flattenStyle(props.style)); + const {outer, inner} = splitLayoutProps( + StyleSheet.flatten(props.style), + ); return { intermediatePropsForRefreshControl: {style: outer}, intermediatePropsForScrollView: {...props, style: inner}, diff --git a/Libraries/Animated/components/AnimatedSectionList.js b/packages/animated/components/AnimatedSectionList.js similarity index 94% rename from Libraries/Animated/components/AnimatedSectionList.js rename to packages/animated/components/AnimatedSectionList.js index 955691f6e2b278..c225c2ed4e6ad3 100644 --- a/Libraries/Animated/components/AnimatedSectionList.js +++ b/packages/animated/components/AnimatedSectionList.js @@ -10,7 +10,7 @@ import type {AnimatedComponentType} from '../createAnimatedComponent'; -import SectionList from '../../Lists/SectionList'; +import {SectionList} from 'react-native'; import createAnimatedComponent from '../createAnimatedComponent'; import * as React from 'react'; diff --git a/Libraries/Animated/components/AnimatedText.js b/packages/animated/components/AnimatedText.js similarity index 93% rename from Libraries/Animated/components/AnimatedText.js rename to packages/animated/components/AnimatedText.js index 02679f1a8013a9..0bde7b22e49b3d 100644 --- a/Libraries/Animated/components/AnimatedText.js +++ b/packages/animated/components/AnimatedText.js @@ -10,7 +10,7 @@ import type {AnimatedComponentType} from '../createAnimatedComponent'; -import Text from '../../Text/Text'; +import {Text} from 'react-native'; import createAnimatedComponent from '../createAnimatedComponent'; import * as React from 'react'; diff --git a/Libraries/Animated/components/AnimatedView.js b/packages/animated/components/AnimatedView.js similarity index 92% rename from Libraries/Animated/components/AnimatedView.js rename to packages/animated/components/AnimatedView.js index 5eca94417e67da..91b215f646905b 100644 --- a/Libraries/Animated/components/AnimatedView.js +++ b/packages/animated/components/AnimatedView.js @@ -10,7 +10,7 @@ import type {AnimatedComponentType} from '../createAnimatedComponent'; -import View from '../../Components/View/View'; +import {View} from 'react-native'; import createAnimatedComponent from '../createAnimatedComponent'; import * as React from 'react'; diff --git a/packages/animated/createAnimatedComponent.js b/packages/animated/createAnimatedComponent.js new file mode 100644 index 00000000000000..e1701bd6750328 --- /dev/null +++ b/packages/animated/createAnimatedComponent.js @@ -0,0 +1,61 @@ +/** + * Copyright (c) Meta Platforms, Inc. and 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 + */ + +import {View} from 'react-native'; +import useMergeRefs from './Utilities/useMergeRefs'; +import useAnimatedProps from './useAnimatedProps'; +import * as React from 'react'; + +export type AnimatedComponentType< + -Props: {+[string]: mixed, ...}, + +Instance = mixed, +> = React.AbstractComponent< + $ObjMap< + Props & + $ReadOnly<{ + passthroughAnimatedPropExplicitValues?: React.ElementConfig< + typeof View, + >, + }>, + () => any, + >, + Instance, +>; + +export default function createAnimatedComponent( + Component: React.AbstractComponent, +): AnimatedComponentType { + return React.forwardRef((props, forwardedRef) => { + const [reducedProps, callbackRef] = useAnimatedProps( + // $FlowFixMe[incompatible-call] + props, + ); + const ref = useMergeRefs(callbackRef, forwardedRef); + + // Some components require explicit passthrough values for animation + // to work properly. For example, if an animated component is + // transformed and Pressable, onPress will not work after transform + // without these passthrough values. + // $FlowFixMe[prop-missing] + const {passthroughAnimatedPropExplicitValues, style} = reducedProps; + const {style: passthroughStyle, ...passthroughProps} = + passthroughAnimatedPropExplicitValues ?? {}; + const mergedStyle = {...style, ...passthroughStyle}; + + return ( + + ); + }); +} diff --git a/packages/animated/index.js b/packages/animated/index.js new file mode 100644 index 00000000000000..ccde0b212deada --- /dev/null +++ b/packages/animated/index.js @@ -0,0 +1,44 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +'use strict'; + +import typeof Animated from './Animated'; +import typeof AnimatedImplementation from './AnimatedImplementation'; +import typeof AnimatedMock from './AnimatedMock'; +import typeof AnimatedNode from './nodes/AnimatedNode'; +import typeof AnimatedValue from './nodes/AnimatedValue'; +import typeof Easing from './Easing'; +import useAnimatedValue from './useAnimatedValue'; +import createAnimatedComponent from './createAnimatedComponent'; + +module.exports = { + createAnimatedComponent, + useAnimatedValue, + + get Animated(): Animated { + return require('./Animated').default; + }, + get AnimatedImplementation(): AnimatedImplementation { + return require('./AnimatedImplementation').default; + }, + get AnimatedMock(): AnimatedMock { + return require('./AnimatedMock').default; + }, + get AnimatedNode(): AnimatedNode { + return require('./nodes/AnimatedNode').default; + }, + get AnimatedValue(): AnimatedValue { + return require('./nodes/AnimatedValue').default; + }, + get Easing(): Easing { + return require('./Easing').default; + }, +}; diff --git a/Libraries/Animated/nodes/AnimatedAddition.js b/packages/animated/nodes/AnimatedAddition.js similarity index 100% rename from Libraries/Animated/nodes/AnimatedAddition.js rename to packages/animated/nodes/AnimatedAddition.js diff --git a/Libraries/Animated/nodes/AnimatedColor.js b/packages/animated/nodes/AnimatedColor.js similarity index 95% rename from Libraries/Animated/nodes/AnimatedColor.js rename to packages/animated/nodes/AnimatedColor.js index 389a4a82ffe009..3a5508d2f22720 100644 --- a/Libraries/Animated/nodes/AnimatedColor.js +++ b/packages/animated/nodes/AnimatedColor.js @@ -10,13 +10,13 @@ 'use strict'; -import type {NativeColorValue} from '../../StyleSheet/PlatformColorValueTypes'; -import type {ProcessedColorValue} from '../../StyleSheet/processColor'; -import type {ColorValue} from '../../StyleSheet/StyleSheet'; +import type {NativeColorValue} from 'react-native/Libraries/StyleSheet/PlatformColorValueTypes'; +import type {ProcessedColorValue} from 'react-native/Libraries/StyleSheet/processColor'; +import type {ColorValue} from 'react-native/Libraries/StyleSheet/StyleSheet'; import type {PlatformConfig} from '../AnimatedPlatformConfig'; -import normalizeColor from '../../StyleSheet/normalizeColor'; -import {processColorObject} from '../../StyleSheet/PlatformColorValueTypes'; +import normalizeColor from 'react-native/Libraries/StyleSheet/normalizeColor'; +import {processColorObject} from 'react-native/Libraries/StyleSheet/PlatformColorValueTypes'; import NativeAnimatedHelper from '../NativeAnimatedHelper'; import AnimatedValue, {flushValue} from './AnimatedValue'; import AnimatedWithChildren from './AnimatedWithChildren'; diff --git a/Libraries/Animated/nodes/AnimatedDiffClamp.js b/packages/animated/nodes/AnimatedDiffClamp.js similarity index 100% rename from Libraries/Animated/nodes/AnimatedDiffClamp.js rename to packages/animated/nodes/AnimatedDiffClamp.js diff --git a/Libraries/Animated/nodes/AnimatedDivision.js b/packages/animated/nodes/AnimatedDivision.js similarity index 100% rename from Libraries/Animated/nodes/AnimatedDivision.js rename to packages/animated/nodes/AnimatedDivision.js diff --git a/Libraries/Animated/nodes/AnimatedInterpolation.js b/packages/animated/nodes/AnimatedInterpolation.js similarity index 99% rename from Libraries/Animated/nodes/AnimatedInterpolation.js rename to packages/animated/nodes/AnimatedInterpolation.js index be85be6e89d742..094e7a9e022641 100644 --- a/Libraries/Animated/nodes/AnimatedInterpolation.js +++ b/packages/animated/nodes/AnimatedInterpolation.js @@ -15,7 +15,7 @@ import type {PlatformConfig} from '../AnimatedPlatformConfig'; import type AnimatedNode from './AnimatedNode'; -import normalizeColor from '../../StyleSheet/normalizeColor'; +import normalizeColor from 'react-native/Libraries/StyleSheet/normalizeColor'; import NativeAnimatedHelper from '../NativeAnimatedHelper'; import AnimatedWithChildren from './AnimatedWithChildren'; import invariant from 'invariant'; diff --git a/Libraries/Animated/nodes/AnimatedModulo.js b/packages/animated/nodes/AnimatedModulo.js similarity index 100% rename from Libraries/Animated/nodes/AnimatedModulo.js rename to packages/animated/nodes/AnimatedModulo.js diff --git a/Libraries/Animated/nodes/AnimatedMultiplication.js b/packages/animated/nodes/AnimatedMultiplication.js similarity index 100% rename from Libraries/Animated/nodes/AnimatedMultiplication.js rename to packages/animated/nodes/AnimatedMultiplication.js diff --git a/Libraries/Animated/nodes/AnimatedNode.js b/packages/animated/nodes/AnimatedNode.js similarity index 98% rename from Libraries/Animated/nodes/AnimatedNode.js rename to packages/animated/nodes/AnimatedNode.js index a4fadb5ab7ecf3..bd9d7b032e8b12 100644 --- a/Libraries/Animated/nodes/AnimatedNode.js +++ b/packages/animated/nodes/AnimatedNode.js @@ -12,7 +12,6 @@ import type {PlatformConfig} from '../AnimatedPlatformConfig'; -import ReactNativeFeatureFlags from '../../ReactNative/ReactNativeFeatureFlags'; import NativeAnimatedHelper from '../NativeAnimatedHelper'; import invariant from 'invariant'; diff --git a/Libraries/Animated/nodes/AnimatedProps.js b/packages/animated/nodes/AnimatedProps.js similarity index 98% rename from Libraries/Animated/nodes/AnimatedProps.js rename to packages/animated/nodes/AnimatedProps.js index ef15cdf13120f6..16171b1da57d2b 100644 --- a/Libraries/Animated/nodes/AnimatedProps.js +++ b/packages/animated/nodes/AnimatedProps.js @@ -12,7 +12,7 @@ import type {PlatformConfig} from '../AnimatedPlatformConfig'; -import {findNodeHandle} from '../../ReactNative/RendererProxy'; +import {findNodeHandle} from 'react-native'; import {AnimatedEvent} from '../AnimatedEvent'; import NativeAnimatedHelper from '../NativeAnimatedHelper'; import AnimatedNode from './AnimatedNode'; diff --git a/Libraries/Animated/nodes/AnimatedStyle.js b/packages/animated/nodes/AnimatedStyle.js similarity index 95% rename from Libraries/Animated/nodes/AnimatedStyle.js rename to packages/animated/nodes/AnimatedStyle.js index 31291968f16e15..7e2224542902a9 100644 --- a/Libraries/Animated/nodes/AnimatedStyle.js +++ b/packages/animated/nodes/AnimatedStyle.js @@ -12,15 +12,14 @@ import type {PlatformConfig} from '../AnimatedPlatformConfig'; -import flattenStyle from '../../StyleSheet/flattenStyle'; -import Platform from '../../Utilities/Platform'; +import {Platform, StyleSheet} from 'react-native'; import NativeAnimatedHelper from '../NativeAnimatedHelper'; import AnimatedNode from './AnimatedNode'; import AnimatedTransform from './AnimatedTransform'; import AnimatedWithChildren from './AnimatedWithChildren'; function createAnimatedStyle(inputStyle: any): Object { - const style = flattenStyle(inputStyle); + const style = StyleSheet.flatten(inputStyle); const animatedStyles: any = {}; for (const key in style) { const value = style[key]; @@ -36,7 +35,7 @@ function createAnimatedStyle(inputStyle: any): Object { } function createStyleWithAnimatedTransform(inputStyle: any): Object { - let style = flattenStyle(inputStyle) || ({}: {[string]: any}); + let style = StyleSheet.flatten(inputStyle) || ({}: {[string]: any}); if (style.transform) { style = { diff --git a/Libraries/Animated/nodes/AnimatedSubtraction.js b/packages/animated/nodes/AnimatedSubtraction.js similarity index 100% rename from Libraries/Animated/nodes/AnimatedSubtraction.js rename to packages/animated/nodes/AnimatedSubtraction.js diff --git a/Libraries/Animated/nodes/AnimatedTracking.js b/packages/animated/nodes/AnimatedTracking.js similarity index 100% rename from Libraries/Animated/nodes/AnimatedTracking.js rename to packages/animated/nodes/AnimatedTracking.js diff --git a/Libraries/Animated/nodes/AnimatedTransform.js b/packages/animated/nodes/AnimatedTransform.js similarity index 100% rename from Libraries/Animated/nodes/AnimatedTransform.js rename to packages/animated/nodes/AnimatedTransform.js diff --git a/packages/animated/nodes/AnimatedValue.js b/packages/animated/nodes/AnimatedValue.js new file mode 100644 index 00000000000000..cb34a351ec3e49 --- /dev/null +++ b/packages/animated/nodes/AnimatedValue.js @@ -0,0 +1,294 @@ +/** + * Copyright (c) Meta Platforms, Inc. and 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'; + +import type Animation, {EndCallback} from '../animations/Animation'; +import type {InterpolationConfigType} from './AnimatedInterpolation'; +import type AnimatedNode from './AnimatedNode'; +import type AnimatedTracking from './AnimatedTracking'; + +import {InteractionManager} from 'react-native'; +import NativeAnimatedHelper from '../NativeAnimatedHelper'; +import AnimatedInterpolation from './AnimatedInterpolation'; +import AnimatedWithChildren from './AnimatedWithChildren'; + +export type AnimatedValueConfig = $ReadOnly<{ + useNativeDriver: boolean, +}>; + +const NativeAnimatedAPI = NativeAnimatedHelper.API; + +/** + * Animated works by building a directed acyclic graph of dependencies + * transparently when you render your Animated components. + * + * new Animated.Value(0) + * .interpolate() .interpolate() new Animated.Value(1) + * opacity translateY scale + * style transform + * View#234 style + * View#123 + * + * A) Top Down phase + * When an Animated.Value is updated, we recursively go down through this + * graph in order to find leaf nodes: the views that we flag as needing + * an update. + * + * B) Bottom Up phase + * When a view is flagged as needing an update, we recursively go back up + * in order to build the new value that it needs. The reason why we need + * this two-phases process is to deal with composite props such as + * transform which can receive values from multiple parents. + */ +export function flushValue(rootNode: AnimatedNode): void { + const leaves = new Set<{update: () => void, ...}>(); + function findAnimatedStyles(node: AnimatedNode) { + // $FlowFixMe[prop-missing] + if (typeof node.update === 'function') { + leaves.add((node: any)); + } else { + node.__getChildren().forEach(findAnimatedStyles); + } + } + findAnimatedStyles(rootNode); + leaves.forEach(leaf => leaf.update()); +} + +/** + * Some operations are executed only on batch end, which is _mostly_ scheduled when + * Animated component props change. For some of the changes which require immediate execution + * (e.g. setValue), we create a separate batch in case none is scheduled. + */ +function _executeAsAnimatedBatch(id: string, operation: () => void) { + NativeAnimatedAPI.setWaitingForIdentifier(id); + operation(); + NativeAnimatedAPI.unsetWaitingForIdentifier(id); +} + +/** + * Standard value for driving animations. One `Animated.Value` can drive + * multiple properties in a synchronized fashion, but can only be driven by one + * mechanism at a time. Using a new mechanism (e.g. starting a new animation, + * or calling `setValue`) will stop any previous ones. + * + * See https://reactnative.dev/docs/animatedvalue + */ +export default class AnimatedValue extends AnimatedWithChildren { + _value: number; + _startingValue: number; + _offset: number; + _animation: ?Animation; + _tracking: ?AnimatedTracking; + + constructor(value: number, config?: ?AnimatedValueConfig) { + super(); + if (typeof value !== 'number') { + throw new Error('AnimatedValue: Attempting to set value to undefined'); + } + this._startingValue = this._value = value; + this._offset = 0; + this._animation = null; + if (config && config.useNativeDriver) { + this.__makeNative(); + } + } + + __detach() { + if (this.__isNative) { + NativeAnimatedAPI.getValue(this.__getNativeTag(), value => { + this._value = value - this._offset; + }); + } + this.stopAnimation(); + super.__detach(); + } + + __getValue(): number { + return this._value + this._offset; + } + + /** + * Directly set the value. This will stop any animations running on the value + * and update all the bound properties. + * + * See https://reactnative.dev/docs/animatedvalue#setvalue + */ + setValue(value: number): void { + if (this._animation) { + this._animation.stop(); + this._animation = null; + } + this._updateValue( + value, + !this.__isNative /* don't perform a flush for natively driven values */, + ); + if (this.__isNative) { + _executeAsAnimatedBatch(this.__getNativeTag().toString(), () => + NativeAnimatedAPI.setAnimatedNodeValue(this.__getNativeTag(), value), + ); + } + } + + /** + * Sets an offset that is applied on top of whatever value is set, whether via + * `setValue`, an animation, or `Animated.event`. Useful for compensating + * things like the start of a pan gesture. + * + * See https://reactnative.dev/docs/animatedvalue#setoffset + */ + setOffset(offset: number): void { + this._offset = offset; + if (this.__isNative) { + NativeAnimatedAPI.setAnimatedNodeOffset(this.__getNativeTag(), offset); + } + } + + /** + * Merges the offset value into the base value and resets the offset to zero. + * The final output of the value is unchanged. + * + * See https://reactnative.dev/docs/animatedvalue#flattenoffset + */ + flattenOffset(): void { + this._value += this._offset; + this._offset = 0; + if (this.__isNative) { + NativeAnimatedAPI.flattenAnimatedNodeOffset(this.__getNativeTag()); + } + } + + /** + * Sets the offset value to the base value, and resets the base value to zero. + * The final output of the value is unchanged. + * + * See https://reactnative.dev/docs/animatedvalue#extractoffset + */ + extractOffset(): void { + this._offset += this._value; + this._value = 0; + if (this.__isNative) { + NativeAnimatedAPI.extractAnimatedNodeOffset(this.__getNativeTag()); + } + } + + /** + * Stops any running animation or tracking. `callback` is invoked with the + * final value after stopping the animation, which is useful for updating + * state to match the animation position with layout. + * + * See https://reactnative.dev/docs/animatedvalue#stopanimation + */ + stopAnimation(callback?: ?(value: number) => void): void { + this.stopTracking(); + this._animation && this._animation.stop(); + this._animation = null; + if (callback) { + if (this.__isNative) { + NativeAnimatedAPI.getValue(this.__getNativeTag(), callback); + } else { + callback(this.__getValue()); + } + } + } + + /** + * Stops any animation and resets the value to its original. + * + * See https://reactnative.dev/docs/animatedvalue#resetanimation + */ + resetAnimation(callback?: ?(value: number) => void): void { + this.stopAnimation(callback); + this._value = this._startingValue; + if (this.__isNative) { + NativeAnimatedAPI.setAnimatedNodeValue( + this.__getNativeTag(), + this._startingValue, + ); + } + } + + __onAnimatedValueUpdateReceived(value: number): void { + this._updateValue(value, false /*flush*/); + } + + /** + * Interpolates the value before updating the property, e.g. mapping 0-1 to + * 0-10. + */ + interpolate( + config: InterpolationConfigType, + ): AnimatedInterpolation { + return new AnimatedInterpolation(this, config); + } + + /** + * Typically only used internally, but could be used by a custom Animation + * class. + * + * See https://reactnative.dev/docs/animatedvalue#animate + */ + animate(animation: Animation, callback: ?EndCallback): void { + let handle = null; + if (animation.__isInteraction) { + handle = InteractionManager.createInteractionHandle(); + } + const previousAnimation = this._animation; + this._animation && this._animation.stop(); + this._animation = animation; + animation.start( + this._value, + value => { + // Natively driven animations will never call into that callback, therefore we can always + // pass flush = true to allow the updated value to propagate to native with setNativeProps + this._updateValue(value, true /* flush */); + }, + result => { + this._animation = null; + if (handle !== null) { + InteractionManager.clearInteractionHandle(handle); + } + callback && callback(result); + }, + previousAnimation, + this, + ); + } + + /** + * Typically only used internally. + */ + stopTracking(): void { + this._tracking && this._tracking.__detach(); + this._tracking = null; + } + + /** + * Typically only used internally. + */ + track(tracking: AnimatedTracking): void { + this.stopTracking(); + this._tracking = tracking; + // Make sure that the tracking animation starts executing + this._tracking && this._tracking.update(); + } + + _updateValue(value: number, flush: boolean): void { + if (value === undefined) { + throw new Error('AnimatedValue: Attempting to set value to undefined'); + } + + this._value = value; + if (flush) { + flushValue(this); + } + this.__callListeners(this.__getValue()); + } +} diff --git a/Libraries/Animated/nodes/AnimatedValueXY.js b/packages/animated/nodes/AnimatedValueXY.js similarity index 100% rename from Libraries/Animated/nodes/AnimatedValueXY.js rename to packages/animated/nodes/AnimatedValueXY.js diff --git a/Libraries/Animated/nodes/AnimatedWithChildren.js b/packages/animated/nodes/AnimatedWithChildren.js similarity index 100% rename from Libraries/Animated/nodes/AnimatedWithChildren.js rename to packages/animated/nodes/AnimatedWithChildren.js diff --git a/packages/animated/package.json b/packages/animated/package.json new file mode 100644 index 00000000000000..c3b9deee74bf09 --- /dev/null +++ b/packages/animated/package.json @@ -0,0 +1,16 @@ +{ + "name": "@react-native/animated", + "version": "0.72.0", + "description": "Fluid animations for React Native.", + "repository": { + "type": "git", + "url": "git@github.com:facebook/react-native.git", + "directory": "packages/animated" + }, + "license": "MIT", + "peerDependencies": { + "react": "18.2.0", + "react-native": "*", + "react-test-renderer": "18.2.0" + } +} diff --git a/Libraries/Animated/useAnimatedProps.js b/packages/animated/useAnimatedProps.js similarity index 99% rename from Libraries/Animated/useAnimatedProps.js rename to packages/animated/useAnimatedProps.js index 1b7d81009fd885..e5fc35bbb06839 100644 --- a/Libraries/Animated/useAnimatedProps.js +++ b/packages/animated/useAnimatedProps.js @@ -10,7 +10,7 @@ 'use strict'; -import useRefEffect from '../Utilities/useRefEffect'; +import useRefEffect from './Utilities/useRefEffect'; import {AnimatedEvent} from './AnimatedEvent'; import NativeAnimatedHelper from './NativeAnimatedHelper'; import AnimatedProps from './nodes/AnimatedProps'; diff --git a/Libraries/Animated/useAnimatedValue.d.ts b/packages/animated/useAnimatedValue.d.ts similarity index 100% rename from Libraries/Animated/useAnimatedValue.d.ts rename to packages/animated/useAnimatedValue.d.ts diff --git a/Libraries/Animated/useAnimatedValue.js b/packages/animated/useAnimatedValue.js similarity index 100% rename from Libraries/Animated/useAnimatedValue.js rename to packages/animated/useAnimatedValue.js diff --git a/types/index.d.ts b/types/index.d.ts index 6ef79292429588..5a1e7868063792 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -73,9 +73,9 @@ export * from '../Libraries/ActionSheetIOS/ActionSheetIOS'; export * from '../Libraries/Alert/Alert'; -export * from '../Libraries/Animated/Animated'; -export * from '../Libraries/Animated/Easing'; -export * from '../Libraries/Animated/useAnimatedValue'; +export * from '@react-native/animated/Animated'; +export * from '@react-native/animated/Easing'; +export * from '@react-native/animated/useAnimatedValue'; export * from '../Libraries/AppState/AppState'; export * from '../Libraries/BatchedBridge/NativeModules'; export * from '../Libraries/Components/AccessibilityInfo/AccessibilityInfo';