Skip to content

Commit

Permalink
Add inline animated props (#4068)
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

Based on
#4062

Adds support for inline animated props like this:
```js
const AnimatedCircle = Animated.createAnimatedComponent(Circle);

export default function SvgExample() {
  const sv = useSharedValue('0%');

  sv.value = withRepeat(withTiming('50%', { duration: 500 }), -1, true);

  return (
    <View style={styles.container}>
      <Svg height="200" width="200">
        <AnimatedCircle cx="50%" cy="50%" fill="lime" r={sv} />
      </Svg>
    </View>
  );
}
```
This syntax is so pretty 🤩 but there is one issue with it though. We
can't differentiate between a shared value and ordinary object with
`value` prop. It may happen (though I think it's rather unlikely) that
user doesn't want the prop to animate but just wants to pass an object
or shared value. So I'm checking if it's a whitelisted prop (user had to
whitelist it anyway). If that's not enough we may add
whitelist/blacklist per component in `createAnimatedComponent`. Or use
`animatedProps` prop but that won't be so pretty ;_;.

Also I'm no adding a warning in babel plugin. Imo it would lead to too
many false positives. Something like `<Component prop={obj.value} />` is
normal.
 
## Test plan

Example above. Run on ReanimatedExample, FabricExample and something
similar on WebExample.
  • Loading branch information
graszka22 authored Feb 22, 2023
1 parent a073f21 commit 131eb84
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 48 deletions.
19 changes: 19 additions & 0 deletions docs/docs/fundamentals/animations.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,3 +381,22 @@ For this purpose Reanimated exposes a separate hook called `useAnimatedProps`.
It works in a very similar way to `useAnimatedStyle` but instead of expecting a method that returns the animated styles, we expect the returned object to contain properties that we want to animate.
Then, in order to hook animated props to a view, we provide the resulting object as `animatedProps` property to the "Animated" version of the view type we want to render (e.g. `Animated.View`).
Please check the documentation of the [`useAnimatedProps`](../api/hooks/useAnimatedProps) hook for usage examples.

## Shared Values in Properties
You can also pass a shared value as a property like this:
```js
const AnimatedCircle = Animated.createAnimatedComponent(Circle);

export default function SvgExample() {
const sv = useSharedValue('50%');

return (
<Svg height="200" width="200">
<AnimatedCircle cx="50%" cy="50%" fill="lime" r={sv} />
</Svg>
);
}
```

The radius of the circle will change according to shared value `sv`.
As with inline styles, remember to pass a shared value, not shared value's `.value`.
2 changes: 1 addition & 1 deletion react-native-reanimated.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ declare module 'react-native-reanimated' {
: Record<string, unknown>;

export type AnimateProps<P extends object> = {
[K in keyof Omit<P, 'style'>]: P[K] | AnimatedNode<P[K]>;
[K in keyof Omit<P, 'style'>]: P[K] | AnimatedNode<P[K]> | SharedValue<P[K]>;
} & {
style?: StyleProp<AnimateStyle<StylesOrDefault<P>>>;
} & {
Expand Down
115 changes: 68 additions & 47 deletions src/createAnimatedComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,25 +128,36 @@ function hasInlineStyles(style: StyleProps): boolean {
});
}

function getInlineStylesFromProps(
function extractSharedValuesMapFromProps(
props: AnimatedComponentProps<InitialComponentProps>
): StyleProps {
const styles = flattenArray<StyleProps>(props.style ?? []);
const inlineStyles: StyleProps = {};

styles.forEach((style) => {
for (const [key, styleValue] of Object.entries(style)) {
if (isSharedValue(styleValue)) {
inlineStyles[key] = styleValue;
} else if (key === 'transform' && isInlineStyleTransform(styleValue)) {
inlineStyles[key] = styleValue;
}
): Record<string, any> {
const inlineProps: Record<string, any> = {};

for (const key in props) {
const value = props[key];
if (key === 'style') {
const styles = flattenArray<StyleProps>(props.style ?? []);
styles.forEach((style) => {
for (const [key, styleValue] of Object.entries(style)) {
if (isSharedValue(styleValue)) {
inlineProps[key] = styleValue;
} else if (
key === 'transform' &&
isInlineStyleTransform(styleValue)
) {
inlineProps[key] = styleValue;
}
}
});
} else if (isSharedValue(value)) {
inlineProps[key] = value;
}
});
return inlineStyles;
}

return inlineProps;
}

function inlineStylesHasChanged(styles1: StyleProps, styles2: StyleProps) {
function inlinePropsHasChanged(styles1: StyleProps, styles2: StyleProps) {
if (Object.keys(styles1).length !== Object.keys(styles2).length) {
return true;
}
Expand All @@ -158,13 +169,13 @@ function inlineStylesHasChanged(styles1: StyleProps, styles2: StyleProps) {
return false;
}

function getInlineStyleUpdate(inlineStyle: StyleProps) {
function getInlinePropsUpdate(inlineProps: Record<string, any>) {
'worklet';
const update: StyleProps = {};
for (const [key, styleValue] of Object.entries(inlineStyle)) {
const update: Record<string, any> = {};
for (const [key, styleValue] of Object.entries(inlineProps)) {
if (key === 'transform') {
update[key] = styleValue.map((transform: Record<string, any>) => {
return getInlineStyleUpdate(transform);
return getInlinePropsUpdate(transform);
});
} else if (isSharedValue(styleValue)) {
update[key] = styleValue.value;
Expand Down Expand Up @@ -239,9 +250,9 @@ export default function createAnimatedComponent(
animatedStyle: { value: StyleProps } = { value: {} };
initialStyle = {};
_component: ComponentRef | null = null;
_inlineStylesViewDescriptors: ViewDescriptorsSet | null = null;
_inlineStylesMapperId: number | null = null;
_inlineStyles: StyleProps = {};
_inlinePropsViewDescriptors: ViewDescriptorsSet | null = null;
_inlinePropsMapperId: number | null = null;
_inlineProps: StyleProps = {};
static displayName: string;

constructor(props: AnimatedComponentProps<InitialComponentProps>) {
Expand All @@ -254,13 +265,13 @@ export default function createAnimatedComponent(
componentWillUnmount() {
this._detachNativeEvents();
this._detachStyles();
this._detachInlineStyles();
this._detachInlineProps();
}

componentDidMount() {
this._attachNativeEvents();
this._attachAnimatedStyles();
this._attachInlineStyles();
this._attachInlineProps();
}

_getEventViewRef() {
Expand Down Expand Up @@ -493,20 +504,26 @@ export default function createAnimatedComponent(
}
}

_attachInlineStyles() {
const newInlineStyles: StyleProps = getInlineStylesFromProps(this.props);
const hasChanged = inlineStylesHasChanged(
newInlineStyles,
this._inlineStyles
_attachInlineProps() {
const newInlineProps: Record<string, any> =
extractSharedValuesMapFromProps(this.props);
const hasChanged = inlinePropsHasChanged(
newInlineProps,
this._inlineProps
);

if (hasChanged) {
if (!this._inlineStylesViewDescriptors) {
this._inlineStylesViewDescriptors = makeViewDescriptorsSet();
if (!this._inlinePropsViewDescriptors) {
this._inlinePropsViewDescriptors = makeViewDescriptorsSet();

const { viewTag, viewName, shadowNodeWrapper } = this._getViewInfo();
const { viewTag, viewName, shadowNodeWrapper, viewConfig } =
this._getViewInfo();

this._inlineStylesViewDescriptors.add({
if (Object.keys(newInlineProps).length && viewConfig) {
adaptViewConfig(viewConfig);
}

this._inlinePropsViewDescriptors.add({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
tag: viewTag!,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
Expand All @@ -516,34 +533,34 @@ export default function createAnimatedComponent(
});
}
const sharableViewDescriptors =
this._inlineStylesViewDescriptors.sharableViewDescriptors;
this._inlinePropsViewDescriptors.sharableViewDescriptors;

const maybeViewRef = NativeReanimatedModule.native
? undefined
: ({ items: new Set([this]) } as ViewRefSet<any>); // see makeViewsRefSet

const updaterFunction = () => {
'worklet';
const update = getInlineStyleUpdate(newInlineStyles);
const update = getInlinePropsUpdate(newInlineProps);
updateProps(sharableViewDescriptors, update, maybeViewRef);
};
this._inlineStyles = newInlineStyles;
if (this._inlineStylesMapperId) {
stopMapper(this._inlineStylesMapperId);
this._inlineProps = newInlineProps;
if (this._inlinePropsMapperId) {
stopMapper(this._inlinePropsMapperId);
}
this._inlineStylesMapperId = null;
if (Object.keys(newInlineStyles).length) {
this._inlineStylesMapperId = startMapper(
this._inlinePropsMapperId = null;
if (Object.keys(newInlineProps).length) {
this._inlinePropsMapperId = startMapper(
updaterFunction,
Object.values(newInlineStyles)
Object.values(newInlineProps)
);
}
}
}

_detachInlineStyles() {
if (this._inlineStylesMapperId) {
stopMapper(this._inlineStylesMapperId);
_detachInlineProps() {
if (this._inlinePropsMapperId) {
stopMapper(this._inlinePropsMapperId);
}
}

Expand All @@ -552,7 +569,7 @@ export default function createAnimatedComponent(
) {
this._reattachNativeEvents(prevProps);
this._attachAnimatedStyles();
this._attachInlineStyles();
this._attachInlineProps();
}

_setComponentRef = setAndForwardRef<Component>({
Expand Down Expand Up @@ -620,7 +637,7 @@ export default function createAnimatedComponent(
return this.initialStyle;
} else if (hasInlineStyles(style)) {
if (this._isFirstRender) {
return getInlineStyleUpdate(style);
return getInlinePropsUpdate(style);
}
const newStyle: StyleProps = {};
for (const [key, styleValue] of Object.entries(style)) {
Expand Down Expand Up @@ -662,6 +679,10 @@ export default function createAnimatedComponent(
} else {
props[key] = dummyListener;
}
} else if (isSharedValue(value)) {
if (this._isFirstRender) {
props[key] = (value as SharedValue<any>).value;
}
} else if (
key !== 'onGestureHandlerStateChange' ||
!isChromeDebugger()
Expand Down

0 comments on commit 131eb84

Please sign in to comment.