Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,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,
Expand All @@ -32,6 +36,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 @@ -152,13 +161,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 @@ -168,6 +180,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 @@ -182,9 +198,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 @@ -193,6 +207,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 @@ -203,12 +246,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
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@ @implementation RCTNativeAnimatedTurboModule {
NSMutableArray<AnimatedOperation> *_preOperations;

NSSet<NSString *> *_userDrivenAnimationEndedEvents;

// TODO: Remove this when https://github.com/facebook/react-native/pull/45457 lands
BOOL _shouldEmitEvent;
}

RCT_EXPORT_MODULE();
Expand All @@ -45,7 +42,6 @@ - (instancetype)init
_operations = [NSMutableArray new];
_preOperations = [NSMutableArray new];
_userDrivenAnimationEndedEvents = [NSSet setWithArray:@[ @"onScrollEnded" ]];
_shouldEmitEvent = NO;
}
return self;
}
Expand Down Expand Up @@ -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<NSNumber *> *)nodes
{
if (!_shouldEmitEvent) {
return;
}

[self sendEventWithName:@"onUserDrivenAnimationEnded" body:@{@"tags" : nodes}];
}

Expand Down