refactor: refactor and perf improvements

This commit is contained in:
satyajit.happy
2019-06-06 12:32:33 +02:00
parent 9b176e9dc8
commit efdfffaeee
12 changed files with 106 additions and 218 deletions

View File

@@ -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: {

View File

@@ -69,6 +69,7 @@ export type HeaderOptions = {
headerBackTitle?: string;
headerBackTitleStyle?: StyleProp<TextStyle>;
headerTruncatedBackTitle?: string;
headerTitleContainerStyle?: StyleProp<ViewStyle>;
headerLeft?: (props: HeaderBackButtonProps) => React.ReactNode;
headerLeftContainerStyle?: StyleProp<ViewStyle>;
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;

View File

@@ -18,14 +18,14 @@ export default class Header extends React.PureComponent<HeaderProps> {
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 (

View File

@@ -42,6 +42,9 @@ class HeaderBackButton extends React.Component<Props, State> {
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<Props, State> {
private renderBackImage() {
const { backImage, labelVisible, tintColor } = this.props;
let label = this.getLabelText();
if (backImage) {
return backImage({ tintColor, label });
return backImage({ tintColor });
} else {
return (
<Image
style={[
styles.icon,
!!labelVisible && styles.iconWithTitle,
!!tintColor && { tintColor },
Boolean(labelVisible) && styles.iconWithLabel,
Boolean(tintColor) && { tintColor },
]}
source={require('../assets/back-icon.png')}
fadeDuration={0}
@@ -72,7 +73,7 @@ class HeaderBackButton extends React.Component<Props, State> {
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<Props, State> {
}
};
private maybeRenderTitle() {
private renderLabel() {
const {
allowFontScaling,
labelVisible,
@@ -104,12 +105,12 @@ class HeaderBackButton extends React.Component<Props, State> {
return null;
}
const title = (
const label = (
<Animated.Text
accessible={false}
onLayout={this.handleLabelLayout}
style={[
styles.title,
styles.label,
screenLayout ? { marginRight: screenLayout.width / 2 } : null,
tintColor ? { color: tintColor } : null,
labelStyle,
@@ -122,7 +123,9 @@ class HeaderBackButton extends React.Component<Props, State> {
);
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<Props, State> {
</View>
}
>
{title}
{label}
</MaskedViewIOS>
);
}
@@ -171,7 +174,7 @@ class HeaderBackButton extends React.Component<Props, State> {
>
<React.Fragment>
{this.renderBackImage()}
{this.maybeRenderTitle()}
{this.renderLabel()}
</React.Fragment>
</TouchableItem>
);
@@ -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,

View File

@@ -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 <View style={[styles.container, style]} {...rest} />;
}

View File

@@ -60,7 +60,7 @@ export default function HeaderContainer({
<View
key={scene.route.key}
onLayout={onLayout}
pointerEvents="box-none"
pointerEvents={isFocused ? 'box-none' : 'none'}
accessibilityElementsHidden={!isFocused}
importantForAccessibility={
isFocused ? 'auto' : 'no-hide-descendants'

View File

@@ -137,6 +137,7 @@ export default class HeaderSegment extends React.Component<Props, State> {
headerBackTitleStyle: customLeftLabelStyle,
headerLeftContainerStyle: leftContainerStyle,
headerRightContainerStyle: rightContainerStyle,
headerTitleContainerStyle: titleContainerStyle,
styleInterpolator,
} = this.props;
@@ -196,21 +197,25 @@ export default class HeaderSegment extends React.Component<Props, State> {
</Animated.View>
) : null}
{currentTitle ? (
<HeaderTitle
allowFontScaling={titleAllowFontScaling}
onLayout={this.handleTitleLayout}
<Animated.View
style={[
styles.title,
Platform.select({
ios: null,
default: { left: onGoBack ? 72 : 16 },
}),
styles.title,
titleStyle,
customTitleStyle,
titleContainerStyle,
]}
>
{currentTitle}
</HeaderTitle>
<HeaderTitle
onLayout={this.handleTitleLayout}
allowFontScaling={titleAllowFontScaling}
style={customTitleStyle}
>
{currentTitle}
</HeaderTitle>
</Animated.View>
) : null}
{right ? (
<Animated.View

View File

@@ -1,13 +1,12 @@
import * as React from 'react';
import { StyleSheet, Platform } from 'react-native';
import Animated from 'react-native-reanimated';
import { Text, StyleSheet, Platform, TextProps } from 'react-native';
type Props = React.ComponentProps<typeof Animated.Text> & {
type Props = TextProps & {
children: string;
};
export default function HeaderTitle({ style, ...rest }: Props) {
return <Animated.Text {...rest} style={[styles.title, style]} />;
return <Text {...rest} style={[styles.title, style]} />;
}
const styles = StyleSheet.create({

View File

@@ -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<Props> {
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<Props> {
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<Props> {
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<Props> {
private runTransition = (isVisible: Binary | Animated.Node<number>) => {
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<Props> {
}),
]),
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<Props> {
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<Props> {
]);
};
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<Props> {
}
)
),
// 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<Props> {
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<Props> {
),
]
),
this.position,
]);
private handleGestureEventHorizontal = Animated.event([
@@ -442,7 +430,7 @@ export default class Card extends React.Component<Props> {
return (
<StackGestureContext.Provider value={this.gestureRef}>
<View pointerEvents="box-none" {...rest}>
<Animated.Code exec={this.translate} />
<Animated.Code exec={this.exec} />
{overlayStyle ? (
<Animated.View
pointerEvents="none"

View File

@@ -160,7 +160,6 @@ export default class Stack extends React.Component<Props, State> {
descriptors,
navigation,
routes,
openingRoutes,
closingRoutes,
onOpenRoute,
onCloseRoute,
@@ -206,7 +205,6 @@ export default class Stack extends React.Component<Props, State> {
closing={closingRoutes.includes(route.key)}
onOpen={() => onOpenRoute({ route })}
onClose={() => onCloseRoute({ route })}
animateIn={openingRoutes.includes(route.key)}
gesturesEnabled={getGesturesEnabled({ route })}
onTransitionStart={({ closing }) => {
onTransitionStart &&

View File

@@ -94,9 +94,9 @@ class StackView extends React.Component<Props, State> {
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<Props, State> {
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 (
<SceneView
screenProps={screenProps}
screenProps={this.props.screenProps}
navigation={navigation}
component={SceneComponent}
/>
);
};
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<Props, State> {
};
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),

View File

@@ -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<number>;
velocity: Animated.Value<number>;
gestureState: Animated.Value<number>;
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<Props> {
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<PanGestureHandler> = React.createRef();
render() {
const {
layout,
direction,
gesturesEnabled,
children,
...rest
} = this.props;
const handleGestureEvent =
direction === 'vertical'
? this.handleGestureEventVertical
: this.handleGestureEventHorizontal;
return (
<StackGestureContext.Provider value={this.gestureRef}>
<PanGestureHandler
ref={this.gestureRef}
enabled={layout.width !== 0 && gesturesEnabled}
onGestureEvent={handleGestureEvent}
onHandlerStateChange={handleGestureEvent}
{...this.gestureActivationCriteria()}
>
<Animated.View {...rest}>{children}</Animated.View>
</PanGestureHandler>
</StackGestureContext.Provider>
);
}
}