diff --git a/packages/react-native/Libraries/Animated/useAnimatedProps.js b/packages/react-native/Libraries/Animated/useAnimatedProps.js index 97472a9f53c..4e76919433f 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(() => {