Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make animated component's event tag properly update #6030

Merged
merged 10 commits into from
Jun 11, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
'use strict';
import type {
INativeEventsManager,
IAnimatedComponentInternal,
AnimatedComponentProps,
InitialComponentProps,
AnimatedComponentRef,
} from './commonTypes';
import { has } from './utils';
import { WorkletEventHandler } from '../WorkletEventHandler';
import { findNodeHandle } from 'react-native';

export class NativeEventsManager implements INativeEventsManager {
readonly #managedComponent: ManagedAnimatedComponent;
readonly #componentOptions?: ComponentOptions;
#eventViewTag = -1;

constructor(component: ManagedAnimatedComponent, options?: ComponentOptions) {
this.#managedComponent = component;
this.#componentOptions = options;
this.#eventViewTag = this.getEventViewTag();
}

public attachEvents() {
executeForEachEventHandler(this.#managedComponent.props, (key, handler) => {
handler.registerForEvents(this.#eventViewTag, key);
});
}

public detachEvents() {
executeForEachEventHandler(
this.#managedComponent.props,
(_key, handler) => {
handler.unregisterFromEvents(this.#eventViewTag);
}
);
}

public updateEvents(
prevProps: AnimatedComponentProps<InitialComponentProps>
) {
const computedEventTag = this.getEventViewTag();
// If the event view tag changes, we need to completely re-mount all events
if (this.#eventViewTag !== computedEventTag) {
// Remove all bindings from previous props that ran on the old viewTag
executeForEachEventHandler(prevProps, (_key, handler) => {
handler.unregisterFromEvents(this.#eventViewTag);
});
// We don't need to unregister from current (new) props, because their events weren't registered yet
// Replace the view tag
this.#eventViewTag = computedEventTag;
// Attach the events with a new viewTag
this.attachEvents();
return;
}

executeForEachEventHandler(prevProps, (key, prevHandler) => {
const newProp = this.#managedComponent.props[key];
if (!newProp) {
// Prop got deleted
prevHandler.unregisterFromEvents(this.#eventViewTag);
} else if (
isWorkletEventHandler(newProp) &&
newProp.workletEventHandler !== prevHandler
) {
// Prop got changed
prevHandler.unregisterFromEvents(this.#eventViewTag);
newProp.workletEventHandler.registerForEvents(this.#eventViewTag);
}
});

executeForEachEventHandler(this.#managedComponent.props, (key, handler) => {
if (!prevProps[key]) {
// Prop got added
handler.registerForEvents(this.#eventViewTag);
}
});
}

private getEventViewTag() {
// Get the tag for registering events - since the event emitting view can be nested inside the main component
const componentAnimatedRef = this.#managedComponent
._component as AnimatedComponentRef;
let newTag: number;
if (componentAnimatedRef.getScrollableNode) {
const scrollableNode = componentAnimatedRef.getScrollableNode();
newTag = findNodeHandle(scrollableNode) ?? -1;
} else {
newTag =
findNodeHandle(
this.#componentOptions?.setNativeProps
? this.#managedComponent
: componentAnimatedRef
) ?? -1;
}
return newTag;
}
}

function isWorkletEventHandler(
prop: unknown
): prop is WorkletEventHandlerHolder {
return (
has('workletEventHandler', prop) &&
prop.workletEventHandler instanceof WorkletEventHandler
);
}

function executeForEachEventHandler(
props: AnimatedComponentProps<InitialComponentProps>,
callback: (
key: string,
handler: InstanceType<typeof WorkletEventHandler>
) => void
) {
for (const key in props) {
const prop = props[key];
if (isWorkletEventHandler(prop)) {
callback(key, prop.workletEventHandler);
}
}
}

type ManagedAnimatedComponent = React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
IAnimatedComponentInternal;

type ComponentOptions = {
setNativeProps: (
ref: AnimatedComponentRef,
props: InitialComponentProps
) => void;
};

type WorkletEventHandlerHolder = {
workletEventHandler: InstanceType<typeof WorkletEventHandler>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ export interface IJSPropsUpdater {
): void;
}

export interface INativeEventsManager {
attachEvents(): void;
detachEvents(): void;
updateEvents(prevProps: AnimatedComponentProps<InitialComponentProps>): void;
}

export type LayoutAnimationStaticContext = {
presetName: string;
};
Expand Down Expand Up @@ -95,15 +101,21 @@ export interface AnimatedComponentRef extends Component {
export interface IAnimatedComponentInternal {
_styles: StyleProps[] | null;
_animatedProps?: Partial<AnimatedComponentProps<AnimatedProps>>;
/**
* Used for Shared Element Transitions, Layout Animations and Animated Styles. It is not related to event handling.
*/
_componentViewTag: number;
_eventViewTag: number;
_isFirstRender: boolean;
jestAnimatedStyle: { value: StyleProps };
_component: AnimatedComponentRef | HTMLElement | null;
_sharedElementTransition: SharedTransition | null;
_jsPropsUpdater: IJSPropsUpdater;
_InlinePropManager: IInlinePropManager;
_PropsFilter: IPropsFilter;
/**
* Doesn't exist on web.
*/
_NativeEventsManager?: INativeEventsManager;
_viewInfo?: ViewInfo;
context: React.ContextType<typeof SkipEnteringContext>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import type {
} from 'react';
import React from 'react';
import { findNodeHandle, Platform } from 'react-native';
import { WorkletEventHandler } from '../WorkletEventHandler';
import '../layoutReanimation/animationsManager';
import invariant from 'invariant';
import { adaptViewConfig } from '../ConfigHelper';
Expand All @@ -30,8 +29,9 @@ import type {
AnimatedComponentRef,
IAnimatedComponentInternal,
ViewInfo,
INativeEventsManager,
} from './commonTypes';
import { has, flattenArray } from './utils';
import { flattenArray } from './utils';
import setAndForwardRef from './setAndForwardRef';
import { isFabric, isJest, isWeb, shouldBeUseWeb } from '../PlatformChecker';
import { InlinePropManager } from './InlinePropManager';
Expand All @@ -48,6 +48,7 @@ import type { CustomConfig } from '../layoutReanimation/web/config';
import type { FlatList, FlatListProps } from 'react-native';
import { addHTMLMutationObserver } from '../layoutReanimation/web/domUtils';
import { getViewInfo } from './getViewInfo';
import { NativeEventsManager } from './NativeEventsManager';

const IS_WEB = isWeb();

Expand Down Expand Up @@ -115,14 +116,14 @@ export function createAnimatedComponent(
_styles: StyleProps[] | null = null;
_animatedProps?: Partial<AnimatedComponentProps<AnimatedProps>>;
_componentViewTag = -1;
_eventViewTag = -1;
_isFirstRender = true;
jestAnimatedStyle: { value: StyleProps } = { value: {} };
_component: AnimatedComponentRef | HTMLElement | null = null;
_sharedElementTransition: SharedTransition | null = null;
_jsPropsUpdater = new JSPropsUpdater();
_InlinePropManager = new InlinePropManager();
_PropsFilter = new PropsFilter();
_NativeEventsManager?: INativeEventsManager;
_viewInfo?: ViewInfo;
static displayName: string;
static contextType = SkipEnteringContext;
Expand All @@ -136,9 +137,12 @@ export function createAnimatedComponent(
}

componentDidMount() {
this._setComponentViewTag();
this._setEventViewTag();
this._attachNativeEvents();
this._componentViewTag = this._getComponentViewTag();
if (!IS_WEB) {
// It exists only on native platforms. We initialize it here because the ref to the animated component is available only post-mount
this._NativeEventsManager = new NativeEventsManager(this, options);
}
this._NativeEventsManager?.attachEvents();
this._jsPropsUpdater.addOnJSPropsChangeListener(this);
this._attachAnimatedStyles();
this._InlinePropManager.attachInlineProps(this, this._getViewInfo());
Expand Down Expand Up @@ -172,7 +176,7 @@ export function createAnimatedComponent(
}

componentWillUnmount() {
this._detachNativeEvents();
this._NativeEventsManager?.detachEvents();
this._jsPropsUpdater.removeOnJSPropsChangeListener(this);
this._detachStyles();
this._InlinePropManager.detachInlineProps();
Expand Down Expand Up @@ -218,46 +222,8 @@ export function createAnimatedComponent(
}
}

_setComponentViewTag() {
this._componentViewTag = this._getViewInfo().viewTag as number;
}

_setEventViewTag() {
// Setting the tag for registering events - since the event emitting view can be nested inside the main component
const componentAnimatedRef = this._component as AnimatedComponentRef;
if (componentAnimatedRef.getScrollableNode) {
const scrollableNode = componentAnimatedRef.getScrollableNode();
this._eventViewTag = findNodeHandle(scrollableNode) ?? -1;
} else {
this._eventViewTag =
findNodeHandle(
options?.setNativeProps ? this : componentAnimatedRef
) ?? -1;
}
}

_attachNativeEvents() {
for (const key in this.props) {
const prop = this.props[key];
if (
has('workletEventHandler', prop) &&
prop.workletEventHandler instanceof WorkletEventHandler
) {
prop.workletEventHandler.registerForEvents(this._eventViewTag, key);
}
}
}

_detachNativeEvents() {
for (const key in this.props) {
const prop = this.props[key];
if (
has('workletEventHandler', prop) &&
prop.workletEventHandler instanceof WorkletEventHandler
) {
prop.workletEventHandler.unregisterFromEvents(this._eventViewTag);
}
}
_getComponentViewTag() {
return this._getViewInfo().viewTag as number;
}

_detachStyles() {
Expand All @@ -280,48 +246,6 @@ export function createAnimatedComponent(
}
}

_updateNativeEvents(
prevProps: AnimatedComponentProps<InitialComponentProps>
) {
for (const key in prevProps) {
const prevProp = prevProps[key];
if (
has('workletEventHandler', prevProp) &&
prevProp.workletEventHandler instanceof WorkletEventHandler
) {
const newProp = this.props[key];
if (!newProp) {
// Prop got deleted
prevProp.workletEventHandler.unregisterFromEvents(
this._eventViewTag
);
} else if (
has('workletEventHandler', newProp) &&
newProp.workletEventHandler instanceof WorkletEventHandler &&
newProp.workletEventHandler !== prevProp.workletEventHandler
) {
// Prop got changed
prevProp.workletEventHandler.unregisterFromEvents(
this._eventViewTag
);
newProp.workletEventHandler.registerForEvents(this._eventViewTag);
}
}
}

for (const key in this.props) {
const newProp = this.props[key];
if (
has('workletEventHandler', newProp) &&
newProp.workletEventHandler instanceof WorkletEventHandler &&
!prevProps[key]
) {
// Prop got added
newProp.workletEventHandler.registerForEvents(this._eventViewTag);
}
}
}

_updateFromNative(props: StyleProps) {
if (options?.setNativeProps) {
options.setNativeProps(this._component as AnimatedComponentRef, props);
Expand Down Expand Up @@ -469,7 +393,7 @@ export function createAnimatedComponent(
) {
this._configureSharedTransition();
}
this._updateNativeEvents(prevProps);
this._NativeEventsManager?.updateEvents(prevProps);
this._attachAnimatedStyles();
this._InlinePropManager.attachInlineProps(this, this._getViewInfo());

Expand Down