Skip to content

Commit

Permalink
Add setNativeProps helper function on UI runtime (#4595)
Browse files Browse the repository at this point in the history
<!-- Thanks for submitting a pull request! We appreciate you spending
the time to work on these changes. Please follow the template so that
the reviewers can easily understand what the code changes affect. -->

## Summary

Currently, there is no way to update props from a worklet imperatively
(except for calling JSI bindings `_updatePropsPaper` or
`_updatePropsFabric` directly, but they are implementation-detail and
meant to be private).

This PR adds `setNativeProps` helper function available on the UI
runtime which accepts an animated ref (created with `useAnimatedRef()`)
along with a plain JS object with props to be updated.

At the moment, both Paper and Fabric is supported as well as basic web
support works. Proper web support is doable but requires a refactor of
`useAnimatedRef`.

## Test plan

`SetNativePropsExample.tsx` 🆕
  • Loading branch information
tomekzaw authored Aug 7, 2023
1 parent c3c1bb5 commit dac030d
Show file tree
Hide file tree
Showing 9 changed files with 192 additions and 39 deletions.
70 changes: 70 additions & 0 deletions app/src/examples/SetNativePropsExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import Animated, {
runOnJS,
setNativeProps,
useAnimatedRef,
} from 'react-native-reanimated';
import { Button, StyleSheet } from 'react-native';
import {
Gesture,
GestureDetector,
GestureHandlerRootView,
TextInput,
} from 'react-native-gesture-handler';

import React from 'react';

const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);

function delay(ms: number) {
const start = performance.now();
while (performance.now() - start < ms) {}
}

export default function SetNativePropsExample() {
const [text, setText] = React.useState('Hello');

const animatedRef = useAnimatedRef<TextInput>();

const send = () => {
delay(500);
console.log('SEND', text);
setText(''); // calling setText affects the JS state but doesn't update the native view
};

const tap = Gesture.Tap().onEnd(() => {
'worklet';
setNativeProps(animatedRef, {
text: '',
backgroundColor: `hsl(${Math.random() * 360}, 100%, 50%)`,
});
runOnJS(send)();
});

return (
<GestureHandlerRootView style={styles.container}>
<AnimatedTextInput
value={text}
onChangeText={setText}
style={styles.input}
ref={animatedRef}
autoFocus
/>
<GestureDetector gesture={tap}>
<Button title="Send" />
</GestureDetector>
</GestureHandlerRootView>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
input: {
width: 250,
padding: 3,
borderWidth: 1,
},
});
20 changes: 13 additions & 7 deletions app/src/examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import AnimatedTabBarExample from './AnimatedTabBarExample';
import AnimatedTextInputExample from './AnimatedTextInputExample';
import AnimatedTextWidthExample from './AnimatedTextWidthExample';
import ArticleProgressExample from './ArticleProgressExample';
import BabelVersionCheckExample from './BabelVersionCheckExample';
import BasicLayoutAnimation from './LayoutAnimations/BasicLayoutAnimation';
import BasicNestedAnimation from './LayoutAnimations/BasicNestedAnimation';
import BasicNestedLayoutAnimation from './LayoutAnimations/BasicNestedLayoutAnimation';
Expand All @@ -29,6 +30,7 @@ import DefaultAnimations from './LayoutAnimations/DefaultAnimations';
import DeleteAncestorOfExiting from './LayoutAnimations/DeleteAncestorOfExiting';
import DispatchCommandExample from './DispatchCommandExample';
import DragAndSnapExample from './DragAndSnapExample';
import DuplicateTagsExample from './SharedElementTransitions/DuplicateTags';
import EmojiWaterfallExample from './EmojiWaterfallExample';
import EmptyExample from './EmptyExample';
import ExtrapolationExample from './ExtrapolationExample';
Expand All @@ -46,6 +48,7 @@ import LightBoxExample from './LightBoxExample';
import LiquidSwipe from './LiquidSwipe/LiquidSwipe';
import ManyScreensExample from './SharedElementTransitions/ManyScreens';
import ManyTagsExample from './SharedElementTransitions/ManyTags';
import MatrixTransform from './MatrixTransform';
import MeasureExample from './MeasureExample';
import Modal from './LayoutAnimations/Modal';
import ModalNewAPI from './LayoutAnimations/ModalNewAPI';
Expand All @@ -54,7 +57,6 @@ import MountingUnmounting from './LayoutAnimations/MountingUnmounting';
import NativeModals from './LayoutAnimations/NativeModals';
import NestedNativeStacksWithLayout from './LayoutAnimations/NestedNativeStacksWithLayout';
import NestedStacksExample from './SharedElementTransitions/NestedStacks';
import ProgressTransitionExample from './SharedElementTransitions/ProgressTransition';
import NestedTest from './LayoutAnimations/Nested';
import NewestShadowNodesRegistryRemoveExample from './NewestShadowNodesRegistryRemoveExample';
import NonLayoutPropAndRenderExample from './NonLayoutPropAndRenderExample';
Expand All @@ -63,7 +65,10 @@ import OldMeasureExample from './OldMeasureExample';
import OlympicAnimation from './LayoutAnimations/OlympicAnimation';
import OverlappingBoxesExample from './OverlappingBoxesExample';
import PagerExample from './CustomHandler/PagerExample';
import PendulumExample from './PendulumExample';
import PinExample from './PinExample';
import ProfilesExample from './SharedElementTransitions/Profiles';
import ProgressTransitionExample from './SharedElementTransitions/ProgressTransition';
import RainbowExample from './RainbowExample';
import ReactionsCounterExample from './LayoutAnimations/ReactionsCounterExample';
import ReducedMotionExample from './ReducedMotionExample';
Expand All @@ -76,24 +81,20 @@ import ScrollToExample from './ScrollToExample';
import ScrollViewExample from './ScrollViewExample';
import ScrollViewOffsetExample from './ScrollViewOffsetExample';
import ScrollableViewExample from './ScrollableViewExample';
import SetNativePropsExample from './SetNativePropsExample';
import SharedStyleExample from './SharedStyleExample';
import SpringLayoutAnimation from './LayoutAnimations/SpringLayoutAnimation';
import SvgExample from './SvgExample';
import SwipeableList from './LayoutAnimations/SwipeableList';
import SwipeableListExample from './SwipeableListExample';
import TransformExample from './TransformExample';
import UpdatePropsPerfExample from './UpdatePropsPerfExample';
import VolumeExample from './VolumeExample';
import WaterfallGridExample from './LayoutAnimations/WaterfallGridExample';
import WidthExample from './WidthExample';
import WithoutBabelPluginExample from './WithoutBabelPluginExample';
import WobbleExample from './WobbleExample';
import WorkletExample from './WorkletExample';
import ProfilesExample from './SharedElementTransitions/Profiles';
import VolumeExample from './VolumeExample';
import MatrixTransform from './MatrixTransform';
import PendulumExample from './PendulumExample';
import DuplicateTagsExample from './SharedElementTransitions/DuplicateTags';
import BabelVersionCheckExample from './BabelVersionCheckExample';

interface Example {
icon?: string;
Expand Down Expand Up @@ -162,6 +163,11 @@ export const EXAMPLES: Record<string, Example> = {
title: 'Letters',
screen: LettersExample,
},
SetNativePropsExample: {
icon: '🪄',
title: 'setNativeProps',
screen: SetNativePropsExample,
},
UpdatePropsPerfExample: {
icon: '🏎️',
title: 'Update props performance',
Expand Down
31 changes: 31 additions & 0 deletions src/reanimated2/Colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

/* eslint no-bitwise: 0 */
import { StyleProps } from './commonTypes';
import { makeShareable } from './core';
import { isAndroid, isWeb } from './PlatformChecker';

Expand Down Expand Up @@ -274,6 +275,27 @@ const names: any = makeShareable({
yellowgreen: 0x9acd32ff,
});

// copied from react-native/Libraries/Components/View/ReactNativeStyleAttributes
export const ColorProperties = makeShareable([
'backgroundColor',
'borderBottomColor',
'borderColor',
'borderLeftColor',
'borderRightColor',
'borderTopColor',
'borderStartColor',
'borderEndColor',
'borderBlockColor',
'borderBlockEndColor',
'borderBlockStartColor',
'color',
'shadowColor',
'textDecorationColor',
'tintColor',
'textShadowColor',
'overlayColor',
]);

function normalizeColor(color: unknown): number | null {
'worklet';

Expand Down Expand Up @@ -597,6 +619,15 @@ export function processColor(color: unknown): number | null | undefined {
return normalizedColor;
}

export function processColorsInProps(props: StyleProps) {
'worklet';
for (const key in props) {
if (ColorProperties.includes(key)) {
props[key] = processColor(props[key]);
}
}
}

export type ParsedColorArray = [number, number, number, number];

export function convertToRGBA(color: unknown): ParsedColorArray {
Expand Down
2 changes: 1 addition & 1 deletion src/reanimated2/NativeMethods.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { MeasuredDimensions, ShadowNodeWrapper } from './commonTypes';
import { MeasuredDimensions, ShadowNodeWrapper } from './commonTypes';
import {
isChromeDebugger,
isJest,
Expand Down
59 changes: 59 additions & 0 deletions src/reanimated2/SetNativeProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { ShadowNodeWrapper, StyleProps } from './commonTypes';
import {
isChromeDebugger,
isJest,
isWeb,
shouldBeUseWeb,
} from './PlatformChecker';

import type { AnimatedRef } from './hook/commonTypes';
import type { Component } from 'react';
import { _updatePropsJS } from './js-reanimated';
import { processColorsInProps } from './Colors';

const IS_NATIVE = !shouldBeUseWeb();

export let setNativeProps: <T extends Component>(
animatedRef: AnimatedRef<T>,
updates: StyleProps
) => void;

if (isWeb()) {
setNativeProps = (_animatedRef, _updates) => {
const component = (_animatedRef as any)();
_updatePropsJS(_updates, { _component: component });
};
} else if (IS_NATIVE && global._IS_FABRIC) {
setNativeProps = (animatedRef, updates) => {
'worklet';
const shadowNodeWrapper = (animatedRef as any)() as ShadowNodeWrapper;
processColorsInProps(updates);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
_updatePropsFabric!([{ shadowNodeWrapper, updates }]);
};
} else if (IS_NATIVE) {
setNativeProps = (animatedRef, updates) => {
'worklet';
const tag = (animatedRef as any)() as number;
const name = (animatedRef as any).viewName.value;
processColorsInProps(updates);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
_updatePropsPaper!([{ tag, name, updates }]);
};
} else if (isChromeDebugger()) {
setNativeProps = () => {
console.warn(
'[Reanimated] setNativeProps() is not supported with Chrome Debugger.'
);
};
} else if (isJest()) {
setNativeProps = () => {
console.warn('[Reanimated] setNativeProps() is not supported with Jest.');
};
} else {
setNativeProps = () => {
console.warn(
'[Reanimated] setNativeProps() is not supported on this configuration.'
);
};
}
29 changes: 2 additions & 27 deletions src/reanimated2/UpdateProps.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,13 @@
import type { MutableRefObject } from 'react';
import { processColor } from './Colors';
import { processColorsInProps } from './Colors';
import type { ShadowNodeWrapper, SharedValue, StyleProps } from './commonTypes';
import type { AnimatedStyle } from './helperTypes';
import { makeShareable } from './core';
import type { Descriptor } from './hook/commonTypes';
import { _updatePropsJS } from './js-reanimated';
import { shouldBeUseWeb } from './PlatformChecker';
import type { ViewRefSet } from './ViewDescriptorsSet';
import { runOnUIImmediately } from './threads';

// copied from react-native/Libraries/Components/View/ReactNativeStyleAttributes
const colorProps = [
'backgroundColor',
'borderBottomColor',
'borderColor',
'borderLeftColor',
'borderRightColor',
'borderTopColor',
'borderStartColor',
'borderEndColor',
'color',
'shadowColor',
'textDecorationColor',
'tintColor',
'textShadowColor',
'overlayColor',
];

export const ColorProperties = makeShareable(colorProps);

let updateProps: (
viewDescriptor: SharedValue<Descriptor[]>,
updates: StyleProps | AnimatedStyle<any>,
Expand All @@ -47,11 +26,7 @@ if (shouldBeUseWeb()) {
} else {
updateProps = (viewDescriptors, updates) => {
'worklet';
for (const key in updates) {
if (ColorProperties.indexOf(key) !== -1) {
updates[key] = processColor(updates[key]);
}
}
processColorsInProps(updates);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
global.UpdatePropsManager!.update(viewDescriptors, updates);
};
Expand Down
3 changes: 1 addition & 2 deletions src/reanimated2/animation/styleAnimation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import type {
import type { AnimatedStyle } from '../helperTypes';
import type { StyleLayoutAnimation } from './commonTypes';
import { withTiming } from './timing';
import { ColorProperties } from '../UpdateProps';
import { processColor } from '../Colors';
import { ColorProperties, processColor } from '../Colors';

// resolves path to value for nested objects
// if path cannot be resolved returns undefined
Expand Down
16 changes: 14 additions & 2 deletions src/reanimated2/hook/useAnimatedRef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ import {
makeShareableCloneRecursive,
registerShareableMapping,
} from '../shareables';
import { findNodeHandle } from 'react-native';
import { Platform, findNodeHandle } from 'react-native';

interface MaybeScrollableComponent extends Component {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getNativeScrollRef?: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getScrollableNode?: any;
viewConfig?: {
uiViewClassName?: string;
};
}

function getComponentOrScrollable(component: MaybeScrollableComponent) {
Expand All @@ -33,6 +37,8 @@ export function useAnimatedRef<
T extends MaybeScrollableComponent
>(): AnimatedRef<T> {
const tag = useSharedValue<number | ShadowNodeWrapper | null>(-1);
const viewName = useSharedValue<string | null>(null);

const ref = useRef<AnimatedRef<T>>();

if (!ref.current) {
Expand All @@ -41,6 +47,10 @@ export function useAnimatedRef<
if (component) {
tag.value = getTagValueFunction(getComponentOrScrollable(component));
fun.current = component;
// viewName is required only on iOS with Paper
if (Platform.OS === 'ios' && !global._IS_FABRIC) {
viewName.value = component?.viewConfig?.uiViewClassName || 'RCTView';
}
}
return tag.value;
});
Expand All @@ -50,7 +60,9 @@ export function useAnimatedRef<
const remoteRef = makeShareableCloneRecursive({
__init: () => {
'worklet';
return () => tag.value;
const f = () => tag.value;
f.viewName = viewName;
return f;
},
});
registerShareableMapping(fun, remoteRef);
Expand Down
1 change: 1 addition & 0 deletions src/reanimated2/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './interpolation';
export * from './interpolateColor';
export * from './Easing';
export * from './NativeMethods';
export { setNativeProps } from './SetNativeProps';
export * from './Colors';
export * from './PropAdapters';
export * from './layoutReanimation';
Expand Down

0 comments on commit dac030d

Please sign in to comment.