-
-
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
feat: Add support for boxShadow #6749
base: main
Are you sure you want to change the base?
Changes from 17 commits
6f85a87
2954f56
c8d4615
481c3ff
fd9d5da
a68aade
96296d8
3deb7bc
e3b5f76
dec4ba2
1e6d049
244e2c0
6f61bb9
cba386f
64a9e7a
6d05428
5fa603a
18259dc
af7a519
9bace78
e43a655
af634fc
deedd17
346a461
6bc6fec
456a2e3
3f7c69e
9f8bd41
b34a22d
2ab6687
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
// TODO: Restore when we add fabric support for runtime tests | ||
|
||
// import { useEffect } from 'react'; | ||
// import { View, StyleSheet, BoxShadowValue } from 'react-native'; | ||
// import Animated, { useSharedValue, withDelay, withSpring, useAnimatedStyle } from 'react-native-reanimated'; | ||
// import { ComparisonMode } from '../../ReJest/types'; | ||
// import { describe, test, expect, render, useTestRef, getTestComponent, wait } from '../../ReJest/RuntimeTestsApi'; | ||
|
||
// type BoxShadow = string | BoxShadowValue[]; | ||
|
||
// describe('animation of BoxShadow', () => { | ||
// enum Component { | ||
// ACTIVE = 'ACTIVE', | ||
// PASSIVE = 'PASSIVE', | ||
// } | ||
// function BoxShadowComponent({ startBoxShadow, finalBoxShadow }: { startBoxShadow: string; finalBoxShadow: string }) { | ||
// const boxShadowActiveSV = useSharedValue(startBoxShadow); | ||
// const boxShadowPassiveSV = useSharedValue(startBoxShadow); | ||
|
||
// const refActive = useTestRef('ACTIVE'); | ||
// const refPassive = useTestRef('PASSIVE'); | ||
|
||
// const styleActive = useAnimatedStyle(() => { | ||
// return { | ||
// boxShadow: [withSpring(boxShadowActiveSV.value, { duration: 700 })], | ||
// }; | ||
// }); | ||
|
||
// const stylePassive = useAnimatedStyle(() => { | ||
// return { | ||
// boxShadow: [boxShadowPassiveSV.value], | ||
// }; | ||
// }); | ||
|
||
// useEffect(() => { | ||
// const timeout = setTimeout(() => { | ||
// boxShadowActiveSV.value = finalBoxShadow; | ||
// boxShadowPassiveSV.value = finalBoxShadow; | ||
// }, 1000); | ||
|
||
// return () => clearTimeout(timeout); | ||
// }, [finalBoxShadow]); | ||
|
||
// return ( | ||
// <View style={styles.container}> | ||
// <Animated.View ref={refActive} style={[styles.animatedBox, styleActive]} /> | ||
// <Animated.View ref={refPassive} style={[styles.animatedBox, stylePassive]} /> | ||
// </View> | ||
// ); | ||
// } | ||
|
||
// test.each([ | ||
// { | ||
// startBoxShadow: { | ||
// offsetX: -10, | ||
// offsetY: 6, | ||
// blurRadius: 7, | ||
// spreadDistance: 10, | ||
// color: 'rgba(245, 40, 145, 0.8)', | ||
// }, | ||
|
||
// finalBoxShadow: { | ||
// offsetX: -20, | ||
// offsetY: 4, | ||
// blurRadius: 10, | ||
// spreadDistance: 20, | ||
// color: 'rgba(39, 185, 245, 0.8)', | ||
// }, | ||
|
||
// description: 'one boxShadow', | ||
// }, | ||
// ])( | ||
// '${description}, from ${startBoxShadow} to ${finalBoxShadow}', | ||
// async ({ startBoxShadow, finalBoxShadow }: { startBoxShadow: BoxShadow; finalBoxShadow: BoxShadow }) => { | ||
// await render(<BoxShadowComponent startBoxShadow={startBoxShadow} finalBoxShadow={finalBoxShadow} />); | ||
|
||
// const activeComponent = getTestComponent(Component.ACTIVE); | ||
// const passiveComponent = getTestComponent(Component.PASSIVE); | ||
|
||
// await wait(200); | ||
|
||
// expect(await activeComponent.getAnimatedStyle('boxShadow')).toBe([finalBoxShadow], ComparisonMode.ARRAY); | ||
// expect(await passiveComponent.getAnimatedStyle('boxShadow')).toBe([finalBoxShadow], ComparisonMode.ARRAY); | ||
// }, | ||
// ); | ||
// }); | ||
|
||
// const styles = StyleSheet.create({ | ||
// container: { | ||
// flex: 1, | ||
// justifyContent: 'center', | ||
// alignItems: 'center', | ||
// }, | ||
// animatedBox: { | ||
// backgroundColor: 'palevioletred', | ||
// width: 100, | ||
// height: 100, | ||
// margin: 30, | ||
// }, | ||
// }); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
import React from 'react'; | ||
import { View, Pressable, type ViewStyle, Text } from 'react-native'; | ||
import { render, fireEvent } from '@testing-library/react-native'; | ||
import Animated, { | ||
interpolate, | ||
interpolateColor, | ||
useSharedValue, | ||
useAnimatedStyle, | ||
} from '../src'; | ||
import { getAnimatedStyle } from '../src/jestUtils'; | ||
import { processBoxShadow } from '../src/processBoxShadow'; | ||
|
||
const AnimatedPressable = Animated.createAnimatedComponent(Pressable); | ||
|
||
const AnimatedComponent = () => { | ||
const pressed = useSharedValue<number>(0); | ||
|
||
const animatedBoxShadow = useAnimatedStyle(() => { | ||
const blurRadius = interpolate(pressed.value, [0, 1], [10, 0]); | ||
const color = interpolateColor( | ||
pressed.value, | ||
[0, 1], | ||
['rgba(255, 0, 0, 1)', 'rgba(0, 0, 255, 1)'] | ||
); | ||
|
||
const boxShadow = `0px 4px ${blurRadius}px 0px ${color}`; | ||
|
||
return { | ||
boxShadow, | ||
}; | ||
}); | ||
|
||
const handlePress = () => { | ||
pressed.value = pressed.value === 0 ? 1 : 0; | ||
}; | ||
|
||
return ( | ||
<View | ||
style={{ | ||
padding: 24, | ||
}}> | ||
<AnimatedPressable | ||
testID={'pressable'} | ||
style={[ | ||
animatedBoxShadow, | ||
{ | ||
backgroundColor: 'red', | ||
padding: 16, | ||
boxShadow: '0px 4px 10px 0px rgba(255, 0, 0, 1)', | ||
}, | ||
]} | ||
onPress={handlePress}> | ||
<Text>Button</Text> | ||
</AnimatedPressable> | ||
</View> | ||
); | ||
}; | ||
|
||
const getDefaultStyle = () => ({ | ||
padding: 16, | ||
backgroundColor: 'red', | ||
boxShadow: '0px 4px 10px 0px rgba(255, 0, 0, 1)', | ||
}); | ||
|
||
const getMultipleBoxShadowStyle = () => ({ | ||
padding: 16, | ||
backgroundColor: 'red', | ||
boxShadow: | ||
'-10px 6px 8px 10px rgba(255, 0, 0, 1), 10px 0px 15px 6px rgba(0, 0, 255, 1)', | ||
}); | ||
|
||
describe('Test of boxShadow prop', () => { | ||
beforeEach(() => { | ||
jest.useFakeTimers(); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.runOnlyPendingTimers(); | ||
jest.useRealTimers(); | ||
}); | ||
|
||
test('boxShadow prop animation', () => { | ||
const style = getDefaultStyle(); | ||
|
||
const { getByTestId } = render(<AnimatedComponent />); | ||
const pressable = getByTestId('pressable'); | ||
|
||
expect(pressable.props.style.boxShadow).toBe( | ||
'0px 4px 10px 0px rgba(255, 0, 0, 1)' | ||
); | ||
expect(pressable).toHaveAnimatedStyle(style); | ||
fireEvent.press(pressable); | ||
jest.advanceTimersByTime(600); | ||
style.boxShadow = '0px 4px 0px 0px rgba(0, 0, 255, 1)'; | ||
expect(pressable).toHaveAnimatedStyle(style); | ||
}); | ||
|
||
test('boxShadow prop animation, get animated style', () => { | ||
const { getByTestId } = render(<AnimatedComponent />); | ||
const pressable = getByTestId('pressable'); | ||
|
||
fireEvent.press(pressable); | ||
jest.advanceTimersByTime(600); | ||
|
||
const style = getAnimatedStyle(pressable); | ||
expect((style as ViewStyle).boxShadow).toBe( | ||
'0px 4px 0px 0px rgba(0, 0, 255, 1)' | ||
); | ||
}); | ||
test('one boxShadow string parsing', () => { | ||
const { getByTestId } = render(<AnimatedComponent />); | ||
const pressable = getByTestId('pressable'); | ||
|
||
expect(pressable.props.style.boxShadow).toBe( | ||
'0px 4px 10px 0px rgba(255, 0, 0, 1)' | ||
); | ||
|
||
processBoxShadow(pressable.props.style); | ||
|
||
expect(pressable.props.style.boxShadow).toEqual([ | ||
{ | ||
offsetX: 0, | ||
offsetY: 4, | ||
blurRadius: 10, | ||
spreadDistance: 0, | ||
color: 'rgba(255, 0, 0, 1)', | ||
}, | ||
]); | ||
|
||
const style = getAnimatedStyle(pressable); | ||
expect((style as ViewStyle).boxShadow).toBe( | ||
'0px 4px 10px 0px rgba(255, 0, 0, 1)' | ||
); | ||
}); | ||
|
||
test('two boxShadows string parsing', () => { | ||
const multipleBoxShadowStyle = getMultipleBoxShadowStyle(); | ||
|
||
processBoxShadow(multipleBoxShadowStyle); | ||
|
||
expect(multipleBoxShadowStyle.boxShadow).toEqual([ | ||
{ | ||
offsetX: -10, | ||
offsetY: 6, | ||
blurRadius: 8, | ||
spreadDistance: 10, | ||
color: 'rgba(255, 0, 0, 1)', | ||
}, | ||
{ | ||
offsetX: 10, | ||
offsetY: 0, | ||
blurRadius: 15, | ||
spreadDistance: 6, | ||
color: 'rgba(0, 0, 255, 1)', | ||
}, | ||
]); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -164,6 +164,13 @@ function parsePercentage(str: string): number { | |||||
return int / 100; | ||||||
} | ||||||
|
||||||
function clampRGBA(RGBA: ParsedColorArray): void { | ||||||
'worklet'; | ||||||
for (let i = 0; i < 4; i++) { | ||||||
RGBA[i] = Math.max(0, Math.min(RGBA[i], 1)); | ||||||
} | ||||||
} | ||||||
|
||||||
patrycjakalinska marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
const names: Record<string, number> = makeShareable({ | ||||||
transparent: 0x00000000, | ||||||
|
||||||
|
@@ -349,6 +356,10 @@ export const ColorProperties = makeShareable([ | |||||
'stroke', | ||||||
]); | ||||||
|
||||||
const NestedColorProperties = makeShareable({ | ||||||
boxShadow: ['color'], | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I think we can just use a simple string instead of an array since we don't know of any more complicated structures. I understand that using an array would be a more flexible solution, but let's keep it simple for now. |
||||||
}); | ||||||
|
||||||
// // ts-prune-ignore-next Exported for the purpose of tests only | ||||||
export function normalizeColor(color: unknown): number | null { | ||||||
'worklet'; | ||||||
|
@@ -675,6 +686,24 @@ export function processColorsInProps(props: StyleProps) { | |||||
for (const key in props) { | ||||||
if (ColorProperties.includes(key)) { | ||||||
props[key] = processColor(props[key]); | ||||||
} else if ( | ||||||
NestedColorProperties[key as keyof typeof NestedColorProperties] | ||||||
) { | ||||||
const nestedPropGroup = props[key] as StyleProps; | ||||||
// most of the time there is only one nested prop in boxShadow array | ||||||
patrycjakalinska marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
for (const groupKey in nestedPropGroup) { | ||||||
const nestedProp = nestedPropGroup[groupKey] as StyleProps; | ||||||
|
||||||
for (const propName in nestedProp) { | ||||||
if ( | ||||||
NestedColorProperties[ | ||||||
key as keyof typeof NestedColorProperties | ||||||
].includes(propName) | ||||||
) { | ||||||
nestedProp[propName] = processColor(nestedProp[propName]); | ||||||
} | ||||||
} | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of iterating over the entire prop object, let's iterate over There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. prop object being the general object with all the props passed (backgroundColor, width, boxShadow etc.)? Because in current approach I use the loop that was there before (loop through keys in props), and if there is a boxShadow in props (boxShadow being an array by default), I iterate through objects in array (most cases one object), and check for if there is a color prop in boxShadow object Can you elaborate more? Do you want to move checking the NestedProp outside the parent loop? |
||||||
} | ||||||
} | ||||||
} | ||||||
} | ||||||
|
@@ -693,6 +722,7 @@ export function convertToRGBA(color: unknown): ParsedColorArray { | |||||
|
||||||
export function rgbaArrayToRGBAColor(RGBA: ParsedColorArray): string { | ||||||
'worklet'; | ||||||
clampRGBA(RGBA); | ||||||
const alpha = RGBA[3] < 0.001 ? 0 : RGBA[3]; | ||||||
return `rgba(${Math.round(RGBA[0] * 255)}, ${Math.round( | ||||||
RGBA[1] * 255 | ||||||
|
@@ -718,6 +748,7 @@ export function toGammaSpace( | |||||
): ParsedColorArray { | ||||||
'worklet'; | ||||||
const res = []; | ||||||
clampRGBA(RGBA); | ||||||
for (let i = 0; i < 3; ++i) { | ||||||
res.push(Math.pow(RGBA[i], 1 / gamma)); | ||||||
} | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,6 +34,7 @@ import type { | |
AnimatedStyle, | ||
} from '../commonTypes'; | ||
import { isWorkletFunction } from '../commonTypes'; | ||
import { processBoxShadow } from '../processBoxShadow'; | ||
import { ReanimatedError } from '../errors'; | ||
|
||
const SHOULD_BE_USE_WEB = shouldBeUseWeb(); | ||
|
@@ -150,7 +151,11 @@ function runAnimations( | |
animation.callback && animation.callback(true /* finished */); | ||
} | ||
} | ||
result[key] = animation.current; | ||
if (typeof animation.current === 'object') { | ||
result[key] = { ...animation.current }; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we also need to copy other objects from the style, such as transforms? 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this approach we also copy transform yes, but it doesn't affect it - it only makes sure any nested the color property will work There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe to avoid potential performance degradation, let's check if this property is specifically a box-shadow. What do you think? |
||
} else { | ||
result[key] = animation.current; | ||
} | ||
tjzel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return finished; | ||
} else if (typeof animation === 'object') { | ||
result[key] = {}; | ||
|
@@ -191,6 +196,9 @@ function styleUpdater( | |
let hasAnimations = false; | ||
let frameTimestamp: number | undefined; | ||
let hasNonAnimatedValues = false; | ||
if (typeof newValues.boxShadow === 'string') { | ||
processBoxShadow(newValues); | ||
} | ||
for (const key in newValues) { | ||
const value = newValues[key]; | ||
if (isAnimated(value)) { | ||
|
@@ -226,7 +234,15 @@ function styleUpdater( | |
animationsActive | ||
); | ||
if (finished) { | ||
last[propName] = updates[propName]; | ||
if (Array.isArray(updates[propName])) { | ||
updates[propName].forEach((obj: StyleProps) => { | ||
for (const prop in obj) { | ||
last[propName][prop] = obj[prop]; | ||
} | ||
}); | ||
} else { | ||
Comment on lines
+248
to
+254
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
last[propName] = updates[propName]; | ||
} | ||
tjzel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
delete animations[propName]; | ||
} else { | ||
allFinished = false; | ||
|
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.
Instead of commenting out the entire file, you could use the
skip
method from the tests