Skip to content

Commit

Permalink
React to onUserDrivenAnimationEnded event in JS (#45839)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #45839

Pull Request resolved: #45414

This change completes the fix for broken pressable when animations were applied to components with native driven animations.

When creating the AnimatedProps, if they are natively drive animation, we look for the AnimatedValue involved and we register a listener. This is needed to make sure that the NativeModule will send te updated value upon calling the `update` function.

Then, when observing the props lifecycle, it register a listener to the new `OnUserAnimationEnded` event, fired by the NativeAnimation module.

When the `OnUserAnimationEnded` event is fired, the AnimatedProps will update the props that depends on the user driven animation.

## Changelog
[General][Fixed] - reallign the shadow tree and the native tree when the user finishes interacting with the app.

Reviewed By: sammy-SC

Differential Revision: D60499583

fbshipit-source-id: 02d25e7ca31b91f4d6e4ec1654350e2d84117eda
  • Loading branch information
cipolleschi authored and facebook-github-bot committed Aug 12, 2024
1 parent 175943f commit a8786fc
Showing 1 changed file with 64 additions and 3 deletions.
67 changes: 64 additions & 3 deletions packages/react-native/Libraries/Animated/useAnimatedProps.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@

'use strict';

import type {EventSubscription} from '../EventEmitter/NativeEventEmitter';

import * as ReactNativeFeatureFlags from '../../src/private/featureflags/ReactNativeFeatureFlags';
import useDebouncedEffect from '../../src/private/hooks/useDebouncedEffect';
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,
Expand All @@ -33,6 +37,11 @@ type ReducedProps<TProps> = {
};
type CallbackRef<T> = T => mixed;

type AnimatedValueListeners = Array<{
propValue: AnimatedValue,
listenerId: string,
}>;

export default function useAnimatedProps<TProps: {...}, TInstance>(
props: TProps,
): [ReducedProps<TProps>, CallbackRef<TInstance | null>] {
Expand Down Expand Up @@ -153,13 +162,16 @@ export default function useAnimatedProps<TProps: {...}, TInstance>(

const target = getEventTarget(instance);
const events = [];
const animatedValueListeners: AnimatedValueListeners = [];

for (const propName in props) {
// $FlowFixMe[invalid-computed-prop]
const propValue = props[propName];
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);
}
}

Expand All @@ -169,6 +181,10 @@ export default function useAnimatedProps<TProps: {...}, TInstance>(
for (const [propName, propValue] of events) {
propValue.__detach(target, propName);
}

for (const {propValue, listenerId} of animatedValueListeners) {
propValue.removeListener(listenerId);
}
};
},
[
Expand All @@ -183,9 +199,7 @@ export default function useAnimatedProps<TProps: {...}, TInstance>(
return [reduceAnimatedProps<TProps>(node), callbackRef];
}

function reduceAnimatedProps<TProps>(
node: AnimatedProps,
): ReducedProps<TProps> {
function reduceAnimatedProps<TProps>(node: AnimatedNode): ReducedProps<TProps> {
// Force `collapsable` to be false so that the native view is not flattened.
// Flattened views cannot be accurately referenced by the native driver.
return {
Expand All @@ -194,6 +208,35 @@ function reduceAnimatedProps<TProps>(
};
}

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`
Expand All @@ -204,12 +247,30 @@ function reduceAnimatedProps<TProps>(
function useAnimatedPropsLifecycle_layoutEffects(node: AnimatedProps): void {
const prevNodeRef = useRef<?AnimatedProps>(null);
const isUnmountingRef = useRef<boolean>(false);
const userDrivenAnimationEndedListener = useRef<?EventSubscription>(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(() => {
Expand Down

0 comments on commit a8786fc

Please sign in to comment.