-
-
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
Open
patrycjakalinska
wants to merge
31
commits into
main
Choose a base branch
from
@patrycjakalinska/support-box-shadow
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+523
−4
Open
Changes from all commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
6f85a87
add support for boxShadow
patrycjakalinska 2954f56
PR review changes
patrycjakalinska c8d4615
add named import, ups
patrycjakalinska 481c3ff
Merge branch 'main' into @patrycjakalinska/support-box-shadow
patrycjakalinska fd9d5da
fixes
patrycjakalinska a68aade
more fixes
patrycjakalinska 96296d8
fix animation not animating bug
patrycjakalinska 3deb7bc
add unit tests for boxShadow
patrycjakalinska e3b5f76
change processBoxShadow to work in place
patrycjakalinska dec4ba2
fix colors flickering when using withSpring
patrycjakalinska 1e6d049
add type for object
patrycjakalinska 244e2c0
lint fixes
patrycjakalinska 6f61bb9
add init runtime tests
patrycjakalinska cba386f
add static runtime test
patrycjakalinska 64a9e7a
add todo to boxShadow runtime test - as it is a newArch prop, the box…
patrycjakalinska 6d05428
small fix for runtime test
patrycjakalinska 5fa603a
Add ViewStyle type
patrycjakalinska 18259dc
smol fix
patrycjakalinska af7a519
remove misleading comment
patrycjakalinska 9bace78
Add comment explaining behaviour of spreading an animation object
patrycjakalinska e43a655
move clampRGBA to separate PR
patrycjakalinska af634fc
Merge branch 'main' into @patrycjakalinska/support-box-shadow
patrycjakalinska deedd17
replace comment with skip directive
patrycjakalinska 346a461
replace array with prop
patrycjakalinska 6bc6fec
Add TODO: to enable test when implemented on Fabric
patrycjakalinska 456a2e3
typescript adjustments
patrycjakalinska 3f7c69e
Merge branch 'main' into @patrycjakalinska/support-box-shadow
patrycjakalinska 9f8bd41
Add comment with explaination
patrycjakalinska b34a22d
add boxShadow to test comparators
patrycjakalinska 2ab6687
Merge branch 'main' into @patrycjakalinska/support-box-shadow
patrycjakalinska 6b3cfd3
Change approach of color process in NestedProps
patrycjakalinska File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
107 changes: 107 additions & 0 deletions
107
apps/common-app/src/examples/RuntimeTests/tests/props/boxShadow.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
// TODO: Enable this test after RuntimeTests are implemented on Fabric | ||
|
||
import { useEffect } from 'react'; | ||
import type { BoxShadowValue } from 'react-native'; | ||
import type { AnimatableValue } from 'react-native-reanimated'; | ||
import type { DefaultStyle } from 'react-native-reanimated/lib/typescript/hook/commonTypes'; | ||
import { ComparisonMode } from '../../ReJest/types'; | ||
import { View, StyleSheet } from 'react-native'; | ||
import Animated, { useSharedValue, withSpring, useAnimatedStyle } from 'react-native-reanimated'; | ||
import { describe, test, expect, render, useTestRef, getTestComponent, wait } from '../../ReJest/RuntimeTestsApi'; | ||
|
||
describe.skip('animation of BoxShadow', () => { | ||
enum Component { | ||
ACTIVE = 'ACTIVE', | ||
PASSIVE = 'PASSIVE', | ||
} | ||
function BoxShadowComponent({ | ||
startBoxShadow, | ||
finalBoxShadow, | ||
}: { | ||
startBoxShadow: BoxShadowValue; | ||
finalBoxShadow: BoxShadowValue; | ||
}) { | ||
const boxShadowActiveSV = useSharedValue(startBoxShadow); | ||
const boxShadowPassiveSV = useSharedValue(startBoxShadow); | ||
|
||
const refActive = useTestRef('ACTIVE'); | ||
const refPassive = useTestRef('PASSIVE'); | ||
|
||
const styleActive = useAnimatedStyle(() => { | ||
return { | ||
boxShadow: [withSpring(boxShadowActiveSV.value as unknown as AnimatableValue, { duration: 700 })], | ||
} as DefaultStyle; | ||
}); | ||
|
||
const stylePassive = useAnimatedStyle(() => { | ||
return { | ||
boxShadow: [boxShadowPassiveSV.value], | ||
} as DefaultStyle; | ||
}); | ||
|
||
useEffect(() => { | ||
const timeout = setTimeout(() => { | ||
boxShadowActiveSV.value = finalBoxShadow; | ||
boxShadowPassiveSV.value = finalBoxShadow; | ||
}, 1000); | ||
|
||
return () => clearTimeout(timeout); | ||
}, [finalBoxShadow, boxShadowActiveSV, boxShadowPassiveSV]); | ||
|
||
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: BoxShadowValue; finalBoxShadow: BoxShadowValue }) => { | ||
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, | ||
}, | ||
}); |
158 changes: 158 additions & 0 deletions
158
packages/react-native-reanimated/__tests__/props.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
import { View, Pressable, Text } from 'react-native'; | ||
import type { ViewStyle } 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(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)', | ||
}, | ||
]); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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,16 @@ function runAnimations( | |
animation.callback && animation.callback(true /* finished */); | ||
} | ||
} | ||
result[key] = animation.current; | ||
/* | ||
* If `animation.current` is an object, spread its properties into a new object | ||
* to avoid modifying the original reference. This ensures when `newValues` has a nested color prop, it stays unparsed | ||
* in rgba format, allowing the animation to run correctly. | ||
*/ | ||
if (typeof animation.current === 'object') { | ||
result[key] = { ...animation.current }; | ||
} 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 +201,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 +239,21 @@ function styleUpdater( | |
animationsActive | ||
); | ||
if (finished) { | ||
last[propName] = updates[propName]; | ||
/** | ||
* If the animated prop is an array, we need to directly set each | ||
* property (manually spread it). This prevents issues where the color | ||
* prop might be incorrectly linked with its `toValue` and `current` | ||
* states, causing abrupt transitions or 'jumps' in animation states. | ||
*/ | ||
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; | ||
|
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
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 comment
The 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 comment
The 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?
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.
In this line the original
key
value is0
rather thatboxShadow
, so we cannot use it. It is because we are doing:I would suggest saving initial
key
tocurrentKey
prop, and passing it. It supports your idea of avoiding performance degradation as well as it's not invasive.by default, on the first run,
runAnimations
function doesn't haveoriginalKey
socurrentKey
becomeskey
. But when we go deeper i.ex. run animation for each item of the array (like we do inboxShadow
case), thecurrentKey
stays asboxShadow
(or any other propName) and lets us determine if we want to spread theanimation.current
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 can push this changes so you can see first hand