Skip to content

Commit d1e403a

Browse files
committed
Polyfill experimental 'spring()' timing function for native
Uses React Native Animated API to polyfill a 2016 proposal for the spring() timing function in CSS. https://lists.w3.org/Archives/Public/www-style/2016Jun/0181.html
1 parent 31ad06a commit d1e403a

6 files changed

Lines changed: 116 additions & 10 deletions

File tree

apps/examples/src/components/App.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -739,7 +739,7 @@ const styles = css.create({
739739
backgroundColor: 'red',
740740
transitionDuration: '500ms',
741741
transitionProperty: 'transform',
742-
transitionTimingFunction: 'ease'
742+
transitionTimingFunction: 'spring(1,100,10,0)'
743743
},
744744
objContain: {
745745
objectFit: 'contain'

packages/react-strict-dom/src/native/modules/useStyleTransition.js

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
import type {
11+
CompositeAnimation,
1112
ReactNativeStyle,
1213
ReactNativeStyleValue,
1314
ReactNativeTransform
@@ -191,6 +192,38 @@ function transitionStyleHasChanged(
191192
return false;
192193
}
193194

195+
function getAnimation(
196+
animatedValue: Animated.Value,
197+
duration: number,
198+
timingFunction: string | null,
199+
shouldUseNativeDriver: boolean
200+
): CompositeAnimation {
201+
// Based on https://lists.w3.org/Archives/Public/www-style/2016Jun/0181.html
202+
// spring(mass, stiffness, damping, initialVelocity)
203+
if (timingFunction != null && timingFunction.includes('spring')) {
204+
const chunk = timingFunction.split('spring(')[1];
205+
const str = chunk.split(')')[0];
206+
const [mass = 1, stiffness = 100, damping = 10, initialVelocity = 0] =
207+
str === '' ? [] : str.split(',').map((point) => parseFloat(point.trim()));
208+
209+
return Animated.spring(animatedValue, {
210+
damping,
211+
mass,
212+
stiffness,
213+
toValue: 1,
214+
useNativeDriver: shouldUseNativeDriver,
215+
velocity: initialVelocity
216+
});
217+
}
218+
219+
return Animated.timing(animatedValue, {
220+
duration,
221+
easing: getEasingFunction(timingFunction),
222+
toValue: 1,
223+
useNativeDriver: shouldUseNativeDriver
224+
});
225+
}
226+
194227
export function useStyleTransition(style: ReactNativeStyle): ReactNativeStyle {
195228
const {
196229
transitionDelay: _delay,
@@ -260,12 +293,12 @@ export function useStyleTransition(style: ReactNativeStyle): ReactNativeStyle {
260293

261294
const animation = Animated.sequence([
262295
Animated.delay(delay),
263-
Animated.timing(animatedValue, {
264-
toValue: 1,
296+
getAnimation(
297+
animatedValue,
265298
duration,
266-
easing: getEasingFunction(timingFunction),
267-
useNativeDriver: shouldUseNativeDriver
268-
})
299+
timingFunction,
300+
shouldUseNativeDriver
301+
)
269302
]);
270303
animation.start();
271304

packages/react-strict-dom/src/types/renderer.native.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99

1010
// $FlowFixMe(nonstrict-import)
1111
import type AnimatedNode from 'react-native/Libraries/Animated/nodes/AnimatedNode';
12+
import type {
13+
// $FlowFixMe(nonstrict-import)
14+
CompositeAnimation
15+
} from 'react-native/Libraries/Animated/Animated';
1216
import type {
1317
// $FlowFixMe(nonstrict-import)
1418
Props as TextInputProps
@@ -141,6 +145,7 @@ type ReactNativeStyleValue =
141145
type ReactNativeStyle = { [string]: ?ReactNativeStyleValue };
142146

143147
export type {
148+
CompositeAnimation,
144149
ReactNativeProps,
145150
ReactNativeStyle,
146151
ReactNativeStyleValue,

packages/react-strict-dom/tests/__mocks__/react-native/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ export const Animated = {
3030
}),
3131
stop: jest.fn()
3232
};
33+
}),
34+
spring: jest.fn(() => {
35+
return {
36+
start: jest.fn()
37+
};
3338
})
3439
};
3540

packages/react-strict-dom/tests/__snapshots__/html-test.native.js.snap-native

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,7 +1043,7 @@ exports[`<html.*> style polyfills "transition" properties backgroundColor trans
10431043
/>
10441044
`;
10451045

1046-
exports[`<html.*> style polyfills "transition" properties cubic-bezier easing: end 1`] = `
1046+
exports[`<html.*> style polyfills "transition" properties cubic-bezier() timing function: end 1`] = `
10471047
<Animated.View
10481048
animated={true}
10491049
style={
@@ -1065,7 +1065,7 @@ exports[`<html.*> style polyfills "transition" properties cubic-bezier easing:
10651065
/>
10661066
`;
10671067

1068-
exports[`<html.*> style polyfills "transition" properties cubic-bezier easing: start 1`] = `
1068+
exports[`<html.*> style polyfills "transition" properties cubic-bezier() timing function: start 1`] = `
10691069
<Animated.View
10701070
animated={true}
10711071
style={
@@ -1328,6 +1328,41 @@ exports[`<html.*> style polyfills "transition" properties other transforms: tra
13281328
/>
13291329
`;
13301330

1331+
exports[`<html.*> style polyfills "transition" properties spring() timing function: end 1`] = `
1332+
<Animated.View
1333+
animated={true}
1334+
style={
1335+
{
1336+
"boxSizing": "content-box",
1337+
"opacity": {
1338+
"inputRange": [
1339+
0,
1340+
1,
1341+
],
1342+
"outputRange": [
1343+
1,
1344+
0,
1345+
],
1346+
},
1347+
"position": "static",
1348+
}
1349+
}
1350+
/>
1351+
`;
1352+
1353+
exports[`<html.*> style polyfills "transition" properties spring() timing function: start 1`] = `
1354+
<Animated.View
1355+
animated={true}
1356+
style={
1357+
{
1358+
"boxSizing": "content-box",
1359+
"opacity": 1,
1360+
"position": "static",
1361+
}
1362+
}
1363+
/>
1364+
`;
1365+
13311366
exports[`<html.*> style polyfills "transition" properties transform transition: end 1`] = `
13321367
<Animated.View
13331368
animated={true}

packages/react-strict-dom/tests/html-test.native.js

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -701,10 +701,9 @@ describe('<html.*>', () => {
701701
expect(Easing.out).toHaveBeenCalled();
702702
});
703703

704-
test('cubic-bezier easing', () => {
704+
test('cubic-bezier() timing function', () => {
705705
const BEZIER_STR = 'cubic-bezier( 0.1, 0.2,0.3 ,0.4)';
706706
let root;
707-
// cubic-bezier easing
708707
act(() => {
709708
root = create(<html.div style={styles.opacity(1, BEZIER_STR)} />);
710709
});
@@ -717,6 +716,35 @@ describe('<html.*>', () => {
717716
expect(Easing.bezier).toHaveBeenCalledWith(0.1, 0.2, 0.3, 0.4);
718717
});
719718

719+
test('spring() timing function', () => {
720+
// spring(mass, stiffness, damping, initialVelocity)
721+
const SPRING_STR = 'spring( 1, 2,3 , 4 )';
722+
let root;
723+
act(() => {
724+
root = create(<html.div style={styles.opacity(1, SPRING_STR)} />);
725+
});
726+
expect(root.toJSON()).toMatchSnapshot('start');
727+
expect(Animated.spring).not.toHaveBeenCalled();
728+
expect(Animated.timing).not.toHaveBeenCalled();
729+
act(() => {
730+
root.update(<html.div style={styles.opacity(0, SPRING_STR)} />);
731+
});
732+
expect(root.toJSON()).toMatchSnapshot('end');
733+
// Animated.spring(damping, mass, stiffness, toValue, useNativeDriver, velocity)
734+
expect(Animated.spring).toHaveBeenCalledWith(
735+
expect.anything(),
736+
expect.objectContaining({
737+
damping: 3,
738+
mass: 1,
739+
stiffness: 2,
740+
toValue: 1,
741+
useNativeDriver: true,
742+
velocity: 4
743+
})
744+
);
745+
expect(Animated.timing).not.toHaveBeenCalled();
746+
});
747+
720748
test('transition all properties (opacity and transform)', () => {
721749
let root;
722750
// transition all properties (opacity and transform)

0 commit comments

Comments
 (0)