-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
Reintroduce onReanimatedPropsChange
#4821
Conversation
@Latropos Quick FYI, lottie-react-native v6 with fabric support was released a short while ago. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's refactor the event registering logic into a separate file, just like we discussed offline.
Did you tested it on Android? |
@piaskowyk Yes, it works on Andorid 💪 |
@@ -286,6 +287,7 @@ export default function createAnimatedComponent( | |||
_inlinePropsMapperId: number | null = null; | |||
_inlineProps: StyleProps = {}; | |||
_sharedElementTransition: SharedTransition | null = null; | |||
_JSPropUpdater = new JSPropUpdater(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably _jsPropsUpdater
would be a better name here
} | ||
|
||
public addOnJSPropsChangeListener( | ||
animatedComponent: React.Component<unknown, unknown> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any TypeScript way to enforce that the component must be animated?
|
||
private static _listener(data: ListenerData) { | ||
const component = JSPropUpdater._tagToComponentMapping.get(data.viewTag); | ||
component && component._updateFromNative(data.props); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this snippet of code was copied but it may be finally worth to convert it into a proper conditional.
Not all svg issues were solved, looks that fill color still doesn't change #1909 |
As far as I know, it required a custom prop adapter, see |
## Summary This pull request adds support for JS props in Fabric. The implementation is available in the common codebase, so there is no platform-specific code required. This approach doesn't rely on events since, based on my research, using events would require messy workarounds with React Native. Same thing on Paper: #4821 ## Test plan <details> <summary>code</summary> ```js import { TextInput } from 'react-native'; import React, { useEffect } from 'react'; import Svg, { Path, Circle, G, } from 'react-native-svg'; import Animated, { runOnJS, useSharedValue, useDerivedValue, useAnimatedProps, useAnimatedGestureHandler, interpolate, } from 'react-native-reanimated'; import {PanGestureHandler} from 'react-native-gesture-handler'; const BORDER_WIDTH = 25; const DIAL_RADIUS = 22.5; export const {PI} = Math; export const TAU = 2 * PI; const AnimatedPath = Animated.createAnimatedComponent(Path); const AnimatedG = Animated.createAnimatedComponent(G); const AnimatedInput = Animated.createAnimatedComponent(TextInput); const polarToCartesian = ( angle: number, radius: number, {x, y}: {x: number; y: number}, ) => { 'worklet'; const a = ((angle - 90) * Math.PI) / 180.0; return {x: x + radius * Math.cos(a), y: y + radius * Math.sin(a)}; }; const cartesianToPolar = ( x: number, y: number, {x: cx, y: cy}: {x: number; y: number}, step = 1, ) => { 'worklet'; const value = Math.atan((y - cy) / (x - cx)) / (Math.PI / 180) + (x > cx ? 90 : 270); return Math.round(value * (1 / step)) / (1 / step); }; const unMix = (value: number, x: number, y: number) => { 'worklet'; return (value - x) / (y - x); }; type Props = { width: number; height: number; fillColor: string[]; value: number; meterColor: string; min?: number; max?: number; onValueChange: (value: any) => void; children: ( props: Partial<{defaultValue: string; text: string}>, ) => React.ReactNode; step?: number; decimals?: number; }; const CircleSlider = (props: Props) => { const { width, height, value, meterColor, children, min, max, step = 1, decimals, } = props; const smallestSide = Math.min(width, height); const cx = width / 2; const cy = height / 2; const r = (smallestSide / 2) * 0.85; const start = useSharedValue(0); const end = useSharedValue(unMix(value, min! / 360, max! / 360)); useEffect(() => { end.value = unMix(value, min! / 360, max! / 360); }, [value, end, max, min]); const startPos = useDerivedValue(() => polarToCartesian(start.value, r, {x: cx, y: cy}), ); const endPos = useDerivedValue(() => polarToCartesian(end.value, r, {x: cx, y: cy}), ); const animatedPath = useAnimatedProps(() => { const p1 = startPos.value; const p2 = endPos.value; return { d: `M${p1.x} ${p1.y} A ${r} ${r} 0 ${end.value > 180 ? 1 : 0} 1 ${p2.x} ${ p2.y }`, }; }); const animatedCircle = useAnimatedProps(() => { const p2 = endPos.value; return { x: p2.x - 7.5, y: p2.y - 7.5, }; }); const animatedChildrenProps = useAnimatedProps(() => { const decimalCount = (num: number) => { if (decimals) { return decimals; } const numStr = String(num); if (numStr.includes('.')) { return numStr.split('.')[1].length; } return 0; }; const value = interpolate(end.value, [min! / 360, max! / 360], [min! / 360, max! / 360]); return { defaultValue: `${value.toFixed(decimalCount(step))}`, text: `${value.toFixed(decimalCount(step))}`, }; }); const gestureHandler = useAnimatedGestureHandler({ onActive: ({x, y}: {x: number; y: number}, ctx: any) => { const value = cartesianToPolar(x, y, {x: cx, y: cy}, step); ctx.value = interpolate(value, [min! / 360, max! / 360], [min! / 360, max! / 360]); end.value = value; }, onFinish: (_, ctx) => { runOnJS(props.onValueChange)(ctx.value); }, }); return ( <PanGestureHandler onGestureEvent={gestureHandler}> <Animated.View> <Svg width={width} height={height}> <Circle cx={cx} cy={cy} r={r + BORDER_WIDTH / 2 - 1} fill="blue" /> <Circle cx={cx} cy={cy} r={r} strokeWidth={BORDER_WIDTH} fill="url(#fill)" stroke="rgba(255, 255, 525, 0.2)" /> <AnimatedPath stroke={meterColor} strokeWidth={BORDER_WIDTH} fill="none" animatedProps={animatedPath} /> <AnimatedG animatedProps={animatedCircle} onPress={() => {}}> <Circle cx={7.5} cy={7.5} r={DIAL_RADIUS} fill={meterColor} /> </AnimatedG> </Svg> {children && children(animatedChildrenProps)} </Animated.View> </PanGestureHandler> ); }; CircleSlider.defaultProps = { width: 300, height: 300, fillColor: ['#fff'], meterColor: '#fff', min: 0, max: 359, step: 1, onValueChange: (x: any) => x, }; export default function EmptyExample(): JSX.Element { const handleChange = value => console.log(value); console.log('EmptyExample'); return ( <CircleSlider width={325} height={325} value={0} meterColor={'#ffffff'} onValueChange={handleChange}> {animatedProps => ( <AnimatedInput keyboardType="numeric" maxLength={3} selectTextOnFocus={false} animatedProps={animatedProps} /> )} </CircleSlider> ); } ``` </details>
## Summary This pull request adds support for JS props in Fabric. The implementation is available in the common codebase, so there is no platform-specific code required. This approach doesn't rely on events since, based on my research, using events would require messy workarounds with React Native. Same thing on Paper: #4821 ## Test plan <details> <summary>code</summary> ```js import { TextInput } from 'react-native'; import React, { useEffect } from 'react'; import Svg, { Path, Circle, G, } from 'react-native-svg'; import Animated, { runOnJS, useSharedValue, useDerivedValue, useAnimatedProps, useAnimatedGestureHandler, interpolate, } from 'react-native-reanimated'; import {PanGestureHandler} from 'react-native-gesture-handler'; const BORDER_WIDTH = 25; const DIAL_RADIUS = 22.5; export const {PI} = Math; export const TAU = 2 * PI; const AnimatedPath = Animated.createAnimatedComponent(Path); const AnimatedG = Animated.createAnimatedComponent(G); const AnimatedInput = Animated.createAnimatedComponent(TextInput); const polarToCartesian = ( angle: number, radius: number, {x, y}: {x: number; y: number}, ) => { 'worklet'; const a = ((angle - 90) * Math.PI) / 180.0; return {x: x + radius * Math.cos(a), y: y + radius * Math.sin(a)}; }; const cartesianToPolar = ( x: number, y: number, {x: cx, y: cy}: {x: number; y: number}, step = 1, ) => { 'worklet'; const value = Math.atan((y - cy) / (x - cx)) / (Math.PI / 180) + (x > cx ? 90 : 270); return Math.round(value * (1 / step)) / (1 / step); }; const unMix = (value: number, x: number, y: number) => { 'worklet'; return (value - x) / (y - x); }; type Props = { width: number; height: number; fillColor: string[]; value: number; meterColor: string; min?: number; max?: number; onValueChange: (value: any) => void; children: ( props: Partial<{defaultValue: string; text: string}>, ) => React.ReactNode; step?: number; decimals?: number; }; const CircleSlider = (props: Props) => { const { width, height, value, meterColor, children, min, max, step = 1, decimals, } = props; const smallestSide = Math.min(width, height); const cx = width / 2; const cy = height / 2; const r = (smallestSide / 2) * 0.85; const start = useSharedValue(0); const end = useSharedValue(unMix(value, min! / 360, max! / 360)); useEffect(() => { end.value = unMix(value, min! / 360, max! / 360); }, [value, end, max, min]); const startPos = useDerivedValue(() => polarToCartesian(start.value, r, {x: cx, y: cy}), ); const endPos = useDerivedValue(() => polarToCartesian(end.value, r, {x: cx, y: cy}), ); const animatedPath = useAnimatedProps(() => { const p1 = startPos.value; const p2 = endPos.value; return { d: `M${p1.x} ${p1.y} A ${r} ${r} 0 ${end.value > 180 ? 1 : 0} 1 ${p2.x} ${ p2.y }`, }; }); const animatedCircle = useAnimatedProps(() => { const p2 = endPos.value; return { x: p2.x - 7.5, y: p2.y - 7.5, }; }); const animatedChildrenProps = useAnimatedProps(() => { const decimalCount = (num: number) => { if (decimals) { return decimals; } const numStr = String(num); if (numStr.includes('.')) { return numStr.split('.')[1].length; } return 0; }; const value = interpolate(end.value, [min! / 360, max! / 360], [min! / 360, max! / 360]); return { defaultValue: `${value.toFixed(decimalCount(step))}`, text: `${value.toFixed(decimalCount(step))}`, }; }); const gestureHandler = useAnimatedGestureHandler({ onActive: ({x, y}: {x: number; y: number}, ctx: any) => { const value = cartesianToPolar(x, y, {x: cx, y: cy}, step); ctx.value = interpolate(value, [min! / 360, max! / 360], [min! / 360, max! / 360]); end.value = value; }, onFinish: (_, ctx) => { runOnJS(props.onValueChange)(ctx.value); }, }); return ( <PanGestureHandler onGestureEvent={gestureHandler}> <Animated.View> <Svg width={width} height={height}> <Circle cx={cx} cy={cy} r={r + BORDER_WIDTH / 2 - 1} fill="blue" /> <Circle cx={cx} cy={cy} r={r} strokeWidth={BORDER_WIDTH} fill="url(#fill)" stroke="rgba(255, 255, 525, 0.2)" /> <AnimatedPath stroke={meterColor} strokeWidth={BORDER_WIDTH} fill="none" animatedProps={animatedPath} /> <AnimatedG animatedProps={animatedCircle} onPress={() => {}}> <Circle cx={7.5} cy={7.5} r={DIAL_RADIUS} fill={meterColor} /> </AnimatedG> </Svg> {children && children(animatedChildrenProps)} </Animated.View> </PanGestureHandler> ); }; CircleSlider.defaultProps = { width: 300, height: 300, fillColor: ['#fff'], meterColor: '#fff', min: 0, max: 359, step: 1, onValueChange: (x: any) => x, }; export default function EmptyExample(): JSX.Element { const handleChange = value => console.log(value); console.log('EmptyExample'); return ( <CircleSlider width={325} height={325} value={0} meterColor={'#ffffff'} onValueChange={handleChange}> {animatedProps => ( <AnimatedInput keyboardType="numeric" maxLength={3} selectTextOnFocus={false} animatedProps={animatedProps} /> )} </CircleSlider> ); } ``` </details>
Summary
Reintroduce support for jsProps.
Closes #4317 and closes #4674
It turns out that jsProps are still in use by many libraries like
react-native-lottie
andreact-native-svg
.Screen.Recording.2023-07-26.at.13.53.25.mov
Screen.Recording.2023-07-26.at.13.52.28.mov
Test plan
Tested on this example: https://github.com/acollazomayer/ReanimatedSVGBug