diff --git a/packages/react-native/Libraries/Animated/__tests__/AnimatedNode-test.js b/packages/react-native/Libraries/Animated/__tests__/AnimatedValue-test.js similarity index 83% rename from packages/react-native/Libraries/Animated/__tests__/AnimatedNode-test.js rename to packages/react-native/Libraries/Animated/__tests__/AnimatedValue-test.js index feb327b65387..ece374cd86c2 100644 --- a/packages/react-native/Libraries/Animated/__tests__/AnimatedNode-test.js +++ b/packages/react-native/Libraries/Animated/__tests__/AnimatedValue-test.js @@ -8,21 +8,15 @@ * @oncall react_native */ -describe('AnimatedNode', () => { +describe('AnimatedValue', () => { let NativeAnimatedHelper; - let AnimatedNode; - - function createNativeAnimatedNode(): AnimatedNode { - class NativeAnimatedNode extends AnimatedNode { - __isNative = true; - __getNativeConfig(): {} { - return {}; - } - } - return new NativeAnimatedNode(); + let AnimatedValue; + + function createNativeAnimatedValue(): AnimatedValue { + return new AnimatedValue(0, {useNativeDriver: true}); } - function emitMockUpdate(node: AnimatedNode, mockValue: number): void { + function emitMockUpdate(node: AnimatedValue, mockValue: number): void { const nativeTag = node.__nativeTag; expect(nativeTag).not.toBe(undefined); @@ -50,7 +44,7 @@ describe('AnimatedNode', () => { NativeAnimatedHelper = require('../../../src/private/animated/NativeAnimatedHelper').default; - AnimatedNode = require('../nodes/AnimatedNode').default; + AnimatedValue = require('../nodes/AnimatedValue').default; jest.spyOn(NativeAnimatedHelper.API, 'createAnimatedNode'); jest.spyOn(NativeAnimatedHelper.API, 'dropAnimatedNode'); @@ -58,7 +52,7 @@ describe('AnimatedNode', () => { it('emits update events for listeners added', () => { const callback = jest.fn(); - const node = createNativeAnimatedNode(); + const node = createNativeAnimatedValue(); node.__attach(); const id = node.addListener(callback); @@ -75,7 +69,7 @@ describe('AnimatedNode', () => { }); it('creates a native node when adding a listener', () => { - const node = createNativeAnimatedNode(); + const node = createNativeAnimatedValue(); node.__attach(); expect(NativeAnimatedHelper.API.createAnimatedNode).not.toBeCalled(); @@ -85,7 +79,7 @@ describe('AnimatedNode', () => { }); it('drops a created native node on detach', () => { - const node = createNativeAnimatedNode(); + const node = createNativeAnimatedValue(); node.__attach(); expect(NativeAnimatedHelper.API.createAnimatedNode).toBeCalledTimes(0); @@ -100,7 +94,7 @@ describe('AnimatedNode', () => { it('emits update events for listeners added after re-attach', () => { const callbackA = jest.fn(); - const node = createNativeAnimatedNode(); + const node = createNativeAnimatedValue(); node.__attach(); node.addListener(callbackA); diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedAddition.js b/packages/react-native/Libraries/Animated/nodes/AnimatedAddition.js index 5dc40064c8d7..7b885521c289 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedAddition.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedAddition.js @@ -52,6 +52,7 @@ export default class AnimatedAddition extends AnimatedWithChildren { __attach(): void { this._a.__addChild(this); this._b.__addChild(this); + super.__attach(); } __detach(): void { diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedDiffClamp.js b/packages/react-native/Libraries/Animated/nodes/AnimatedDiffClamp.js index 242bd840442d..a192bee7215e 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedDiffClamp.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedDiffClamp.js @@ -60,6 +60,7 @@ export default class AnimatedDiffClamp extends AnimatedWithChildren { __attach(): void { this._a.__addChild(this); + super.__attach(); } __detach(): void { diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedDivision.js b/packages/react-native/Libraries/Animated/nodes/AnimatedDivision.js index 4abbb362f4ea..39c6eff566e2 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedDivision.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedDivision.js @@ -68,6 +68,7 @@ export default class AnimatedDivision extends AnimatedWithChildren { __attach(): void { this._a.__addChild(this); this._b.__addChild(this); + super.__attach(); } __detach(): void { diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedInterpolation.js b/packages/react-native/Libraries/Animated/nodes/AnimatedInterpolation.js index e425d1cbf48c..908790bca5b6 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedInterpolation.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedInterpolation.js @@ -376,6 +376,7 @@ export default class AnimatedInterpolation< __attach(): void { this._parent.__addChild(this); + super.__attach(); } __detach(): void { diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedModulo.js b/packages/react-native/Libraries/Animated/nodes/AnimatedModulo.js index 3dd59b88ee63..0766ae7918c3 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedModulo.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedModulo.js @@ -47,6 +47,7 @@ export default class AnimatedModulo extends AnimatedWithChildren { __attach(): void { this._a.__addChild(this); + super.__attach(); } __detach(): void { diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedMultiplication.js b/packages/react-native/Libraries/Animated/nodes/AnimatedMultiplication.js index 3e2f66efa110..dcf3066e7085 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedMultiplication.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedMultiplication.js @@ -51,6 +51,7 @@ export default class AnimatedMultiplication extends AnimatedWithChildren { __attach(): void { this._a.__addChild(this); this._b.__addChild(this); + super.__attach(); } __detach(): void { diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedNode.js b/packages/react-native/Libraries/Animated/nodes/AnimatedNode.js index e09a2c7edea2..c97222e2e1da 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedNode.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedNode.js @@ -8,15 +8,11 @@ * @format */ -import type {EventSubscription} from '../../vendor/emitter/EventEmitter'; import type {PlatformConfig} from '../AnimatedPlatformConfig'; import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper'; import invariant from 'invariant'; -const {startListeningToAnimatedNodeValue, stopListeningToAnimatedNodeValue} = - NativeAnimatedHelper.API; - type ValueListenerCallback = (state: {value: number, ...}) => mixed; export type AnimatedNodeConfig = $ReadOnly<{ @@ -33,7 +29,6 @@ let _assertNativeAnimatedModule: ?() => void = () => { export default class AnimatedNode { #listeners: Map = new Map(); - #updateSubscription: ?EventSubscription = null; _platformConfig: ?PlatformConfig = undefined; @@ -78,9 +73,6 @@ export default class AnimatedNode { ); this._platformConfig = platformConfig; - if (this.#listeners.size > 0) { - this.#ensureUpdateSubscriptionExists(); - } } /** @@ -93,9 +85,6 @@ export default class AnimatedNode { addListener(callback: (value: any) => mixed): string { const id = String(_uniqueId++); this.#listeners.set(id, callback); - if (this.__isNative) { - this.#ensureUpdateSubscriptionExists(); - } return id; } @@ -107,9 +96,6 @@ export default class AnimatedNode { */ removeListener(id: string): void { this.#listeners.delete(id); - if (this.__isNative && this.#listeners.size === 0) { - this.#updateSubscription?.remove(); - } } /** @@ -119,44 +105,12 @@ export default class AnimatedNode { */ removeAllListeners(): void { this.#listeners.clear(); - if (this.__isNative) { - this.#updateSubscription?.remove(); - } } hasListeners(): boolean { return this.#listeners.size > 0; } - #ensureUpdateSubscriptionExists(): void { - if (this.#updateSubscription != null) { - return; - } - const nativeTag = this.__getNativeTag(); - startListeningToAnimatedNodeValue(nativeTag); - const subscription: EventSubscription = - NativeAnimatedHelper.nativeEventEmitter.addListener( - 'onAnimatedValueUpdate', - data => { - if (data.tag === nativeTag) { - this.__onAnimatedValueUpdateReceived(data.value); - } - }, - ); - - this.#updateSubscription = { - remove: () => { - // Only this function assigns to `this.#updateSubscription`. - if (this.#updateSubscription == null) { - return; - } - this.#updateSubscription = null; - subscription.remove(); - stopListeningToAnimatedNodeValue(nativeTag); - }, - }; - } - __onAnimatedValueUpdateReceived(value: number): void { this.__callListeners(value); } diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedObject.js b/packages/react-native/Libraries/Animated/nodes/AnimatedObject.js index 8077ed9d22fb..ba0aab03c767 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedObject.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedObject.js @@ -136,6 +136,7 @@ export default class AnimatedObject extends AnimatedWithChildren { const node = nodes[ii]; node.__addChild(this); } + super.__attach(); } __detach(): void { diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js b/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js index 80e29a5704c0..82121000cccb 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js @@ -160,6 +160,7 @@ export default class AnimatedProps extends AnimatedNode { const node = nodes[ii]; node.__addChild(this); } + super.__attach(); } __detach(): void { diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedStyle.js b/packages/react-native/Libraries/Animated/nodes/AnimatedStyle.js index 439d1a46fe5e..89a2561ac447 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedStyle.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedStyle.js @@ -201,6 +201,7 @@ export default class AnimatedStyle extends AnimatedWithChildren { const node = nodes[ii]; node.__addChild(this); } + super.__attach(); } __detach(): void { diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedSubtraction.js b/packages/react-native/Libraries/Animated/nodes/AnimatedSubtraction.js index 7e0b9cad884f..39601816e984 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedSubtraction.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedSubtraction.js @@ -52,6 +52,7 @@ export default class AnimatedSubtraction extends AnimatedWithChildren { __attach(): void { this._a.__addChild(this); this._b.__addChild(this); + super.__attach(); } __detach(): void { diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedTracking.js b/packages/react-native/Libraries/Animated/nodes/AnimatedTracking.js index f4541817aedf..8fabf2cc9fdc 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedTracking.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedTracking.js @@ -67,6 +67,7 @@ export default class AnimatedTracking extends AnimatedNode { let {platformConfig} = this._animationConfig; this.__makeNative(platformConfig); } + super.__attach(); } __detach(): void { diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedTransform.js b/packages/react-native/Libraries/Animated/nodes/AnimatedTransform.js index c77a546090f4..b0b79b1b0b62 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedTransform.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedTransform.js @@ -117,6 +117,7 @@ export default class AnimatedTransform extends AnimatedWithChildren { const node = nodes[ii]; node.__addChild(this); } + super.__attach(); } __detach(): void { diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedValue.js b/packages/react-native/Libraries/Animated/nodes/AnimatedValue.js index fe72a33d243d..44122d9fe5bc 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedValue.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedValue.js @@ -8,8 +8,8 @@ * @format */ -'use strict'; - +import type {EventSubscription} from '../../vendor/emitter/EventEmitter'; +import type {PlatformConfig} from '../AnimatedPlatformConfig'; import type Animation, {EndCallback} from '../animations/Animation'; import type {InterpolationConfigType} from './AnimatedInterpolation'; import type AnimatedNode from './AnimatedNode'; @@ -85,6 +85,9 @@ function _executeAsAnimatedBatch(id: string, operation: () => void) { * See https://reactnative.dev/docs/animatedvalue */ export default class AnimatedValue extends AnimatedWithChildren { + #listenerCount: number = 0; + #updateSubscription: ?EventSubscription = null; + _value: number; _startingValue: number; _offset: number; @@ -118,6 +121,67 @@ export default class AnimatedValue extends AnimatedWithChildren { return this._value + this._offset; } + __makeNative(platformConfig: ?PlatformConfig): void { + super.__makeNative(platformConfig); + if (this.#listenerCount > 0) { + this.#ensureUpdateSubscriptionExists(); + } + } + + addListener(callback: (value: any) => mixed): string { + const id = super.addListener(callback); + this.#listenerCount++; + if (this.__isNative) { + this.#ensureUpdateSubscriptionExists(); + } + return id; + } + + removeListener(id: string): void { + super.removeListener(id); + this.#listenerCount--; + if (this.__isNative && this.#listenerCount === 0) { + this.#updateSubscription?.remove(); + } + } + + removeAllListeners(): void { + super.removeAllListeners(); + this.#listenerCount = 0; + if (this.__isNative) { + this.#updateSubscription?.remove(); + } + } + + #ensureUpdateSubscriptionExists(): void { + if (this.#updateSubscription != null) { + return; + } + const nativeTag = this.__getNativeTag(); + NativeAnimatedAPI.startListeningToAnimatedNodeValue(nativeTag); + const subscription: EventSubscription = + NativeAnimatedHelper.nativeEventEmitter.addListener( + 'onAnimatedValueUpdate', + data => { + if (data.tag === nativeTag) { + this.__onAnimatedValueUpdateReceived(data.value); + } + }, + ); + + this.#updateSubscription = { + remove: () => { + // Only this function assigns to `this.#updateSubscription`. + if (this.#updateSubscription == null) { + return; + } + this.#updateSubscription = null; + subscription.remove(); + NativeAnimatedAPI.stopListeningToAnimatedNodeValue(nativeTag); + }, + }; + } + /** * Directly set the value. This will stop any animations running on the value * and update all the bound properties. diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index b56956a0396f..8de483cf2955 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -1168,6 +1168,10 @@ declare export default class AnimatedValue extends AnimatedWithChildren { constructor(value: number, config?: ?AnimatedValueConfig): void; __detach(): void; __getValue(): number; + __makeNative(platformConfig: ?PlatformConfig): void; + addListener(callback: (value: any) => mixed): string; + removeListener(id: string): void; + removeAllListeners(): void; setValue(value: number): void; setOffset(offset: number): void; flattenOffset(): void;