mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Ship modern animated
Summary: changelog: [general][Added] - Concurrent rendering safe implementation of Animated Reviewed By: yungsters Differential Revision: D40681265 fbshipit-source-id: b3979c69342ebd7f232f7a00f279ef0b082d4182
This commit is contained in:
committed by
Facebook GitHub Bot
parent
d9ab5e81cf
commit
5e863fc42c
@@ -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: <Args: $ReadOnlyArray<mixed>, Fn: (...Args) => void>(
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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<TProps: {...}, TInstance>(
|
||||
Component: React.AbstractComponent<TProps, TInstance>,
|
||||
): React.AbstractComponent<TProps, TInstance> {
|
||||
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.',
|
||||
);
|
||||
});
|
||||
@@ -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<Props: {+[string]: mixed, ...}, Instance>(
|
||||
Component: React.AbstractComponent<Props, Instance>,
|
||||
): AnimatedComponentType<Props, Instance> {
|
||||
invariant(
|
||||
typeof Component !== 'function' ||
|
||||
(Component.prototype && Component.prototype.isReactComponent),
|
||||
'`createAnimatedComponent` does not support stateless functional components; ' +
|
||||
'use a class component instead.',
|
||||
);
|
||||
export default function createAnimatedComponent<TProps: {...}, TInstance>(
|
||||
Component: React.AbstractComponent<TProps, TInstance>,
|
||||
): AnimatedComponentType<TProps, TInstance> {
|
||||
return React.forwardRef((props, forwardedRef) => {
|
||||
const [reducedProps, callbackRef] = useAnimatedProps<TProps, TInstance>(
|
||||
// $FlowFixMe[incompatible-call]
|
||||
props,
|
||||
);
|
||||
const ref = useMergeRefs<TInstance | null>(callbackRef, forwardedRef);
|
||||
|
||||
class AnimatedComponent extends React.Component<Object> {
|
||||
_component: any; // TODO T53738161: flow type this, and the whole file
|
||||
_invokeAnimatedPropsCallbackOnMount: boolean = false;
|
||||
_prevComponent: any;
|
||||
_propsAnimated: AnimatedProps;
|
||||
_eventDetachers: Array<Function> = [];
|
||||
// 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};
|
||||
|
||||
// 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<any>) => 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 (
|
||||
<Component
|
||||
{...props}
|
||||
{...passthruProps}
|
||||
collapsable={false}
|
||||
style={mergedStyle}
|
||||
ref={this._setComponentRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return React.forwardRef(function AnimatedComponentWrapper(props, ref) {
|
||||
return (
|
||||
<AnimatedComponent
|
||||
{...props}
|
||||
{...(ref == null ? null : {forwardedRef: ref})}
|
||||
<Component
|
||||
{...reducedProps}
|
||||
{...passthroughProps}
|
||||
style={mergedStyle}
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// $FlowIgnore[incompatible-cast] - Will be compatible after refactors.
|
||||
export default (createAnimatedComponentInjection.recordAndRetrieve() ??
|
||||
createAnimatedComponent: typeof createAnimatedComponent);
|
||||
|
||||
@@ -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 = <TProps: {...}, TInstance>(
|
||||
Component: React.AbstractComponent<TProps, TInstance>,
|
||||
) => React.AbstractComponent<TProps, TInstance>;
|
||||
|
||||
// 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;
|
||||
}
|
||||
@@ -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<TProps: {...}, TInstance>(
|
||||
Component: React.AbstractComponent<TProps, TInstance>,
|
||||
): React.AbstractComponent<TProps, TInstance> {
|
||||
return React.forwardRef((props, forwardedRef) => {
|
||||
const [reducedProps, callbackRef] = useAnimatedProps<TProps, TInstance>(
|
||||
props,
|
||||
);
|
||||
const ref = useMergeRefs<TInstance | null>(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 (
|
||||
<Component
|
||||
{...reducedProps}
|
||||
{...passthroughProps}
|
||||
style={mergedStyle}
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
+2
-2
@@ -27,7 +27,7 @@ exports[`LogBoxInspectorSourceMapStatus should render for failed 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<ForwardRef(AnimatedComponentWrapper)
|
||||
<ForwardRef
|
||||
source={
|
||||
Object {
|
||||
"testUri": "../Libraries/LogBox/UI/LogBoxImages/alert-triangle.png",
|
||||
@@ -94,7 +94,7 @@ exports[`LogBoxInspectorSourceMapStatus should render for pending 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<ForwardRef(AnimatedComponentWrapper)
|
||||
<ForwardRef
|
||||
source={
|
||||
Object {
|
||||
"testUri": "../Libraries/LogBox/UI/LogBoxImages/loader.png",
|
||||
|
||||
Reference in New Issue
Block a user