From 5e863fc42c8a2b27f4a785766eb643de9a243b2d Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Thu, 3 Nov 2022 06:55:39 -0700 Subject: [PATCH] Ship modern animated Summary: changelog: [general][Added] - Concurrent rendering safe implementation of Animated Reviewed By: yungsters Differential Revision: D40681265 fbshipit-source-id: b3979c69342ebd7f232f7a00f279ef0b082d4182 --- Libraries/Animated/NativeAnimatedHelper.js | 10 +- Libraries/Animated/__tests__/Animated-test.js | 14 +- .../createAnimatedComponentInjection-test.js | 68 ----- Libraries/Animated/createAnimatedComponent.js | 265 ++---------------- .../createAnimatedComponentInjection.js | 48 ---- .../createAnimatedComponent_EXPERIMENTAL.js | 48 ---- ...ogBoxInspectorSourceMapStatus-test.js.snap | 4 +- 7 files changed, 42 insertions(+), 415 deletions(-) delete mode 100644 Libraries/Animated/__tests__/createAnimatedComponentInjection-test.js delete mode 100644 Libraries/Animated/createAnimatedComponentInjection.js delete mode 100644 Libraries/Animated/createAnimatedComponent_EXPERIMENTAL.js diff --git a/Libraries/Animated/NativeAnimatedHelper.js b/Libraries/Animated/NativeAnimatedHelper.js index e0c91788c4b830..1e0d9e57611556 100644 --- a/Libraries/Animated/NativeAnimatedHelper.js +++ b/Libraries/Animated/NativeAnimatedHelper.js @@ -142,7 +142,8 @@ const API = { } }, flushQueue: function (): void { - invariant(NativeAnimatedModule, 'Native animated module is not available'); + // TODO: (T136971132) + // invariant(NativeAnimatedModule, 'Native animated module is not available'); flushQueueTimeout = null; // Early returns before calling any APIs @@ -165,16 +166,17 @@ const API = { // use RCTDeviceEventEmitter. This reduces overhead of sending lots of // JSI functions across to native code; but also, TM infrastructure currently // does not support packing a function into native arrays. - NativeAnimatedModule.queueAndExecuteBatchedOperations?.(singleOpQueue); + NativeAnimatedModule?.queueAndExecuteBatchedOperations?.(singleOpQueue); singleOpQueue.length = 0; } else { - Platform.OS === 'android' && NativeAnimatedModule.startOperationBatch?.(); + Platform.OS === 'android' && + NativeAnimatedModule?.startOperationBatch?.(); for (let q = 0, l = queue.length; q < l; q++) { queue[q](); } queue.length = 0; Platform.OS === 'android' && - NativeAnimatedModule.finishOperationBatch?.(); + NativeAnimatedModule?.finishOperationBatch?.(); } }, queueOperation: , Fn: (...Args) => void>( diff --git a/Libraries/Animated/__tests__/Animated-test.js b/Libraries/Animated/__tests__/Animated-test.js index 698dee83e9994a..d02ffa8aad1f8d 100644 --- a/Libraries/Animated/__tests__/Animated-test.js +++ b/Libraries/Animated/__tests__/Animated-test.js @@ -9,10 +9,10 @@ */ import * as React from 'react'; -import TestRenderer from 'react-test-renderer'; let Animated = require('../Animated').default; let AnimatedProps = require('../nodes/AnimatedProps').default; +let TestRenderer = require('react-test-renderer'); jest.mock('../../BatchedBridge/NativeModules', () => ({ NativeAnimatedModule: {}, @@ -175,11 +175,13 @@ describe('Animated tests', () => { expect(testRenderer.toJSON().props.style.opacity).toEqual(0); - Animated.timing(opacity, { - toValue: 1, - duration: 0, - useNativeDriver: false, - }).start(); + TestRenderer.act(() => { + Animated.timing(opacity, { + toValue: 1, + duration: 0, + useNativeDriver: false, + }).start(); + }); expect(testRenderer.toJSON().props.style.opacity).toEqual(1); }); diff --git a/Libraries/Animated/__tests__/createAnimatedComponentInjection-test.js b/Libraries/Animated/__tests__/createAnimatedComponentInjection-test.js deleted file mode 100644 index 15936f7b675a3b..00000000000000 --- a/Libraries/Animated/__tests__/createAnimatedComponentInjection-test.js +++ /dev/null @@ -1,68 +0,0 @@ -/** - * 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 - */ - -'use strict'; - -import * as React from 'react'; - -const createAnimatedComponent = require('../createAnimatedComponent').default; -const createAnimatedComponentInjection = require('../createAnimatedComponentInjection'); - -function injected( - Component: React.AbstractComponent, -): React.AbstractComponent { - return createAnimatedComponent(Component); -} - -beforeEach(() => { - jest.resetModules(); - jest.resetAllMocks(); -}); - -test('does nothing without injection', () => { - expect(typeof createAnimatedComponent).toBe('function'); - expect(createAnimatedComponent).not.toBe(injected); -}); - -test('injection overrides `createAnimatedComponent`', () => { - createAnimatedComponentInjection.inject(injected); - - expect(createAnimatedComponent).toBe(injected); -}); - -test('injection errors if called too late', () => { - jest.spyOn(console, 'error').mockReturnValue(undefined); - - // Causes `createAnimatedComponent` to be initialized. - createAnimatedComponent; - - createAnimatedComponentInjection.inject(injected); - - expect(createAnimatedComponent).not.toBe(injected); - expect(console.error).toBeCalledWith( - 'createAnimatedComponentInjection: Must be called before `createAnimatedComponent`.', - ); -}); - -test('injection errors if called more than once', () => { - jest.spyOn(console, 'error').mockReturnValue(undefined); - - createAnimatedComponentInjection.inject(injected); - - expect(createAnimatedComponent).toBe(injected); - expect(console.error).not.toBeCalled(); - - createAnimatedComponentInjection.inject(injected); - - expect(console.error).toBeCalledWith( - 'createAnimatedComponentInjection: Cannot be called more than once.', - ); -}); diff --git a/Libraries/Animated/createAnimatedComponent.js b/Libraries/Animated/createAnimatedComponent.js index fb64ba05ffd375..09d749c8ca85e0 100644 --- a/Libraries/Animated/createAnimatedComponent.js +++ b/Libraries/Animated/createAnimatedComponent.js @@ -8,19 +8,11 @@ * @format */ -'use strict'; - import View from '../Components/View/View'; -import setAndForwardRef from '../Utilities/setAndForwardRef'; -import {AnimatedEvent} from './AnimatedEvent'; -import * as createAnimatedComponentInjection from './createAnimatedComponentInjection'; -import NativeAnimatedHelper from './NativeAnimatedHelper'; -import AnimatedProps from './nodes/AnimatedProps'; -import invariant from 'invariant'; +import useMergeRefs from '../Utilities/useMergeRefs'; +import useAnimatedProps from './useAnimatedProps'; import * as React from 'react'; -let animatedComponentNextId = 1; - export type AnimatedComponentType< -Props: {+[string]: mixed, ...}, +Instance = mixed, @@ -37,238 +29,33 @@ export type AnimatedComponentType< Instance, >; -function createAnimatedComponent( - Component: React.AbstractComponent, -): AnimatedComponentType { - invariant( - typeof Component !== 'function' || - (Component.prototype && Component.prototype.isReactComponent), - '`createAnimatedComponent` does not support stateless functional components; ' + - 'use a class component instead.', - ); - - class AnimatedComponent extends React.Component { - _component: any; // TODO T53738161: flow type this, and the whole file - _invokeAnimatedPropsCallbackOnMount: boolean = false; - _prevComponent: any; - _propsAnimated: AnimatedProps; - _eventDetachers: Array = []; - - // Only to be used in this file, and only in Fabric. - _animatedComponentId: string = `${animatedComponentNextId++}:animatedComponent`; - - _attachNativeEvents() { - // Make sure to get the scrollable node for components that implement - // `ScrollResponder.Mixin`. - const scrollableNode = this._component?.getScrollableNode - ? this._component.getScrollableNode() - : this._component; - - for (const key in this.props) { - const prop = this.props[key]; - if (prop instanceof AnimatedEvent && prop.__isNative) { - prop.__attach(scrollableNode, key); - this._eventDetachers.push(() => prop.__detach(scrollableNode, key)); - } - } - } - - _detachNativeEvents() { - this._eventDetachers.forEach(remove => remove()); - this._eventDetachers = []; - } - - _isFabric = (): boolean => { - // When called during the first render, `_component` is always null. - // Therefore, even if a component is rendered in Fabric, we can't detect - // that until ref is set, which happens sometime after the first render. - // In cases where this value switching between "false" and "true" on Fabric - // causes issues, add an additional check for _component nullity. - if (this._component == null) { - return false; - } - return ( - // eslint-disable-next-line dot-notation - this._component['_internalInstanceHandle']?.stateNode?.canonical != - null || - // Some components have a setNativeProps function but aren't a host component - // such as lists like FlatList and SectionList. These should also use - // forceUpdate in Fabric since setNativeProps doesn't exist on the underlying - // host component. This crazy hack is essentially special casing those lists and - // ScrollView itself to use forceUpdate in Fabric. - // If these components end up using forwardRef then these hacks can go away - // as this._component would actually be the underlying host component and the above check - // would be sufficient. - (this._component.getNativeScrollRef != null && - this._component.getNativeScrollRef() != null && - // eslint-disable-next-line dot-notation - this._component.getNativeScrollRef()['_internalInstanceHandle'] - ?.stateNode?.canonical != null) || - (this._component.getScrollResponder != null && - this._component.getScrollResponder() != null && - this._component.getScrollResponder().getNativeScrollRef != null && - this._component.getScrollResponder().getNativeScrollRef() != null && - this._component.getScrollResponder().getNativeScrollRef()[ - // eslint-disable-next-line dot-notation - '_internalInstanceHandle' - ]?.stateNode?.canonical != null) - ); - }; - - _waitForUpdate = (): void => { - if (this._isFabric()) { - NativeAnimatedHelper.API.setWaitingForIdentifier( - this._animatedComponentId, - ); - } - }; - - _markUpdateComplete = (): void => { - if (this._isFabric()) { - NativeAnimatedHelper.API.unsetWaitingForIdentifier( - this._animatedComponentId, - ); - } - }; - - // The system is best designed when setNativeProps is implemented. It is - // able to avoid re-rendering and directly set the attributes that changed. - // However, setNativeProps can only be implemented on leaf native - // components. If you want to animate a composite component, you need to - // re-render it. In this case, we have a fallback that uses forceUpdate. - // This fallback is also called in Fabric. - _animatedPropsCallback = (): void => { - if (this._component == null) { - // AnimatedProps is created in will-mount because it's used in render. - // But this callback may be invoked before mount in async mode, - // In which case we should defer the setNativeProps() call. - // React may throw away uncommitted work in async mode, - // So a deferred call won't always be invoked. - this._invokeAnimatedPropsCallbackOnMount = true; - } else if ( - process.env.NODE_ENV === 'test' || - // For animating properties of non-leaf/non-native components - typeof this._component.setNativeProps !== 'function' || - // In Fabric, force animations to go through forceUpdate and skip setNativeProps - this._isFabric() - ) { - this.forceUpdate(); - } else if (!this._propsAnimated.__isNative) { - this._component.setNativeProps( - this._propsAnimated.__getAnimatedValue(), - ); - } else { - throw new Error( - 'Attempting to run JS driven animation on animated ' + - 'node that has been moved to "native" earlier by starting an ' + - 'animation with `useNativeDriver: true`', - ); - } - }; - - _attachProps(nextProps: any) { - const oldPropsAnimated = this._propsAnimated; - - this._propsAnimated = new AnimatedProps( - nextProps, - this._animatedPropsCallback, - ); - this._propsAnimated.__attach(); - - // When you call detach, it removes the element from the parent list - // of children. If it goes to 0, then the parent also detaches itself - // and so on. - // An optimization is to attach the new elements and THEN detach the old - // ones instead of detaching and THEN attaching. - // This way the intermediate state isn't to go to 0 and trigger - // this expensive recursive detaching to then re-attach everything on - // the very next operation. - if (oldPropsAnimated) { - oldPropsAnimated.__restoreDefaultValues(); - oldPropsAnimated.__detach(); - } - } - - _setComponentRef: (ref: React.ElementRef) => void = setAndForwardRef({ - getForwardedRef: () => this.props.forwardedRef, - setLocalRef: ref => { - this._prevComponent = this._component; - this._component = ref; - }, - }); - - render(): React.Node { - const animatedProps = this._propsAnimated.__getValue() || {}; - - const {style = {}, ...props} = animatedProps; - const {style: passthruStyle = {}, ...passthruProps} = - this.props.passthroughAnimatedPropExplicitValues || {}; - const mergedStyle = {...style, ...passthruStyle}; - - // Force `collapsable` to be false so that native view is not flattened. - // Flattened views cannot be accurately referenced by a native driver. - return ( - - ); - } - - UNSAFE_componentWillMount() { - this._waitForUpdate(); - this._attachProps(this.props); - } - - componentDidMount() { - if (this._invokeAnimatedPropsCallbackOnMount) { - this._invokeAnimatedPropsCallbackOnMount = false; - this._animatedPropsCallback(); - } - - this._propsAnimated.setNativeView(this._component); - this._attachNativeEvents(); - this._markUpdateComplete(); - } - - UNSAFE_componentWillReceiveProps(newProps: any) { - this._waitForUpdate(); - this._attachProps(newProps); - } - - componentDidUpdate(prevProps: any) { - if (this._component !== this._prevComponent) { - this._propsAnimated.setNativeView(this._component); - } - if (this._component !== this._prevComponent || prevProps !== this.props) { - this._detachNativeEvents(); - this._attachNativeEvents(); - } - this._markUpdateComplete(); - } - - componentWillUnmount() { - this._propsAnimated && this._propsAnimated.__detach(); - this._detachNativeEvents(); - this._markUpdateComplete(); - this._component = null; - this._prevComponent = null; - } - } +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 React.forwardRef(function AnimatedComponentWrapper(props, ref) { return ( - ); }); } - -// $FlowIgnore[incompatible-cast] - Will be compatible after refactors. -export default (createAnimatedComponentInjection.recordAndRetrieve() ?? - createAnimatedComponent: typeof createAnimatedComponent); diff --git a/Libraries/Animated/createAnimatedComponentInjection.js b/Libraries/Animated/createAnimatedComponentInjection.js deleted file mode 100644 index ab172bf12ca21d..00000000000000 --- a/Libraries/Animated/createAnimatedComponentInjection.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * 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 - */ - -import * as React from 'react'; - -type createAnimatedComponent = ( - Component: React.AbstractComponent, -) => React.AbstractComponent; - -// This can be undefined, null, or the experimental implementation. If this is -// null, that means `createAnimatedComponent` has already been initialized and -// it is too late to call `inject`. -let injected: ?createAnimatedComponent; - -/** - * Call during bundle initialization to opt-in to new `createAnimatedComponent`. - */ -export function inject(newInjected: createAnimatedComponent): void { - if (injected !== undefined) { - if (__DEV__) { - console.error( - 'createAnimatedComponentInjection: ' + - (injected == null - ? 'Must be called before `createAnimatedComponent`.' - : 'Cannot be called more than once.'), - ); - } - return; - } - injected = newInjected; -} - -/** - * Only called by `createAnimatedComponent.js`. - */ -export function recordAndRetrieve(): createAnimatedComponent | null { - if (injected === undefined) { - injected = null; - } - return injected; -} diff --git a/Libraries/Animated/createAnimatedComponent_EXPERIMENTAL.js b/Libraries/Animated/createAnimatedComponent_EXPERIMENTAL.js deleted file mode 100644 index eb7c78cecc815c..00000000000000 --- a/Libraries/Animated/createAnimatedComponent_EXPERIMENTAL.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * 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 - */ - -import StyleSheet from '../StyleSheet/StyleSheet'; -import useMergeRefs from '../Utilities/useMergeRefs'; -import useAnimatedProps from './useAnimatedProps'; -import * as React from 'react'; - -/** - * Experimental implementation of `createAnimatedComponent` that is intended to - * be compatible with concurrent rendering. - */ -export default function createAnimatedComponent( - Component: React.AbstractComponent, -): React.AbstractComponent { - return React.forwardRef((props, forwardedRef) => { - const [reducedProps, callbackRef] = useAnimatedProps( - 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 = StyleSheet.compose(style, passthroughStyle); - - return ( - - ); - }); -} diff --git a/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspectorSourceMapStatus-test.js.snap b/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspectorSourceMapStatus-test.js.snap index a2c2cd03b6a442..d814a90fe50c96 100644 --- a/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspectorSourceMapStatus-test.js.snap +++ b/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspectorSourceMapStatus-test.js.snap @@ -27,7 +27,7 @@ exports[`LogBoxInspectorSourceMapStatus should render for failed 1`] = ` } } > - -