From efdfffaeee3f789850b36893d9253b4dd28e31ed Mon Sep 17 00:00:00 2001 From: "satyajit.happy" Date: Thu, 6 Jun 2019 12:32:33 +0200 Subject: [PATCH] refactor: refactor and perf improvements --- .../src/TransitionConfigs/TransitionSpecs.tsx | 1 + packages/stack/src/types.tsx | 3 +- packages/stack/src/views/Header/Header.tsx | 12 +- .../src/views/Header/HeaderBackButton.tsx | 33 ++--- .../src/views/Header/HeaderBackground.tsx | 4 +- .../src/views/Header/HeaderContainer.tsx | 2 +- .../stack/src/views/Header/HeaderSegment.tsx | 19 +-- .../stack/src/views/Header/HeaderTitle.tsx | 7 +- packages/stack/src/views/Stack/Card.tsx | 114 ++++++++---------- packages/stack/src/views/Stack/Stack.tsx | 2 - packages/stack/src/views/Stack/StackView.tsx | 22 ++-- packages/stack/src/views/Stack/Swipeable.tsx | 105 ---------------- 12 files changed, 106 insertions(+), 218 deletions(-) delete mode 100644 packages/stack/src/views/Stack/Swipeable.tsx diff --git a/packages/stack/src/TransitionConfigs/TransitionSpecs.tsx b/packages/stack/src/TransitionConfigs/TransitionSpecs.tsx index 426b541c..2f3c1cec 100644 --- a/packages/stack/src/TransitionConfigs/TransitionSpecs.tsx +++ b/packages/stack/src/TransitionConfigs/TransitionSpecs.tsx @@ -1,6 +1,7 @@ import { Easing } from 'react-native-reanimated'; import { TransitionSpec } from '../types'; +// These are the exact values from UINavigationController's animation configuration export const TransitionIOSSpec: TransitionSpec = { timing: 'spring', config: { diff --git a/packages/stack/src/types.tsx b/packages/stack/src/types.tsx index 94fa8def..c2f3c87d 100644 --- a/packages/stack/src/types.tsx +++ b/packages/stack/src/types.tsx @@ -69,6 +69,7 @@ export type HeaderOptions = { headerBackTitle?: string; headerBackTitleStyle?: StyleProp; headerTruncatedBackTitle?: string; + headerTitleContainerStyle?: StyleProp; headerLeft?: (props: HeaderBackButtonProps) => React.ReactNode; headerLeftContainerStyle?: StyleProp; headerRight?: () => React.ReactNode; @@ -118,7 +119,7 @@ export type HeaderBackButtonProps = { disabled?: boolean; onPress?: () => void; pressColorAndroid?: string; - backImage?: (props: { tintColor: string; label?: string }) => React.ReactNode; + backImage?: (props: { tintColor: string }) => React.ReactNode; tintColor?: string; label?: string; truncatedLabel?: string; diff --git a/packages/stack/src/views/Header/Header.tsx b/packages/stack/src/views/Header/Header.tsx index cfe21eaa..1120ad6c 100644 --- a/packages/stack/src/views/Header/Header.tsx +++ b/packages/stack/src/views/Header/Header.tsx @@ -18,14 +18,14 @@ export default class Header extends React.PureComponent { let leftLabel; + // The label for the left back button shows the title of the previous screen + // If a custom label is specified, we use it, otherwise use previous screen's title if (options.headerBackTitle !== undefined) { leftLabel = options.headerBackTitle; - } else { - if (previous) { - const opts = previous.descriptor.options; - leftLabel = - opts.headerTitle !== undefined ? opts.headerTitle : opts.title; - } + } else if (previous) { + const opts = previous.descriptor.options; + leftLabel = + opts.headerTitle !== undefined ? opts.headerTitle : opts.title; } return ( diff --git a/packages/stack/src/views/Header/HeaderBackButton.tsx b/packages/stack/src/views/Header/HeaderBackButton.tsx index 81a05b1a..ed6390c9 100644 --- a/packages/stack/src/views/Header/HeaderBackButton.tsx +++ b/packages/stack/src/views/Header/HeaderBackButton.tsx @@ -42,6 +42,9 @@ class HeaderBackButton extends React.Component { return; } + // This measurement is used to determine if we should truncate the label when it doesn't fit + // Only measure it once because `onLayout` will fire again when we render truncated label + // and we want the measurement of not-truncated label this.setState({ initialLabelWidth: e.nativeEvent.layout.x + e.nativeEvent.layout.width, }); @@ -50,17 +53,15 @@ class HeaderBackButton extends React.Component { private renderBackImage() { const { backImage, labelVisible, tintColor } = this.props; - let label = this.getLabelText(); - if (backImage) { - return backImage({ tintColor, label }); + return backImage({ tintColor }); } else { return ( { private getLabelText = () => { const { titleLayout, screenLayout, label, truncatedLabel } = this.props; - let { initialLabelWidth: initialLabelWidth } = this.state; + let { initialLabelWidth } = this.state; if (!label) { return truncatedLabel; @@ -88,7 +89,7 @@ class HeaderBackButton extends React.Component { } }; - private maybeRenderTitle() { + private renderLabel() { const { allowFontScaling, labelVisible, @@ -104,12 +105,12 @@ class HeaderBackButton extends React.Component { return null; } - const title = ( + const label = ( { ); if (backImage) { - return title; + // When a custom backimage is specified, we can't mask the label + // Otherwise there might be weird effect due to our mask not being the same as the image + return label; } return ( @@ -137,7 +140,7 @@ class HeaderBackButton extends React.Component { } > - {title} + {label} ); } @@ -171,7 +174,7 @@ class HeaderBackButton extends React.Component { > {this.renderBackImage()} - {this.maybeRenderTitle()} + {this.renderLabel()} ); @@ -193,9 +196,9 @@ const styles = StyleSheet.create({ disabled: { opacity: 0.5, }, - title: { + label: { fontSize: 17, - // Title and back title are a bit different width due to title being bold + // Title and back label are a bit different width due to title being bold // Adjusting the letterSpacing makes them coincide better letterSpacing: 0.35, }, @@ -217,7 +220,7 @@ const styles = StyleSheet.create({ transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }], }, }), - iconWithTitle: + iconWithLabel: Platform.OS === 'ios' ? { marginRight: 6, diff --git a/packages/stack/src/views/Header/HeaderBackground.tsx b/packages/stack/src/views/Header/HeaderBackground.tsx index 05023000..1e721468 100644 --- a/packages/stack/src/views/Header/HeaderBackground.tsx +++ b/packages/stack/src/views/Header/HeaderBackground.tsx @@ -1,9 +1,7 @@ import * as React from 'react'; import { View, StyleSheet, Platform, ViewProps } from 'react-native'; -type Props = ViewProps; - -export default function HeaderBackground({ style, ...rest }: Props) { +export default function HeaderBackground({ style, ...rest }: ViewProps) { return ; } diff --git a/packages/stack/src/views/Header/HeaderContainer.tsx b/packages/stack/src/views/Header/HeaderContainer.tsx index 5295f8eb..aac750ab 100644 --- a/packages/stack/src/views/Header/HeaderContainer.tsx +++ b/packages/stack/src/views/Header/HeaderContainer.tsx @@ -60,7 +60,7 @@ export default function HeaderContainer({ { headerBackTitleStyle: customLeftLabelStyle, headerLeftContainerStyle: leftContainerStyle, headerRightContainerStyle: rightContainerStyle, + headerTitleContainerStyle: titleContainerStyle, styleInterpolator, } = this.props; @@ -196,21 +197,25 @@ export default class HeaderSegment extends React.Component { ) : null} {currentTitle ? ( - - {currentTitle} - + + {currentTitle} + + ) : null} {right ? ( & { +type Props = TextProps & { children: string; }; export default function HeaderTitle({ style, ...rest }: Props) { - return ; + return ; } const styles = StyleSheet.create({ diff --git a/packages/stack/src/views/Stack/Card.tsx b/packages/stack/src/views/Stack/Card.tsx index c7dc771d..72e888ed 100755 --- a/packages/stack/src/views/Stack/Card.tsx +++ b/packages/stack/src/views/Stack/Card.tsx @@ -23,7 +23,6 @@ type Props = ViewProps & { onGestureCanceled?: () => void; onGestureEnd?: () => void; children: React.ReactNode; - animateIn: boolean; gesturesEnabled: boolean; gestureResponseDistance?: { vertical?: number; @@ -58,54 +57,46 @@ const GESTURE_RESPONSE_DISTANCE_HORIZONTAL = 50; const GESTURE_RESPONSE_DISTANCE_VERTICAL = 135; const { - cond, - eq, - neq, - set, + abs, and, - or, + block, + call, + cond, + divide, + eq, greaterThan, lessThan, - abs, - add, max, - block, - stopClock, - startClock, - clockRunning, + min, + neq, onChange, - Value, - Clock, - call, + or, + set, spring, + sub, timing, - interpolate, + startClock, + stopClock, + clockRunning, + Clock, + Value, } = Animated; export default class Card extends React.Component { static defaultProps = { - animateIn: true, gesturesEnabled: true, }; componentDidUpdate(prevProps: Props) { - const { layout, direction, closing, animateIn } = this.props; + const { layout, direction, closing } = this.props; const { width, height } = layout; - if ( - width !== prevProps.layout.width || - height !== prevProps.layout.height - ) { + if (width !== prevProps.layout.width) { this.layout.width.setValue(width); - this.layout.height.setValue(height); + } - this.position.setValue( - animateIn - ? direction === 'vertical' - ? layout.height - : layout.width - : 0 - ); + if (height !== prevProps.layout.height) { + this.layout.height.setValue(height); } if (direction !== prevProps.direction) { @@ -143,14 +134,6 @@ export default class Card extends React.Component { this.layout.width ); - private position = new Value( - this.props.animateIn - ? this.props.direction === 'vertical' - ? this.props.layout.height - : this.props.layout.width - : 0 - ); - private gesture = new Value(0); private offset = new Value(0); private velocity = new Value(0); @@ -164,8 +147,10 @@ export default class Card extends React.Component { private toValue = new Value(0); private frameTime = new Value(0); + private transitionVelocity = new Value(0); + private transitionState = { - position: this.position, + position: this.props.current, time: new Value(0), finished: new Value(FALSE), }; @@ -173,13 +158,17 @@ export default class Card extends React.Component { private runTransition = (isVisible: Binary | Animated.Node) => { const { open: openingSpec, close: closingSpec } = this.props.transitionSpec; - const toValue = cond(isVisible, 0, this.distance); - - return cond(eq(this.position, toValue), NOOP, [ + return cond(eq(this.props.current, isVisible), NOOP, [ cond(clockRunning(this.clock), NOOP, [ // Animation wasn't running before // Set the initial values and start the clock - set(this.toValue, toValue), + set(this.toValue, isVisible), + // The velocity value is ideal for translating the whole screen + // But since we have 0-1 scale, we need to adjust the velocity + set( + this.transitionVelocity, + cond(this.distance, divide(this.velocity, this.distance), 0) + ), set(this.frameTime, 0), set(this.transitionState.time, 0), set(this.transitionState.finished, FALSE), @@ -192,11 +181,11 @@ export default class Card extends React.Component { }), ]), cond( - eq(toValue, 0), + eq(isVisible, 1), openingSpec.timing === 'spring' ? spring( this.clock, - { ...this.transitionState, velocity: this.velocity }, + { ...this.transitionState, velocity: this.transitionVelocity }, { ...openingSpec.config, toValue: this.toValue } ) : timing( @@ -207,7 +196,7 @@ export default class Card extends React.Component { closingSpec.timing === 'spring' ? spring( this.clock, - { ...this.transitionState, velocity: this.velocity }, + { ...this.transitionState, velocity: this.transitionVelocity }, { ...closingSpec.config, toValue: this.toValue } ) : timing( @@ -237,7 +226,7 @@ export default class Card extends React.Component { ]); }; - private translate = block([ + private exec = block([ onChange( this.isClosing, cond(this.isClosing, set(this.nextIsVisible, FALSE)) @@ -276,18 +265,6 @@ export default class Card extends React.Component { } ) ), - // Synchronize the translation with the animated value representing the progress - set( - this.props.current, - cond( - or(eq(this.layout.width, 0), eq(this.layout.height, 0)), - this.isVisible, - interpolate(this.position, { - inputRange: [0, this.distance], - outputRange: [1, 0], - }) - ) - ), cond( eq(this.gestureState, GestureState.ACTIVE), [ @@ -297,10 +274,22 @@ export default class Card extends React.Component { set(this.isSwiping, TRUE), set(this.isSwipeGesture, TRUE), // Also update the drag offset to the last position - set(this.offset, this.position), + set(this.offset, this.props.current), ]), // Update position with next offset + gesture distance - set(this.position, max(add(this.offset, this.gesture), 0)), + set( + this.props.current, + min( + max( + sub( + this.offset, + cond(this.distance, divide(this.gesture, this.distance), 1) + ), + 0 + ), + 1 + ) + ), // Stop animations while we're dragging stopClock(this.clock), ], @@ -342,7 +331,6 @@ export default class Card extends React.Component { ), ] ), - this.position, ]); private handleGestureEventHorizontal = Animated.event([ @@ -442,7 +430,7 @@ export default class Card extends React.Component { return ( - + {overlayStyle ? ( { descriptors, navigation, routes, - openingRoutes, closingRoutes, onOpenRoute, onCloseRoute, @@ -206,7 +205,6 @@ export default class Stack extends React.Component { closing={closingRoutes.includes(route.key)} onOpen={() => onOpenRoute({ route })} onClose={() => onCloseRoute({ route })} - animateIn={openingRoutes.includes(route.key)} gesturesEnabled={getGesturesEnabled({ route })} onTransitionStart={({ closing }) => { onTransitionStart && diff --git a/packages/stack/src/views/Stack/StackView.tsx b/packages/stack/src/views/Stack/StackView.tsx index 34a7a6f5..b16cd94e 100644 --- a/packages/stack/src/views/Stack/StackView.tsx +++ b/packages/stack/src/views/Stack/StackView.tsx @@ -94,9 +94,9 @@ class StackView extends React.Component { const { routes } = this.props.navigation.state; const isFirst = routes[0].key === route.key; - const isLast = routes[routes.length - 1].key === route.key; - if (isFirst || !isLast) { + if (isFirst) { + // The user shouldn't be able to close the first screen return false; } @@ -109,29 +109,26 @@ class StackView extends React.Component { private renderScene = ({ route }: { route: Route }) => { const descriptor = this.state.descriptors[route.key]; - - if (!descriptor) { - return null; - } - const { navigation, getComponent } = descriptor; const SceneComponent = getComponent(); - const { screenProps } = this.props; - return ( ); }; - private handleGoBack = ({ route }: { route: Route }) => + private handleGoBack = ({ route }: { route: Route }) => { + // This event will trigger when a gesture ends + // We need to perform the transition before popping the route completely this.props.navigation.dispatch(StackActions.pop({ key: route.key })); + }; private handleTransitionComplete = ({ route }: { route: Route }) => { + // When transition completes, we need to remove the item from the pushing/popping arrays this.props.navigation.dispatch( StackActions.completeTransition({ toChildKey: route.key }) ); @@ -142,6 +139,9 @@ class StackView extends React.Component { }; private handleCloseRoute = ({ route }: { route: Route }) => { + // This event will trigger when the transition for closing the route ends + // In this case, we need to clean up any state tracking the route and pop it immediately + // @ts-ignore this.setState(state => ({ routes: state.routes.filter(r => r.key !== route.key), diff --git a/packages/stack/src/views/Stack/Swipeable.tsx b/packages/stack/src/views/Stack/Swipeable.tsx deleted file mode 100644 index e6a79f9f..00000000 --- a/packages/stack/src/views/Stack/Swipeable.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import * as React from 'react'; -import { ViewProps } from 'react-native'; -import Animated from 'react-native-reanimated'; -import { PanGestureHandler } from 'react-native-gesture-handler'; -import StackGestureContext from '../../utils/StackGestureContext'; -import { Layout } from '../../types'; - -type Props = ViewProps & { - gesture: Animated.Value; - velocity: Animated.Value; - gestureState: Animated.Value; - layout: Layout; - direction: 'horizontal' | 'vertical'; - gesturesEnabled: boolean; - gestureResponseDistance?: { - vertical?: number; - horizontal?: number; - }; - children: React.ReactNode; -}; - -/** - * The distance of touch start from the edge of the screen where the gesture will be recognized - */ -const GESTURE_RESPONSE_DISTANCE_HORIZONTAL = 50; -const GESTURE_RESPONSE_DISTANCE_VERTICAL = 135; - -export default class Swipeable extends React.Component { - private handleGestureEventHorizontal = Animated.event([ - { - nativeEvent: { - translationX: this.props.gesture, - velocityX: this.props.velocity, - state: this.props.gestureState, - }, - }, - ]); - - private handleGestureEventVertical = Animated.event([ - { - nativeEvent: { - translationY: this.props.gesture, - velocityY: this.props.velocity, - state: this.props.gestureState, - }, - }, - ]); - - private gestureActivationCriteria() { - const { layout, direction, gestureResponseDistance } = this.props; - - // Doesn't make sense for a response distance of 0, so this works fine - const distance = - direction === 'vertical' - ? (gestureResponseDistance && gestureResponseDistance.vertical) || - GESTURE_RESPONSE_DISTANCE_VERTICAL - : (gestureResponseDistance && gestureResponseDistance.horizontal) || - GESTURE_RESPONSE_DISTANCE_HORIZONTAL; - - if (direction === 'vertical') { - return { - maxDeltaX: 15, - minOffsetY: 5, - hitSlop: { bottom: -layout.height + distance }, - }; - } else { - return { - minOffsetX: 5, - maxDeltaY: 20, - hitSlop: { right: -layout.width + distance }, - }; - } - } - - private gestureRef: React.Ref = React.createRef(); - - render() { - const { - layout, - direction, - gesturesEnabled, - children, - ...rest - } = this.props; - - const handleGestureEvent = - direction === 'vertical' - ? this.handleGestureEventVertical - : this.handleGestureEventHorizontal; - - return ( - - - {children} - - - ); - } -}