From 3f853d458f79a6abe30425a773d368ebf4d37680 Mon Sep 17 00:00:00 2001 From: Michal Osadnik Date: Sun, 1 Sep 2019 02:09:48 +0100 Subject: [PATCH] feat: optimizations in stack --- packages/stack/src/views/Stack/Card.tsx | 184 +++++++++++++++++++----- 1 file changed, 145 insertions(+), 39 deletions(-) diff --git a/packages/stack/src/views/Stack/Card.tsx b/packages/stack/src/views/Stack/Card.tsx index 82594e8b..037f4176 100755 --- a/packages/stack/src/views/Stack/Card.tsx +++ b/packages/stack/src/views/Stack/Card.tsx @@ -13,7 +13,13 @@ import { PanGestureHandler, State as GestureState, } from 'react-native-gesture-handler'; -import { TransitionSpec, CardStyleInterpolator, Layout } from '../../types'; +import { + TransitionSpec, + CardStyleInterpolator, + Layout, + SpringConfig, + TimingConfig, +} from '../../types'; import memoize from '../../utils/memoize'; import StackGestureContext from '../../utils/StackGestureContext'; import PointerEventsView from './PointerEventsView'; @@ -50,12 +56,31 @@ type Props = ViewProps & { contentStyle?: StyleProp; }; +type AnimatedSpringConfig = { + damping: Animated.Value; + mass: Animated.Value; + stiffness: Animated.Value; + restSpeedThreshold: Animated.Value; + restDisplacementThreshold: Animated.Value; + overshootClamping: Animated.Value; +}; + +export type AnimatedTimingConfig = { + duration: Animated.Value; + easing: Animated.EasingFunction; +}; + type Binary = 0 | 1; const TRUE = 1; +const TRUE_NODE = new Animated.Value(TRUE); const FALSE = 0; -const NOOP = 0; +const FALSE_NODE = new Animated.Value(FALSE); +const NOOP_NODE = FALSE_NODE; const UNSET = -1; +const UNSET_NODE = new Animated.Value(UNSET); + +const MINUS_ONE_NODE = UNSET_NODE; const DIRECTION_VERTICAL = -1; const DIRECTION_HORIZONTAL = 1; @@ -110,7 +135,7 @@ if (Animated.proc) { damping: Animated.Adaptable, mass: Animated.Adaptable, stiffness: Animated.Adaptable, - overshootClamping: Animated.Adaptable, + overshootClamping: Animated.Adaptable, restSpeedThreshold: Animated.Adaptable, restDisplacementThreshold: Animated.Adaptable, clock: Animated.Clock @@ -174,6 +199,30 @@ if (Animated.proc) { }; } +function transformSpringConfigToAnimatedValues( + config: SpringConfig +): AnimatedSpringConfig { + return { + damping: new Animated.Value(config.damping), + stiffness: new Animated.Value(config.stiffness), + mass: new Animated.Value(config.mass), + restDisplacementThreshold: new Animated.Value( + config.restDisplacementThreshold + ), + restSpeedThreshold: new Animated.Value(config.restSpeedThreshold), + overshootClamping: new Animated.Value(config.overshootClamping), + }; +} + +function transformTimingConfigToAnimatedValues( + config: TimingConfig +): AnimatedTimingConfig { + return { + duration: new Animated.Value(config.duration), + easing: config.easing, + }; +} + export default class Card extends React.Component { static defaultProps = { overlayEnabled: Platform.OS !== 'ios', @@ -244,14 +293,34 @@ export default class Card extends React.Component { height: new Value(this.props.layout.height), }; + openingSpecConfig = + this.props.transitionSpec.open.animation === 'timing' + ? transformTimingConfigToAnimatedValues( + this.props.transitionSpec.open.config + ) + : transformSpringConfigToAnimatedValues( + this.props.transitionSpec.open.config + ); + + closingSpecConfig = + this.props.transitionSpec.close.animation === 'timing' + ? transformTimingConfigToAnimatedValues( + this.props.transitionSpec.close.config + ) + : transformSpringConfigToAnimatedValues( + this.props.transitionSpec.close.config + ); + private distance = cond( eq(this.direction, DIRECTION_VERTICAL), this.layout.height, this.layout.width ); + private gestureUntraversed = new Value(0); private gesture = new Value(0); private offset = new Value(0); + private velocityUntraversed = new Value(0); private velocity = new Value(0); private gestureState = new Value(0); @@ -285,8 +354,8 @@ export default class Card extends React.Component { private runTransition = (isVisible: Binary | Animated.Node) => { const { open: openingSpec, close: closingSpec } = this.props.transitionSpec; - return cond(eq(this.props.current, isVisible), NOOP, [ - cond(clockRunning(this.clock), NOOP, [ + return cond(eq(this.props.current, isVisible), NOOP_NODE, [ + cond(clockRunning(this.clock), NOOP_NODE, [ // Animation wasn't running before // Set the initial values and start the clock set(this.toValue, isVisible), @@ -295,13 +364,17 @@ export default class Card extends React.Component { set( this.transitionVelocity, multiply( - cond(this.distance, divide(this.velocity, this.distance), 0), + cond( + this.distance, + divide(this.velocity, this.distance), + FALSE_NODE + ), -1 ) ), - set(this.frameTime, 0), - set(this.transitionState.time, 0), - set(this.transitionState.finished, FALSE), + set(this.frameTime, FALSE_NODE), + set(this.transitionState.time, FALSE_NODE), + set(this.transitionState.finished, FALSE_NODE), set(this.isVisible, isVisible), startClock(this.clock), call([this.isVisible], ([value]: ReadonlyArray) => { @@ -312,35 +385,49 @@ export default class Card extends React.Component { }), ]), cond( - eq(isVisible, 1), + eq(isVisible, TRUE_NODE), openingSpec.animation === 'spring' ? memoizedSpring( this.clock, { ...this.transitionState, velocity: this.transitionVelocity }, - { ...openingSpec.config, toValue: this.toValue } + // @ts-ignore + { + ...(this.openingSpecConfig as AnimatedSpringConfig), + toValue: this.toValue, + } ) : timing( this.clock, { ...this.transitionState, frameTime: this.frameTime }, - { ...openingSpec.config, toValue: this.toValue } + { + ...(this.openingSpecConfig as AnimatedTimingConfig), + toValue: this.toValue, + } ), closingSpec.animation === 'spring' ? memoizedSpring( this.clock, { ...this.transitionState, velocity: this.transitionVelocity }, - { ...closingSpec.config, toValue: this.toValue } + // @ts-ignore + { + ...(this.closingSpecConfig as AnimatedSpringConfig), + toValue: this.toValue, + } ) : timing( this.clock, { ...this.transitionState, frameTime: this.frameTime }, - { ...closingSpec.config, toValue: this.toValue } + { + ...(this.closingSpecConfig as AnimatedTimingConfig), + toValue: this.toValue, + } ) ), cond(this.transitionState.finished, [ // Reset values - set(this.isSwipeGesture, FALSE), - set(this.gesture, 0), - set(this.velocity, 0), + set(this.isSwipeGesture, FALSE_NODE), + set(this.gesture, FALSE_NODE), + set(this.velocity, FALSE_NODE), // When the animation finishes, stop the clock stopClock(this.clock), call([this.isVisible], ([value]: ReadonlyArray) => { @@ -365,22 +452,36 @@ export default class Card extends React.Component { ); private exec = block([ + set( + this.gesture, + multiply( + this.gestureUntraversed, + I18nManager.isRTL ? MINUS_ONE_NODE : TRUE_NODE + ) + ), + set( + this.velocity, + multiply( + this.velocityUntraversed, + I18nManager.isRTL ? MINUS_ONE_NODE : TRUE_NODE + ) + ), onChange( this.isClosing, - cond(this.isClosing, set(this.nextIsVisible, FALSE)) + cond(this.isClosing, set(this.nextIsVisible, FALSE_NODE)) ), onChange( this.nextIsVisible, - cond(neq(this.nextIsVisible, UNSET), [ + cond(neq(this.nextIsVisible, UNSET_NODE), [ // Stop any running animations cond(clockRunning(this.clock), [ call([], this.handleTransitionEnd), stopClock(this.clock), ]), - set(this.gesture, 0), + set(this.gesture, FALSE_NODE), // Update the index to trigger the transition set(this.isVisible, this.nextIsVisible), - set(this.nextIsVisible, UNSET), + set(this.nextIsVisible, UNSET_NODE), ]) ), onChange( @@ -418,11 +519,11 @@ export default class Card extends React.Component { cond( eq(this.gestureState, GestureState.ACTIVE), [ - cond(this.isSwiping, NOOP, [ + cond(this.isSwiping, NOOP_NODE, [ // We weren't dragging before, set it to true - set(this.isSwipeCancelled, FALSE), - set(this.isSwiping, TRUE), - set(this.isSwipeGesture, TRUE), + set(this.isSwipeCancelled, FALSE_NODE), + set(this.isSwiping, TRUE_NODE), + set(this.isSwipeGesture, TRUE_NODE), // Also update the drag offset to the last position set(this.offset, this.props.current), ]), @@ -433,11 +534,15 @@ export default class Card extends React.Component { max( sub( this.offset, - cond(this.distance, divide(this.gesture, this.distance), 1) + cond( + this.distance, + divide(this.gesture, this.distance), + TRUE_NODE + ) ), - 0 + FALSE_NODE ), - 1 + TRUE_NODE ) ), // Stop animations while we're dragging @@ -460,7 +565,7 @@ export default class Card extends React.Component { this.isSwipeCancelled, eq(this.gestureState, GestureState.CANCELLED) ), - set(this.isSwiping, FALSE), + set(this.isSwiping, FALSE_NODE), this.runTransition( cond( greaterThan( @@ -469,11 +574,15 @@ export default class Card extends React.Component { ), cond( lessThan( - cond(eq(this.velocity, 0), this.gesture, this.velocity), - 0 + cond( + eq(this.velocity, FALSE_NODE), + this.gesture, + this.velocity + ), + FALSE_NODE ), - TRUE, - FALSE + TRUE_NODE, + FALSE_NODE ), this.isVisible ) @@ -485,11 +594,8 @@ export default class Card extends React.Component { private handleGestureEventHorizontal = Animated.event([ { nativeEvent: { - translationX: (x: Animated.Adaptable) => - set(this.gesture, multiply(x, I18nManager.isRTL ? -1 : 1)), - velocityX: (x: Animated.Adaptable) => - set(this.velocity, multiply(x, I18nManager.isRTL ? -1 : 1)), - state: this.gestureState, + translationX: this.gestureUntraversed, + velocityX: this.velocityUntraversed, }, }, ]); @@ -526,7 +632,7 @@ export default class Card extends React.Component { }) ); - // Keep track of the style in a property to avoid changing the animated node when deps change + // Keep track of the style in a property to avoid changing the animated node when deps change` // The style shouldn't change in the middle of the animation and should refer to what was there at the start of it // Which will be the last value when just before the render which started the animation // We need to make sure to update this when the running animation ends