diff --git a/packages/react-native/Libraries/Animated/useAnimatedProps.js b/packages/react-native/Libraries/Animated/useAnimatedProps.js index 97472a9f53c6..4e76919433fd 100644 --- a/packages/react-native/Libraries/Animated/useAnimatedProps.js +++ b/packages/react-native/Libraries/Animated/useAnimatedProps.js @@ -10,12 +10,16 @@ 'use strict'; +import type {EventSubscription} from '../EventEmitter/NativeEventEmitter'; + import * as ReactNativeFeatureFlags from '../../src/private/featureflags/ReactNativeFeatureFlags'; import {isPublicInstance as isFabricPublicInstance} from '../ReactNative/ReactFabricPublicInstance/ReactFabricPublicInstanceUtils'; import useRefEffect from '../Utilities/useRefEffect'; import {AnimatedEvent} from './AnimatedEvent'; import NativeAnimatedHelper from './NativeAnimatedHelper'; +import AnimatedNode from './nodes/AnimatedNode'; import AnimatedProps from './nodes/AnimatedProps'; +import AnimatedValue from './nodes/AnimatedValue'; import { useCallback, useEffect, @@ -32,6 +36,11 @@ type ReducedProps = { }; type CallbackRef = T => mixed; +type AnimatedValueListeners = Array<{ + propValue: AnimatedValue, + listenerId: string, +}>; + export default function useAnimatedProps( props: TProps, ): [ReducedProps, CallbackRef] { @@ -152,6 +161,7 @@ export default function useAnimatedProps( const target = getEventTarget(instance); const events = []; + const animatedValueListeners: AnimatedValueListeners = []; for (const propName in props) { // $FlowFixMe[invalid-computed-prop] @@ -159,6 +169,8 @@ export default function useAnimatedProps( if (propValue instanceof AnimatedEvent && propValue.__isNative) { propValue.__attach(target, propName); events.push([propName, propValue]); + // $FlowFixMe[incompatible-call] - the `addListenersToPropsValue` drills down the propValue. + addListenersToPropsValue(propValue, animatedValueListeners); } } @@ -168,6 +180,10 @@ export default function useAnimatedProps( for (const [propName, propValue] of events) { propValue.__detach(target, propName); } + + for (const {propValue, listenerId} of animatedValueListeners) { + propValue.removeListener(listenerId); + } }; }, [ @@ -182,9 +198,7 @@ export default function useAnimatedProps( return [reduceAnimatedProps(node), callbackRef]; } -function reduceAnimatedProps( - node: AnimatedProps, -): ReducedProps { +function reduceAnimatedProps(node: AnimatedNode): ReducedProps { // Force `collapsable` to be false so that the native view is not flattened. // Flattened views cannot be accurately referenced by the native driver. return { @@ -193,6 +207,35 @@ function reduceAnimatedProps( }; } +function addListenersToPropsValue( + propValue: AnimatedValue, + accumulator: AnimatedValueListeners, +) { + // propValue can be a scalar value, an array or an object. + if (propValue instanceof AnimatedValue) { + const listenerId = propValue.addListener(() => {}); + accumulator.push({propValue, listenerId}); + } else if (Array.isArray(propValue)) { + // An array can be an array of scalar values, arrays of arrays, or arrays of objects + for (const prop of propValue) { + addListenersToPropsValue(prop, accumulator); + } + } else if (propValue instanceof Object) { + addAnimatedValuesListenersToProps(propValue, accumulator); + } +} + +function addAnimatedValuesListenersToProps( + props: AnimatedNode, + accumulator: AnimatedValueListeners, +) { + for (const propName in props) { + // $FlowFixMe[prop-missing] - This is an object contained in a prop, but we don't know the exact type. + const propValue = props[propName]; + addListenersToPropsValue(propValue, accumulator); + } +} + /** * Manages the lifecycle of the supplied `AnimatedProps` by invoking `__attach` * and `__detach`. However, this is more complicated because `AnimatedProps` @@ -203,12 +246,30 @@ function reduceAnimatedProps( function useAnimatedPropsLifecycle_layoutEffects(node: AnimatedProps): void { const prevNodeRef = useRef(null); const isUnmountingRef = useRef(false); + const userDrivenAnimationEndedListener = useRef(null); useEffect(() => { // It is ok for multiple components to call `flushQueue` because it noops // if the queue is empty. When multiple animated components are mounted at // the same time. Only first component flushes the queue and the others will noop. NativeAnimatedHelper.API.flushQueue(); + + if (node.__isNative) { + userDrivenAnimationEndedListener.current = + NativeAnimatedHelper.nativeEventEmitter.addListener( + 'onUserDrivenAnimationEnded', + data => { + node.update(); + }, + ); + } + + return () => { + if (userDrivenAnimationEndedListener.current) { + userDrivenAnimationEndedListener.current?.remove(); + userDrivenAnimationEndedListener.current = null; + } + }; }); useLayoutEffect(() => { diff --git a/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedTurboModule.mm b/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedTurboModule.mm index 4d16e5009254..a3f8d1583cfc 100644 --- a/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedTurboModule.mm +++ b/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedTurboModule.mm @@ -27,9 +27,6 @@ @implementation RCTNativeAnimatedTurboModule { NSMutableArray *_preOperations; NSSet *_userDrivenAnimationEndedEvents; - - // TODO: Remove this when https://github.com/facebook/react-native/pull/45457 lands - BOOL _shouldEmitEvent; } RCT_EXPORT_MODULE(); @@ -45,7 +42,6 @@ - (instancetype)init _operations = [NSMutableArray new]; _preOperations = [NSMutableArray new]; _userDrivenAnimationEndedEvents = [NSSet setWithArray:@[ @"onScrollEnded" ]]; - _shouldEmitEvent = NO; } return self; } @@ -379,27 +375,8 @@ - (void)animatedNode:(RCTValueAnimatedNode *)node didUpdateValue:(CGFloat)value [self sendEventWithName:@"onAnimatedValueUpdate" body:@{@"tag" : node.nodeTag, @"value" : @(value)}]; } -// TODO: Remove this when https://github.com/facebook/react-native/pull/45457 lands -- (void)startObserving -{ - [super startObserving]; - _shouldEmitEvent = YES; -} - -- (void)stopObserving -{ - [super stopObserving]; - _shouldEmitEvent = NO; -} - -// ---- - - (void)userDrivenAnimationEnded:(NSArray *)nodes { - if (!_shouldEmitEvent) { - return; - } - [self sendEventWithName:@"onUserDrivenAnimationEnded" body:@{@"tags" : nodes}]; }