From 2861c52e2968e573ebd2a725a49ce98659a48807 Mon Sep 17 00:00:00 2001 From: Mad Dinh Date: Fri, 21 Aug 2026 12:08:31 +0700 Subject: [PATCH] Fix Animated listeners on nodes derived from Animated.Value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `addListener` stopped firing for natively driven animations on nodes derived from an `Animated.Value` — the `Animated.add`/`subtract`/ `multiply`/`divide`/`modulo`/`diffClamp` operators and `.interpolate()`. This regressed in 0.78 with 38c46fe86530, which moved the native value-update subscription from `AnimatedNode` down into `AnimatedValue` because `startListeningToAnimatedNodeValue` only accepts "value" node tags. The operator and interpolation nodes are value nodes natively (they all derive from `ValueAnimatedNode` / `RCTValueAnimatedNode`), so they lost a subscription they were entitled to. Restore the subscription on `AnimatedNode`, gated on a new `__isNativeValueNode` flag that only nodes backed by a native value node set, preserving the "never listen to a non-value tag" guarantee. Drop the subscription in `__detach` so it cannot outlive its native tag. The C++ backend also rejected these nodes: it compared the exact `AnimatedNodeType::Value` tag rather than testing for a `ValueAnimatedNode` subclass. Replace that with an exhaustive `isValueNodeType` predicate. Fixes #49719 --- .../__tests__/AnimatedComposition-itest.js | 112 ++++++++++++++++++ .../Animated/nodes/AnimatedAddition.js | 2 + .../Animated/nodes/AnimatedDiffClamp.js | 2 + .../Animated/nodes/AnimatedDivision.js | 2 + .../Animated/nodes/AnimatedInterpolation.js | 2 + .../Animated/nodes/AnimatedModulo.js | 2 + .../Animated/nodes/AnimatedMultiplication.js | 2 + .../Libraries/Animated/nodes/AnimatedNode.js | 73 +++++++++++- .../Animated/nodes/AnimatedSubtraction.js | 2 + .../Libraries/Animated/nodes/AnimatedValue.js | 76 ++---------- .../animated/NativeAnimatedNodesManager.cpp | 35 +++++- .../animated/tests/AnimatedNodeTests.cpp | 80 +++++++++++++ packages/react-native/ReactNativeApi.d.ts | 42 ++++--- 13 files changed, 334 insertions(+), 98 deletions(-) diff --git a/packages/react-native/Libraries/Animated/__tests__/AnimatedComposition-itest.js b/packages/react-native/Libraries/Animated/__tests__/AnimatedComposition-itest.js index ba97eacec7f9..f5af08be88e4 100644 --- a/packages/react-native/Libraries/Animated/__tests__/AnimatedComposition-itest.js +++ b/packages/react-native/Libraries/Animated/__tests__/AnimatedComposition-itest.js @@ -379,3 +379,115 @@ describe('composition nodes: native driver, interpolation and detach', () => { }); } }); + +// Regression test for https://github.com/facebook/react-native/issues/49719. +// Listeners attached to a node *derived* from an `Animated.Value` (an operator +// or an interpolation) must keep firing, on both drivers. On the native driver +// the value is computed natively, so the derived node has to subscribe to +// native updates for its own tag rather than relying on the JS graph. +describe('addListener on derived value nodes', () => { + const derivedNodes = [ + { + name: 'Animated.add', + make: (base: Animated.Value) => Animated.add(base, 10), + expected: 60, + }, + { + name: 'Animated.subtract', + make: (base: Animated.Value) => Animated.subtract(base, 10), + expected: 40, + }, + { + name: 'Animated.multiply', + make: (base: Animated.Value) => Animated.multiply(base, 2), + expected: 100, + }, + { + name: 'Animated.divide', + make: (base: Animated.Value) => Animated.divide(base, 2), + expected: 25, + }, + { + name: 'Animated.modulo', + make: (base: Animated.Value) => Animated.modulo(base, 7), + expected: 50 % 7, + }, + { + name: 'interpolate', + make: (base: Animated.Value) => + base.interpolate({inputRange: [0, 50], outputRange: [0, 500]}), + expected: 500, + }, + ]; + + for (const useNativeDriver of [false, true]) { + const driverName = useNativeDriver ? 'native driver' : 'JS driver'; + + for (const {name, make, expected} of derivedNodes) { + it(`${name} notifies its listeners on the ${driverName}`, () => { + let base: ?Animated.Value; + let node: ?Animated.Node; + + function MyApp() { + const value = useAnimatedValue(0); + base = value; + node = make(value); + return ( + + ); + } + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render(); + }); + + // The listener is attached before the node is made native, so this also + // covers the subscription being created from `__makeNative`. + const values: Array = []; + const listenerId = nullthrows(node).addListener(state => { + values.push(state.value); + }); + + let animation: ?Animated.CompositeAnimation; + Fantom.runTask(() => { + animation = Animated.timing(nullthrows(base), { + toValue: 50, + duration: 100, + useNativeDriver, + }); + animation.start(); + }); + Fantom.unstable_produceFramesForDuration(200); + Fantom.runWorkLoop(); + + expect(values.length).toBeGreaterThan(0); + expect(values[values.length - 1]).toBeCloseTo(expected, 0); + + // Removing the listener stops the updates. + nullthrows(node).removeListener(listenerId); + const countAfterRemoval = values.length; + Fantom.runTask(() => { + nullthrows(base).setValue(0); + }); + Fantom.unstable_produceFramesForDuration(32); + Fantom.runWorkLoop(); + expect(values.length).toBe(countAfterRemoval); + + Fantom.runTask(() => { + nullthrows(animation).stop(); + }); + Fantom.runTask(() => { + root.render(); + }); + Fantom.unstable_produceFramesForDuration(16); + Fantom.runWorkLoop(); + }); + } + } +}); diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedAddition.js b/packages/react-native/Libraries/Animated/nodes/AnimatedAddition.js index 91d686f5af3d..8bbc8a4aca95 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedAddition.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedAddition.js @@ -20,6 +20,8 @@ import AnimatedValue from './AnimatedValue'; import AnimatedWithChildren from './AnimatedWithChildren'; export default class AnimatedAddition extends AnimatedWithChildren { + __isNativeValueNode: boolean = true; + _a: AnimatedNode; _b: AnimatedNode; diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedDiffClamp.js b/packages/react-native/Libraries/Animated/nodes/AnimatedDiffClamp.js index 94fef85aa0e1..fd36fb223c3a 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedDiffClamp.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedDiffClamp.js @@ -19,6 +19,8 @@ import AnimatedInterpolation from './AnimatedInterpolation'; import AnimatedWithChildren from './AnimatedWithChildren'; export default class AnimatedDiffClamp extends AnimatedWithChildren { + __isNativeValueNode: boolean = true; + _a: AnimatedNode; _min: number; _max: number; diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedDivision.js b/packages/react-native/Libraries/Animated/nodes/AnimatedDivision.js index 6b73ff7d9931..3bef22a6c92a 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedDivision.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedDivision.js @@ -20,6 +20,8 @@ import AnimatedValue from './AnimatedValue'; import AnimatedWithChildren from './AnimatedWithChildren'; export default class AnimatedDivision extends AnimatedWithChildren { + __isNativeValueNode: boolean = true; + _a: AnimatedNode; _b: AnimatedNode; _warnedAboutDivideByZero: boolean = false; diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedInterpolation.js b/packages/react-native/Libraries/Animated/nodes/AnimatedInterpolation.js index b6a0d4d3b5f5..1ce99ecd6237 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedInterpolation.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedInterpolation.js @@ -444,6 +444,8 @@ function sampleEasingStops( export default class AnimatedInterpolation< OutputT extends InterpolationConfigSupportedOutputType, > extends AnimatedWithChildren { + __isNativeValueNode: boolean = true; + _parent: AnimatedNode; _config: InterpolationConfigType; _interpolation: ?(input: number) => OutputT; diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedModulo.js b/packages/react-native/Libraries/Animated/nodes/AnimatedModulo.js index 32a698afbdac..7c8e1823cbe2 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedModulo.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedModulo.js @@ -19,6 +19,8 @@ import AnimatedInterpolation from './AnimatedInterpolation'; import AnimatedWithChildren from './AnimatedWithChildren'; export default class AnimatedModulo extends AnimatedWithChildren { + __isNativeValueNode: boolean = true; + _a: AnimatedNode; _modulus: number; diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedMultiplication.js b/packages/react-native/Libraries/Animated/nodes/AnimatedMultiplication.js index 93764f86c751..29e7a7019477 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedMultiplication.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedMultiplication.js @@ -20,6 +20,8 @@ import AnimatedValue from './AnimatedValue'; import AnimatedWithChildren from './AnimatedWithChildren'; export default class AnimatedMultiplication extends AnimatedWithChildren { + __isNativeValueNode: boolean = true; + _a: AnimatedNode; _b: AnimatedNode; diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedNode.js b/packages/react-native/Libraries/Animated/nodes/AnimatedNode.js index a7414bd48218..34ea5ea09ace 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedNode.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedNode.js @@ -8,6 +8,7 @@ * @format */ +import type {EventSubscription} from '../../vendor/emitter/EventEmitter'; import type {PlatformConfig} from '../AnimatedPlatformConfig'; import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper'; @@ -54,7 +55,12 @@ export default class AnimatedNode { this.removeAllListeners(); } if (this.__isNative && this.__nativeTag != null) { - NativeAnimatedHelper.API.dropAnimatedNode(this.__nativeTag); + const nativeTag = this.__nativeTag; + // The subscription must not outlive the native tag it observes. Any + // listeners kept around are re-subscribed by `__makeNative` if this node + // is attached again. + this.__updateSubscription?.remove(); + NativeAnimatedHelper.API.dropAnimatedNode(nativeTag); this.__nativeTag = undefined; } } @@ -73,6 +79,19 @@ export default class AnimatedNode { __nativeTag: ?number = undefined; __disableBatchingForNativeCreate: ?boolean = undefined; + /** + * Whether the native node backing this one holds a number, and therefore + * supports `startListeningToAnimatedNodeValue`. That native module method + * only accepts tags of "value" nodes (`ValueAnimatedNode` on Android and in + * C++, `RCTValueAnimatedNode` on iOS); passing any other tag throws on + * Android and is a no-op elsewhere. + * + * Subclasses backed by a non-value native node — props, style, transform, + * object, tracking and color — must leave this `false`. + */ + __isNativeValueNode: boolean = false; + __updateSubscription: ?EventSubscription = null; + __makeNative(platformConfig: ?PlatformConfig): void { // Subclasses are expected to set `__isNative` to true before this. invariant( @@ -81,6 +100,9 @@ export default class AnimatedNode { ); this._platformConfig = platformConfig; + if (this._listeners.size > 0) { + this.__ensureUpdateSubscriptionExists(); + } } /** @@ -93,6 +115,9 @@ export default class AnimatedNode { addListener(callback: (value: any) => unknown): string { const id = String(_uniqueId++); this._listeners.set(id, callback); + if (this.__isNative) { + this.__ensureUpdateSubscriptionExists(); + } return id; } @@ -104,6 +129,9 @@ export default class AnimatedNode { */ removeListener(id: string): void { this._listeners.delete(id); + if (this.__isNative && this._listeners.size === 0) { + this.__updateSubscription?.remove(); + } } /** @@ -113,14 +141,53 @@ export default class AnimatedNode { */ removeAllListeners(): void { this._listeners.clear(); + if (this.__isNative) { + this.__updateSubscription?.remove(); + } } hasListeners(): boolean { return this._listeners.size > 0; } - __onAnimatedValueUpdateReceived(value: number, offset: number): void { - this.__callListeners(value + offset); + /** + * Subscribes to native updates of this node's value, so that listeners keep + * firing for natively driven animations. No-op for nodes that are not backed + * by a native "value" node. + */ + __ensureUpdateSubscriptionExists(): void { + if (!this.__isNativeValueNode || this.__updateSubscription != null) { + return; + } + const nativeTag = this.__getNativeTag(); + NativeAnimatedHelper.API.startListeningToAnimatedNodeValue(nativeTag); + const subscription: EventSubscription = + NativeAnimatedHelper.nativeEventEmitter.addListener( + 'onAnimatedValueUpdate', + data => { + if (data.tag === nativeTag) { + this.__onAnimatedValueUpdateReceived(data.value, data.offset); + } + }, + ); + + this.__updateSubscription = { + remove: () => { + // Only this function assigns to `this.__updateSubscription`. + if (this.__updateSubscription == null) { + return; + } + this.__updateSubscription = null; + subscription.remove(); + NativeAnimatedHelper.API.stopListeningToAnimatedNodeValue(nativeTag); + }, + }; + } + + // NOTE: `offset` is omitted by backends that do not track one separately + // (e.g. the C++ backend), in which case it is already folded into `value`. + __onAnimatedValueUpdateReceived(value: number, offset?: ?number): void { + this.__callListeners(value + (offset ?? 0)); } __callListeners(value: number): void { diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedSubtraction.js b/packages/react-native/Libraries/Animated/nodes/AnimatedSubtraction.js index 04181bda7b20..27366cbe60b3 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedSubtraction.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedSubtraction.js @@ -20,6 +20,8 @@ import AnimatedValue from './AnimatedValue'; import AnimatedWithChildren from './AnimatedWithChildren'; export default class AnimatedSubtraction extends AnimatedWithChildren { + __isNativeValueNode: boolean = true; + _a: AnimatedNode; _b: AnimatedNode; diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedValue.js b/packages/react-native/Libraries/Animated/nodes/AnimatedValue.js index 74ce65ae3649..157511d5727b 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedValue.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedValue.js @@ -8,8 +8,6 @@ * @format */ -import type {EventSubscription} from '../../vendor/emitter/EventEmitter'; -import type {PlatformConfig} from '../AnimatedPlatformConfig'; import type Animation from '../animations/Animation'; import type {EndCallback} from '../animations/Animation'; import type { @@ -88,8 +86,7 @@ function _executeAsAnimatedBatch(id: string, operation: () => void) { * See https://reactnative.dev/docs/animatedvalue */ export default class AnimatedValue extends AnimatedWithChildren { - _listenerCount: number; - _updateSubscription: ?EventSubscription; + __isNativeValueNode: boolean = true; _value: number; _startingValue: number; @@ -104,9 +101,6 @@ export default class AnimatedValue extends AnimatedWithChildren { throw new Error('AnimatedValue: Attempting to set value to undefined'); } - this._listenerCount = 0; - this._updateSubscription = null; - this._startingValue = this._value = value; this._offset = 0; this.__deferAnimationStart = @@ -124,9 +118,6 @@ export default class AnimatedValue extends AnimatedWithChildren { }); } this.stopAnimation(); - if (ReactNativeFeatureFlags.animatedKeepListenersOnDetach()) { - this._updateSubscription?.remove(); - } super.__detach(); } @@ -134,65 +125,12 @@ export default class AnimatedValue extends AnimatedWithChildren { return this._value + this._offset; } - __makeNative(platformConfig: ?PlatformConfig): void { - super.__makeNative(platformConfig); - if (this._listenerCount > 0) { - this.__ensureUpdateSubscriptionExists(); - } - } - + /** + * Narrows `AnimatedNode.addListener`: the value of an `Animated.Value` is + * always a number, so listeners always receive `{value: number}`. + */ addListener(callback: ValueListenerCallback): 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, data.offset); - } - }, - ); - - this._updateSubscription = { - remove: () => { - // Only this function assigns to `this.#updateSubscription`. - if (this._updateSubscription == null) { - return; - } - this._updateSubscription = null; - subscription.remove(); - NativeAnimatedAPI.stopListeningToAnimatedNodeValue(nativeTag); - }, - }; + return super.addListener(callback); } /** @@ -297,7 +235,7 @@ export default class AnimatedValue extends AnimatedWithChildren { } } - __onAnimatedValueUpdateReceived(value: number, offset?: number): void { + __onAnimatedValueUpdateReceived(value: number, offset?: ?number): void { this._updateValue(value, false /*flush*/); if (offset != null) { this._offset = offset; diff --git a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp index 6b7befe12f30..b21524aed354 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp @@ -51,6 +51,33 @@ struct NodesQueueItem { bool connectedToFinishedAnimation; }; +// Whether `type` is backed by a `ValueAnimatedNode` subclass, i.e. whether the +// node holds a number that can be observed. Kept exhaustive (no `default`) so +// that adding a node type is a compile error until it is classified here. +bool isValueNodeType(AnimatedNodeType type) noexcept { + switch (type) { + case AnimatedNodeType::Value: + case AnimatedNodeType::Interpolation: + case AnimatedNodeType::Addition: + case AnimatedNodeType::Subtraction: + case AnimatedNodeType::Division: + case AnimatedNodeType::Multiplication: + case AnimatedNodeType::Modulus: + case AnimatedNodeType::Diffclamp: + case AnimatedNodeType::Round: + return true; + case AnimatedNodeType::Style: + case AnimatedNodeType::Props: + case AnimatedNodeType::Transform: + case AnimatedNodeType::Tracking: + case AnimatedNodeType::Color: + case AnimatedNodeType::Object: + return false; + } + // Unreachable: the switch above is exhaustive. + return false; +} + void mergeObjects(folly::dynamic& out, const folly::dynamic& objectToMerge) { react_native_assert(objectToMerge.isObject()); if (out.isObject() && !out.empty()) { @@ -921,8 +948,8 @@ void NativeAnimatedNodesManager::resolvePlatformColor( void NativeAnimatedNodesManager::startListeningToAnimatedNodeValue( Tag tag, ValueListenerCallback&& callback) noexcept { - if (auto iter = animatedNodes_.find(tag); iter != animatedNodes_.end() && - iter->second->type() == AnimatedNodeType::Value) { + if (auto iter = animatedNodes_.find(tag); + iter != animatedNodes_.end() && isValueNodeType(iter->second->type())) { static_cast(iter->second.get()) ->setValueListener(std::move(callback)); } else { @@ -933,8 +960,8 @@ void NativeAnimatedNodesManager::startListeningToAnimatedNodeValue( void NativeAnimatedNodesManager::stopListeningToAnimatedNodeValue( Tag tag) noexcept { - if (auto iter = animatedNodes_.find(tag); iter != animatedNodes_.end() && - iter->second->type() == AnimatedNodeType::Value) { + if (auto iter = animatedNodes_.find(tag); + iter != animatedNodes_.end() && isValueNodeType(iter->second->type())) { static_cast(iter->second.get()) ->setValueListener(nullptr); } else { diff --git a/packages/react-native/ReactCommon/react/renderer/animated/tests/AnimatedNodeTests.cpp b/packages/react-native/ReactCommon/react/renderer/animated/tests/AnimatedNodeTests.cpp index fd5f0545105c..2c77fb7b57bd 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/tests/AnimatedNodeTests.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animated/tests/AnimatedNodeTests.cpp @@ -11,6 +11,7 @@ #include #include #include +#include namespace facebook::react { @@ -253,6 +254,85 @@ TEST_F(AnimatedNodeTests, RoundAnimatedNodeUsesNearestConfigKey) { EXPECT_DOUBLE_EQ(nodesManager_->getValue(roundTag).value(), 15.0); } +TEST_F(AnimatedNodeTests, StartListeningToDerivedValueNode) { + // Every node that derives from ValueAnimatedNode holds an observable number, + // so `startListeningToAnimatedNodeValue` must accept it — not only nodes of + // type "value". See https://github.com/facebook/react-native/issues/49719. + initNodesManager(); + + auto rootTag = getNextRootViewTag(); + + auto valueTag = ++rootTag; + auto addendTag = ++rootTag; + auto additionTag = ++rootTag; + + nodesManager_->createAnimatedNode( + valueTag, + folly::dynamic::object("type", "value")("value", 0)("offset", 0)); + nodesManager_->createAnimatedNode( + addendTag, + folly::dynamic::object("type", "value")("value", 10)("offset", 0)); + nodesManager_->createAnimatedNode( + additionTag, + folly::dynamic::object("type", "addition")( + "input", folly::dynamic::array(valueTag, addendTag))); + nodesManager_->connectAnimatedNodes(valueTag, additionTag); + nodesManager_->connectAnimatedNodes(addendTag, additionTag); + + std::vector observedValues; + nodesManager_->startListeningToAnimatedNodeValue( + additionTag, + [&observedValues](double value) { observedValues.push_back(value); }); + + runAnimationFrame(0); + + nodesManager_->setAnimatedNodeValue(valueTag, 32); + runAnimationFrame(0); + + ASSERT_FALSE(observedValues.empty()); + EXPECT_DOUBLE_EQ(observedValues.back(), 42); + + nodesManager_->stopListeningToAnimatedNodeValue(additionTag); + + const auto countAfterStop = observedValues.size(); + nodesManager_->setAnimatedNodeValue(valueTag, 0); + runAnimationFrame(0); + + EXPECT_EQ(observedValues.size(), countAfterStop); +} + +TEST_F(AnimatedNodeTests, StartListeningToNonValueNodeIsIgnored) { + // Nodes that do not hold a number cannot be observed. Registering a listener + // on one must be a no-op rather than an unchecked cast. + initNodesManager(); + + auto rootTag = getNextRootViewTag(); + + auto valueTag = ++rootTag; + auto transformTag = ++rootTag; + + nodesManager_->createAnimatedNode( + valueTag, + folly::dynamic::object("type", "value")("value", 1)("offset", 0)); + nodesManager_->createAnimatedNode( + transformTag, + folly::dynamic::object("type", "transform")( + "transforms", + folly::dynamic::array( + folly::dynamic::object("type", "animated")( + "property", "translateX")("nodeTag", valueTag)))); + nodesManager_->connectAnimatedNodes(valueTag, transformTag); + + bool called = false; + nodesManager_->startListeningToAnimatedNodeValue( + transformTag, [&called](double /*value*/) { called = true; }); + + nodesManager_->setAnimatedNodeValue(valueTag, 5); + runAnimationFrame(0); + + EXPECT_FALSE(called); +} + TEST_F(AnimatedNodeTests, SetOffsetReturnsFalseWhenUnchanged) { // This test verifies that setAnimatedNodeOffset doesn't trigger unnecessary // updates when the offset value hasn't changed. diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index c1ac9628efe7..a8bdb3f2b845 100644 --- a/packages/react-native/ReactNativeApi.d.ts +++ b/packages/react-native/ReactNativeApi.d.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<2199d280ef42e2a63ec4d6c405af893a>> + * @generated SignedSource<<3fed4eaff1cc09116351259a28ec70ee>> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -1415,8 +1415,6 @@ declare class AnimatedValue_default extends AnimatedWithChildren_default { interpolate( config: InterpolationConfigType, ): AnimatedInterpolation_default - removeAllListeners(): void - removeListener(id: string): void resetAnimation(callback?: ((value: number) => void) | null | undefined): void setOffset(offset: number): void setValue(value: number): void @@ -5761,7 +5759,7 @@ export { AlertOptions, // 8a116d2a AlertType, // 5ab91217 AndroidKeyboardEvent, // e03becc8 - Animated, // 8ed2ff0a + Animated, // f2f73ecf AppConfig, // 35c0ca70 AppRegistry, // 5bc2bced AppState, // 12012be5 @@ -5775,8 +5773,8 @@ export { BackPressEventName, // 4620fb76 BlurEvent, // 4ba4f941 BoxShadowValue, // b679703f - Button, // af384e38 - ButtonInstance, // 3f6e29ea + Button, // 63f85809 + ButtonInstance, // c1f0fffc ButtonProps, // 21c5780c Clipboard, // 41addb89 CodegenTypes, // ab4986cc @@ -5813,9 +5811,9 @@ export { EventSubscription, // b8d084aa ExtendedExceptionData, // 5a6ccf5a FilterFunction, // bf24c0e3 - FlatList, // 901c50da - FlatListInstance, // c2dd86eb - FlatListProps, // 3bd11d32 + FlatList, // 366bf900 + FlatListInstance, // 1d904b48 + FlatListProps, // 77248e85 FocusEvent, // 850f1517 FontVariant, // 7c7558bb GestureResponderEvent, // 14d3e77a @@ -5956,18 +5954,18 @@ export { ScrollEvent, // d7abdd0a ScrollResponderType, // 603c33d5 ScrollToLocationParamsType, // d7ecdad1 - ScrollView, // a644be1d + ScrollView, // 4bf271c7 ScrollViewImperativeMethods, // 480a85e1 ScrollViewInstance, // 1030cf7f - ScrollViewProps, // 901ba6eb + ScrollViewProps, // 5c40b38c ScrollViewPropsAndroid, // 02f3df2e ScrollViewPropsIOS, // 807cb4f6 ScrollViewScrollToOptions, // 3313411e SectionBase, // 9f13db00 - SectionList, // d3af1e2c + SectionList, // 940fc8a0 SectionListData, // 1a4de01a - SectionListInstance, // 07b91520 - SectionListProps, // ea05da1f + SectionListInstance, // 0315bcb7 + SectionListProps, // dabe5f6b SectionListRenderItem, // 715b2086 SectionListRenderItemInfo, // 4a48a922 Separators, // 6a45f7e3 @@ -5984,7 +5982,7 @@ export { StatusBarProps, // c2a44d88 StatusBarStyle, // 78f53eea StyleProp, // fa0e9b4a - StyleSheet, // f7fe407a + StyleSheet, // e2400c9e SubmitBehavior, // c4ddf490 Switch, // b3e75e79 SwitchChangeEvent, // 899635b1 @@ -6020,9 +6018,9 @@ export { TouchableNativeFeedback, // 49b246df TouchableNativeFeedbackInstance, // 95dc4a1d TouchableNativeFeedbackProps, // b32639f0 - TouchableOpacity, // 36c0926f + TouchableOpacity, // 83d8d1e8 TouchableOpacityInstance, // b186055b - TouchableOpacityProps, // d7db3879 + TouchableOpacityProps, // 53b08714 TouchableWithoutFeedback, // 4bf9d65a TouchableWithoutFeedbackProps, // 931958b6 TransformsStyle, // 65e70f18 @@ -6040,10 +6038,10 @@ export { VirtualViewMode, // 6be59722 VirtualizedList, // 68c7345e VirtualizedListInstance, // 423ee7c0 - VirtualizedListProps, // 3194847b + VirtualizedListProps, // 0b3c1a43 VirtualizedSectionList, // 9fd9cd61 VirtualizedSectionListInstance, // 12b706d5 - VirtualizedSectionListProps, // f42f54c4 + VirtualizedSectionListProps, // bfe2924e WrapperComponentProvider, // 9ef54e61 codegenNativeCommands, // 628a7c0a codegenNativeComponent, // 32a1bca6 @@ -6051,9 +6049,9 @@ export { processColor, // 6e877698 registerCallableModule, // 839c8cfe requireNativeComponent, // aa36a6dd - useAnimatedColor, // 31a919f9 - useAnimatedValue, // 3eb9d3c0 - useAnimatedValueXY, // b434ca0f + useAnimatedColor, // 821ed7dc + useAnimatedValue, // d625d6a9 + useAnimatedValueXY, // a4c13498 useColorScheme, // d585efdb usePressability, // 782138ed useWindowDimensions, // bb4b683f