mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Animated: Manually Manage Connected viewTag (#50230)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/50230 Currently, `AnimatedProps` invokes `findNodeHandle` to both connect and disconnect the native `AnimatedNode` instances to corresponding `viewTag`s. Not only is this slow and wasteful (because `findNodeHandle` requires traversing the fiber tree), but it prevents deferring disconnection to after the fiber tree has been unmounted. Disconnecting after unmount is necessary when the `scheduleAnimatedCleanupInMicrotask` feature flag is enabled, which is necessary to avoid invoking animation completion callbacks in the commit phase that unmounts animated views. I have verified that `disconnectAnimatedNodeFromView` is needed and handles being called after fibers are unmounted. Changelog: [Internal] Reviewed By: javache Differential Revision: D71745805 fbshipit-source-id: ce8c2c95d38c4d5adbb79bac3c07b0872211cf51
This commit is contained in:
committed by
Facebook GitHub Bot
parent
185c809afd
commit
05fe502821
+26
-24
@@ -25,6 +25,10 @@ export type AnimatedPropsAllowlist = $ReadOnly<{
|
||||
[string]: true,
|
||||
}>;
|
||||
|
||||
type TargetView = {
|
||||
+instance: TargetViewInstance,
|
||||
connectedViewTag: ?number,
|
||||
};
|
||||
type TargetViewInstance = React.ElementRef<React.ElementType>;
|
||||
|
||||
function createAnimatedProps(
|
||||
@@ -81,7 +85,7 @@ export default class AnimatedProps extends AnimatedNode {
|
||||
#nodeKeys: $ReadOnlyArray<string>;
|
||||
#nodes: $ReadOnlyArray<AnimatedNode>;
|
||||
#props: {[string]: mixed};
|
||||
#targetInstance: ?TargetViewInstance = null;
|
||||
#target: ?TargetView = null;
|
||||
|
||||
constructor(
|
||||
inputProps: {[string]: mixed},
|
||||
@@ -182,10 +186,10 @@ export default class AnimatedProps extends AnimatedNode {
|
||||
}
|
||||
|
||||
__detach(): void {
|
||||
if (this.__isNative && this.#targetInstance != null) {
|
||||
this.#disconnectAnimatedView(this.#targetInstance);
|
||||
if (this.__isNative && this.#target != null) {
|
||||
this.#disconnectAnimatedView(this.#target);
|
||||
}
|
||||
this.#targetInstance = null;
|
||||
this.#target = null;
|
||||
|
||||
const nodes = this.#nodes;
|
||||
for (let ii = 0, length = nodes.length; ii < length; ii++) {
|
||||
@@ -215,52 +219,50 @@ export default class AnimatedProps extends AnimatedNode {
|
||||
// where it will be needed to traverse the graph of attached values.
|
||||
super.__setPlatformConfig(platformConfig);
|
||||
|
||||
if (this.#targetInstance != null) {
|
||||
this.#connectAnimatedView(this.#targetInstance);
|
||||
if (this.#target != null) {
|
||||
this.#connectAnimatedView(this.#target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setNativeView(targetInstance: TargetViewInstance): void {
|
||||
if (this.#targetInstance === targetInstance) {
|
||||
setNativeView(instance: TargetViewInstance): void {
|
||||
if (this.#target?.instance === instance) {
|
||||
return;
|
||||
}
|
||||
this.#targetInstance = targetInstance;
|
||||
this.#target = {instance, connectedViewTag: null};
|
||||
if (this.__isNative) {
|
||||
this.#connectAnimatedView(this.#targetInstance);
|
||||
this.#connectAnimatedView(this.#target);
|
||||
}
|
||||
}
|
||||
|
||||
#connectAnimatedView(targetInstance: TargetViewInstance): void {
|
||||
#connectAnimatedView(target: TargetView): void {
|
||||
invariant(this.__isNative, 'Expected node to be marked as "native"');
|
||||
let nativeViewTag: ?number = findNodeHandle(targetInstance);
|
||||
if (nativeViewTag == null) {
|
||||
let viewTag: ?number = findNodeHandle(target.instance);
|
||||
if (viewTag == null) {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
nativeViewTag = -1;
|
||||
viewTag = -1;
|
||||
} else {
|
||||
throw new Error('Unable to locate attached view in the native tree');
|
||||
}
|
||||
}
|
||||
NativeAnimatedHelper.API.connectAnimatedNodeToView(
|
||||
this.__getNativeTag(),
|
||||
nativeViewTag,
|
||||
viewTag,
|
||||
);
|
||||
target.connectedViewTag = viewTag;
|
||||
}
|
||||
|
||||
#disconnectAnimatedView(targetInstance: TargetViewInstance): void {
|
||||
#disconnectAnimatedView(target: TargetView): void {
|
||||
invariant(this.__isNative, 'Expected node to be marked as "native"');
|
||||
let nativeViewTag: ?number = findNodeHandle(targetInstance);
|
||||
if (nativeViewTag == null) {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
nativeViewTag = -1;
|
||||
} else {
|
||||
throw new Error('Unable to locate attached view in the native tree');
|
||||
}
|
||||
const viewTag = target.connectedViewTag;
|
||||
if (viewTag == null) {
|
||||
return;
|
||||
}
|
||||
NativeAnimatedHelper.API.disconnectAnimatedNodeFromView(
|
||||
this.__getNativeTag(),
|
||||
nativeViewTag,
|
||||
viewTag,
|
||||
);
|
||||
target.connectedViewTag = null;
|
||||
}
|
||||
|
||||
__restoreDefaultValues(): void {
|
||||
|
||||
@@ -897,7 +897,7 @@ declare export default class AnimatedProps extends AnimatedNode {
|
||||
config?: ?AnimatedNodeConfig
|
||||
): void;
|
||||
update(): void;
|
||||
setNativeView(targetInstance: TargetViewInstance): void;
|
||||
setNativeView(instance: TargetViewInstance): void;
|
||||
}
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 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 'react-native/Libraries/Core/InitializeCore';
|
||||
|
||||
import NativeAnimatedHelper from '../NativeAnimatedHelper';
|
||||
import * as Fantom from '@react-native/fantom';
|
||||
import * as React from 'react';
|
||||
import {Animated} from 'react-native';
|
||||
|
||||
function mockNativeAnimatedHelperAPI() {
|
||||
const mocks = {
|
||||
connectAnimatedNodeToView: jest.fn(),
|
||||
disconnectAnimatedNodeFromView: jest.fn(),
|
||||
};
|
||||
// $FlowFixMe[cannot-write] - Switch to `jest.spyOn` when supported.
|
||||
Object.assign(NativeAnimatedHelper.API, mocks);
|
||||
return mocks;
|
||||
}
|
||||
|
||||
test('connects and disconnects views', () => {
|
||||
const mocks = mockNativeAnimatedHelperAPI();
|
||||
const opacity = new Animated.Value(0);
|
||||
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(<Animated.View style={{opacity}} />);
|
||||
});
|
||||
expect(mocks.connectAnimatedNodeToView).not.toBeCalled();
|
||||
expect(mocks.disconnectAnimatedNodeFromView).not.toBeCalled();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
Animated.timing(opacity, {
|
||||
toValue: 1,
|
||||
duration: 1000,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
});
|
||||
expect(mocks.connectAnimatedNodeToView).toBeCalledTimes(1);
|
||||
expect(mocks.disconnectAnimatedNodeFromView).not.toBeCalled();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.destroy();
|
||||
});
|
||||
|
||||
expect(mocks.connectAnimatedNodeToView).toBeCalledTimes(1);
|
||||
expect(mocks.disconnectAnimatedNodeFromView).toBeCalledTimes(1);
|
||||
});
|
||||
Reference in New Issue
Block a user