Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/examples/src/components/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -739,7 +739,7 @@ const styles = css.create({
backgroundColor: 'red',
transitionDuration: '500ms',
transitionProperty: 'transform',
transitionTimingFunction: 'ease'
transitionTimingFunction: 'spring(1,100,10,0)'
},
objContain: {
objectFit: 'contain'
Expand Down
81 changes: 75 additions & 6 deletions packages/react-strict-dom/src/native/modules/useStyleTransition.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@
*/

import type {
CompositeAnimation,
ReactNativeStyle,
ReactNativeStyleValue,
ReactNativeTransform
} from '../../types/renderer.native';

import { useEffect, useRef, useState } from 'react';
import { warnMsg } from '../../shared/logUtils';
import { errorMsg, warnMsg } from '../../shared/logUtils';
import { Animated, Easing } from 'react-native';

type AnimatedStyle = {
Expand Down Expand Up @@ -191,6 +192,74 @@ function transitionStyleHasChanged(
return false;
}

function getAnimation(
animatedValue: Animated.Value,
duration: number,
timingFunction: string | null,
shouldUseNativeDriver: boolean
): CompositeAnimation {
// Based on https://lists.w3.org/Archives/Public/www-style/2016Jun/0181.html
// spring(mass, stiffness, damping, initialVelocity)
if (timingFunction != null && timingFunction.trim().startsWith('spring(')) {
const chunk = timingFunction.split('spring(')[1];

const closingParenIndex = chunk.indexOf(')');
if (closingParenIndex === -1) {
errorMsg(
`spring() timing function of "${timingFunction}" is missing closing parenthesis.`
);
return Animated.timing(animatedValue, {
duration,
easing: getEasingFunction(null),
toValue: 1,
useNativeDriver: shouldUseNativeDriver
});
}

const str = chunk.split(')')[0];
let [mass = 1, stiffness = 100, damping = 10, initialVelocity = 0] =
str === '' ? [] : str.split(',').map((point) => parseFloat(point.trim()));

if (mass <= 0) {
errorMsg(
`spring() timing function "mass" must be greater than 0. Received ${mass}. Defaulting to 1.`
);
mass = 1;
}
if (stiffness <= 0) {
errorMsg(
`spring() timing function "stiffness" must be greater than 0. Received ${stiffness}. Defaulting to 100.`
);
stiffness = 100;
}
if (damping < 0) {
errorMsg(
`spring() timing function "damping" must be greater than or equal to 0. Received ${damping}. Defaulting to 10.`
);
damping = 10;
}
if (initialVelocity == null) {
initialVelocity = 0;
}

return Animated.spring(animatedValue, {
damping,
mass,
stiffness,
toValue: 1,
useNativeDriver: shouldUseNativeDriver,
velocity: initialVelocity
});
}

return Animated.timing(animatedValue, {
duration,
easing: getEasingFunction(timingFunction),
toValue: 1,
useNativeDriver: shouldUseNativeDriver
});
}

export function useStyleTransition(style: ReactNativeStyle): ReactNativeStyle {
const {
transitionDelay: _delay,
Expand Down Expand Up @@ -260,12 +329,12 @@ export function useStyleTransition(style: ReactNativeStyle): ReactNativeStyle {

const animation = Animated.sequence([
Animated.delay(delay),
Animated.timing(animatedValue, {
toValue: 1,
getAnimation(
animatedValue,
duration,
easing: getEasingFunction(timingFunction),
useNativeDriver: shouldUseNativeDriver
})
timingFunction,
shouldUseNativeDriver
)
]);
animation.start();

Expand Down
5 changes: 5 additions & 0 deletions packages/react-strict-dom/src/types/renderer.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@

// $FlowFixMe(nonstrict-import)
import type AnimatedNode from 'react-native/Libraries/Animated/nodes/AnimatedNode';
import type {
// $FlowFixMe(nonstrict-import)
CompositeAnimation
} from 'react-native/Libraries/Animated/Animated';
import type {
// $FlowFixMe(nonstrict-import)
Props as TextInputProps
Expand Down Expand Up @@ -141,6 +145,7 @@ type ReactNativeStyleValue =
type ReactNativeStyle = { [string]: ?ReactNativeStyleValue };

export type {
CompositeAnimation,
ReactNativeProps,
ReactNativeStyle,
ReactNativeStyleValue,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ export const Animated = {
}),
stop: jest.fn()
};
}),
spring: jest.fn(() => {
return {
start: jest.fn()
};
})
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1043,7 +1043,7 @@ exports[`<html.*> style polyfills "transition" properties backgroundColor trans
/>
`;

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

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

exports[`<html.*> style polyfills "transition" properties spring() timing function: end 1`] = `
<Animated.View
animated={true}
style={
{
"boxSizing": "content-box",
"opacity": {
"inputRange": [
0,
1,
],
"outputRange": [
1,
0,
],
},
"position": "static",
}
}
/>
`;

exports[`<html.*> style polyfills "transition" properties spring() timing function: start 1`] = `
<Animated.View
animated={true}
style={
{
"boxSizing": "content-box",
"opacity": 1,
"position": "static",
}
}
/>
`;

exports[`<html.*> style polyfills "transition" properties transform transition: end 1`] = `
<Animated.View
animated={true}
Expand Down
68 changes: 66 additions & 2 deletions packages/react-strict-dom/tests/html-test.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -701,10 +701,9 @@ describe('<html.*>', () => {
expect(Easing.out).toHaveBeenCalled();
});

test('cubic-bezier easing', () => {
test('cubic-bezier() timing function', () => {
const BEZIER_STR = 'cubic-bezier( 0.1, 0.2,0.3 ,0.4)';
let root;
// cubic-bezier easing
act(() => {
root = create(<html.div style={styles.opacity(1, BEZIER_STR)} />);
});
Expand All @@ -717,6 +716,71 @@ describe('<html.*>', () => {
expect(Easing.bezier).toHaveBeenCalledWith(0.1, 0.2, 0.3, 0.4);
});

test('spring() timing function', () => {
// spring(mass, stiffness, damping, initialVelocity)
const SPRING_STR = 'spring( 1, 2,3 , 4 )';
let root;
act(() => {
root = create(<html.div style={styles.opacity(1, SPRING_STR)} />);
});
expect(root.toJSON()).toMatchSnapshot('start');
expect(Animated.spring).not.toHaveBeenCalled();
expect(Animated.timing).not.toHaveBeenCalled();
act(() => {
root.update(<html.div style={styles.opacity(0, SPRING_STR)} />);
});
expect(root.toJSON()).toMatchSnapshot('end');
// Animated.spring(damping, mass, stiffness, toValue, useNativeDriver, velocity)
expect(Animated.spring).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
damping: 3,
mass: 1,
stiffness: 2,
toValue: 1,
useNativeDriver: true,
velocity: 4
})
);
expect(Animated.timing).not.toHaveBeenCalled();

// Test that we replace invalid spring params and print error msg
const SPRING_BAD_STR = 'spring(0,0,-1,0)';
let badRoot;
act(() => {
badRoot = create(
<html.div style={styles.opacity(1, SPRING_BAD_STR)} />
);
});
act(() => {
badRoot.update(
<html.div style={styles.opacity(0, SPRING_BAD_STR)} />
);
});
expect(console.error).toHaveBeenCalledWith(
expect.stringContaining('"mass" must be greater than 0')
);
expect(console.error).toHaveBeenCalledWith(
expect.stringContaining('"stiffness" must be greater than 0')
);
expect(console.error).toHaveBeenCalledWith(
expect.stringContaining(
'"damping" must be greater than or equal to 0'
)
);
expect(Animated.spring).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
damping: 10,
mass: 1,
stiffness: 100,
toValue: 1,
useNativeDriver: true,
velocity: 0
})
);
});

test('transition all properties (opacity and transform)', () => {
let root;
// transition all properties (opacity and transform)
Expand Down