-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
refactor: withSpring end conditions #7803
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
Conversation
previousAnimation?.startValue | ||
: Number(animation.toValue) - value; | ||
(previousAnimation?.startValue as number) | ||
: value - (animation.toValue as number); |
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.
this changes actually breaks overshootClamping
assume we have a value 1000 which needs to go to 900
now x0 will bee 100, and in the isAnimationTerminatingCalculation
condition (current > toValue && startValue < toValue)
current would be 999 which is bigger than 900 and startValue would be 100 (instead of 1000) which is also less than 900, so if the spring is not moving more than 50% it will terminate immediately.
which is breaking @gorhom/bottom-sheets for small bottom sheets
## Summary Fixes #7947 When fixing various thing in withSpring in #7803 I moved over to a coherent coordinate system for the animation. This means that when animating a value from 700 to 600, the animation internally animated from 100 to 0. For 600 to 700 it's -100 to 0. I accidentally didn't reflect that change for `overshootClamping` checks. ## Test plan ```tsx import React, { useEffect, useState } from 'react'; import { Button, StyleSheet, Text, View } from 'react-native'; import Animated, { useAnimatedStyle, useSharedValue, withSpring, } from 'react-native-reanimated'; export default function EmptyExample() { const [toggle, setToggle] = useState(false); const height = useSharedValue(600); const animatedStyle = useAnimatedStyle(() => { return { height: withSpring(height.value, { overshootClamping: true, }), }; }); return ( <View style={styles.container}> <Animated.View style={[{ width: 100, backgroundColor: 'blue' }, animatedStyle]} /> <Button title="Change Height" onPress={() => { const target = toggle ? 600 : 500; height.value = target; setToggle(!toggle); }} /> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', }, }); ``` | Before | After | |--------|--------| | <video src="https://github.com/user-attachments/assets/75ac8595-90dd-44f1-ab04-5c5126941e1e"/> | <video src="https://github.com/user-attachments/assets/45c6f4e8-0291-42be-a268-8ee43ee72fb7"/> |
## Summary Fixes #7947 When fixing various thing in withSpring in #7803 I moved over to a coherent coordinate system for the animation. This means that when animating a value from 700 to 600, the animation internally animated from 100 to 0. For 600 to 700 it's -100 to 0. I accidentally didn't reflect that change for `overshootClamping` checks. ## Test plan ```tsx import React, { useEffect, useState } from 'react'; import { Button, StyleSheet, Text, View } from 'react-native'; import Animated, { useAnimatedStyle, useSharedValue, withSpring, } from 'react-native-reanimated'; export default function EmptyExample() { const [toggle, setToggle] = useState(false); const height = useSharedValue(600); const animatedStyle = useAnimatedStyle(() => { return { height: withSpring(height.value, { overshootClamping: true, }), }; }); return ( <View style={styles.container}> <Animated.View style={[{ width: 100, backgroundColor: 'blue' }, animatedStyle]} /> <Button title="Change Height" onPress={() => { const target = toggle ? 600 : 500; height.value = target; setToggle(!toggle); }} /> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', }, }); ``` | Before | After | |--------|--------| | <video src="https://github.com/user-attachments/assets/75ac8595-90dd-44f1-ab04-5c5126941e1e"/> | <video src="https://github.com/user-attachments/assets/45c6f4e8-0291-42be-a268-8ee43ee72fb7"/> |
## Summary Fixes #7947 When fixing various thing in withSpring in #7803 I moved over to a coherent coordinate system for the animation. This means that when animating a value from 700 to 600, the animation internally animated from 100 to 0. For 600 to 700 it's -100 to 0. I accidentally didn't reflect that change for `overshootClamping` checks. ## Test plan ```tsx import React, { useEffect, useState } from 'react'; import { Button, StyleSheet, Text, View } from 'react-native'; import Animated, { useAnimatedStyle, useSharedValue, withSpring, } from 'react-native-reanimated'; export default function EmptyExample() { const [toggle, setToggle] = useState(false); const height = useSharedValue(600); const animatedStyle = useAnimatedStyle(() => { return { height: withSpring(height.value, { overshootClamping: true, }), }; }); return ( <View style={styles.container}> <Animated.View style={[{ width: 100, backgroundColor: 'blue' }, animatedStyle]} /> <Button title="Change Height" onPress={() => { const target = toggle ? 600 : 500; height.value = target; setToggle(!toggle); }} /> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', }, }); ``` | Before | After | |--------|--------| | <video src="https://github.com/user-attachments/assets/75ac8595-90dd-44f1-ab04-5c5126941e1e"/> | <video src="https://github.com/user-attachments/assets/45c6f4e8-0291-42be-a268-8ee43ee72fb7"/> |
Summary
Previous
withSpring
end conditions were based on two parameters:restDisplacementThreshold
- how far the spring is from its resting point andrestSpeedThreshold
- the speed of the spring. These parameters were absolute and adjusted for animations in ranges greater than 0-1. For animations in a lesser range the user would need to overwrite therestDisplacementThreshold
andrestSpeedThreshold
parameters so the animation wouldn't snap immediately after starting. However, since these parameters were absolute it wasn't so obvious what the values should've been.Instead, we replace both these parameters with one
energyThreshold
which tells at what fraction of the initial energy the animation should snap. This value is relative, quadratic and bound to both position and speed of the spring. In my testing it proved much more reliable than the fixed absolute threshold and didn't need to be overridden in any case.To make equations make sense I had to fix the coordinates system used in withSpring animation. Now the resting point is always at relative 0, which makes things more readable in my opinion. If there's an animation from 0 to 100, it gets translated to (-100, 0). If it's from 100 to 0, it gets translated to (100, 0). Thanks to this we don't have to compare the start with the end to figure out whether we should use a minus sign somewhere or not.
Test plan
I added an example that shows that the behavior of the spring works as expected.
ChatHeads
example shows that chainedwithSpring
animations work too.