Files
react-navigation/packages/stack/src/views/Stack/Stack.tsx
2019-08-24 02:12:48 +01:00

444 lines
13 KiB
TypeScript
Executable File

import * as React from 'react';
import {
View,
StyleSheet,
LayoutChangeEvent,
Dimensions,
Platform,
ViewProps,
} from 'react-native';
import Animated from 'react-native-reanimated';
// eslint-disable-next-line import/no-unresolved
import * as Screens from 'react-native-screens'; // Import with * as to prevent getters being called
import { Route } from '@react-navigation/core';
import { StackNavigationState } from '@react-navigation/routers';
import { getDefaultHeaderHeight } from '../Header/HeaderSegment';
import { Props as HeaderContainerProps } from '../Header/HeaderContainer';
import StackItem from './StackItem';
import {
DefaultTransition,
ModalTransition,
} from '../../TransitionConfigs/TransitionPresets';
import { forNoAnimation } from '../../TransitionConfigs/HeaderStyleInterpolators';
import {
Layout,
HeaderMode,
HeaderScene,
StackDescriptorMap,
StackNavigationOptions,
StackNavigationHelpers,
} from '../../types';
type ProgressValues = {
[key: string]: Animated.Value<number>;
};
type Props = {
mode: 'card' | 'modal';
state: StackNavigationState;
navigation: StackNavigationHelpers;
descriptors: StackDescriptorMap;
routes: Route<string>[];
openingRoutes: string[];
closingRoutes: string[];
onGoBack: (props: { route: Route<string> }) => void;
onOpenRoute: (props: { route: Route<string> }) => void;
onCloseRoute: (props: { route: Route<string> }) => void;
getPreviousRoute: (props: {
route: Route<string>;
}) => Route<string> | undefined;
getGesturesEnabled: (props: { route: Route<string> }) => boolean;
renderHeader: (props: HeaderContainerProps) => React.ReactNode;
renderScene: (props: { route: Route<string> }) => React.ReactNode;
headerMode: HeaderMode;
onPageChangeStart?: () => void;
onPageChangeConfirm?: () => void;
onPageChangeCancel?: () => void;
};
type State = {
routes: Route<string>[];
descriptors: StackDescriptorMap;
scenes: HeaderScene<Route<string>>[];
progress: ProgressValues;
layout: Layout;
floatingHeaderHeights: { [key: string]: number };
};
const dimensions = Dimensions.get('window');
const layout = { width: dimensions.width, height: dimensions.height };
let AnimatedScreen: React.ComponentType<
ViewProps & { active: number | Animated.Node<number> }
>;
const MaybeScreenContainer = ({
enabled,
...rest
}: ViewProps & {
enabled: boolean;
children: React.ReactNode;
}) => {
if (Platform.OS !== 'ios' && enabled && Screens.screensEnabled()) {
return <Screens.ScreenContainer {...rest} />;
}
return <View {...rest} />;
};
const MaybeScreen = ({
enabled,
active,
...rest
}: ViewProps & {
enabled: boolean;
active: number | Animated.Node<number>;
children: React.ReactNode;
}) => {
if (Platform.OS !== 'ios' && enabled && Screens.screensEnabled()) {
AnimatedScreen =
AnimatedScreen || Animated.createAnimatedComponent(Screens.NativeScreen);
return <AnimatedScreen active={active} {...rest} />;
}
return <View {...rest} />;
};
const { cond, eq } = Animated;
const ANIMATED_ONE = new Animated.Value(1);
const getFloatingHeaderHeights = (
routes: Route<string>[],
layout: Layout,
previous: { [key: string]: number }
) => {
const defaultHeaderHeight = getDefaultHeaderHeight(layout);
return routes.reduce(
(acc, curr) => {
acc[curr.key] = previous[curr.key] || defaultHeaderHeight;
return acc;
},
{} as { [key: string]: number }
);
};
export default class Stack extends React.Component<Props, State> {
static getDerivedStateFromProps(props: Props, state: State) {
if (
props.routes === state.routes &&
props.descriptors === state.descriptors
) {
return null;
}
const progress = props.routes.reduce(
(acc, curr) => {
const descriptor = props.descriptors[curr.key];
acc[curr.key] =
state.progress[curr.key] ||
new Animated.Value(
props.openingRoutes.includes(curr.key) &&
descriptor &&
descriptor.options.animationEnabled !== false
? 0
: 1
);
return acc;
},
{} as ProgressValues
);
return {
routes: props.routes,
scenes: props.routes.map((route, index, self) => {
const previousRoute = self[index - 1];
const nextRoute = self[index + 1];
const current = progress[route.key];
const previous = previousRoute
? progress[previousRoute.key]
: undefined;
const next = nextRoute ? progress[nextRoute.key] : undefined;
const scene = {
route,
previous: previousRoute,
descriptor:
props.descriptors[route.key] || state.descriptors[route.key],
progress: {
current,
next,
previous,
},
};
const oldScene = state.scenes[index];
if (
oldScene &&
scene.route === oldScene.route &&
scene.progress.current === oldScene.progress.current &&
scene.progress.next === oldScene.progress.next &&
scene.progress.previous === oldScene.progress.previous &&
scene.descriptor === oldScene.descriptor
) {
return oldScene;
}
return scene;
}),
progress,
descriptors: props.descriptors,
floatingHeaderHeights: getFloatingHeaderHeights(
props.routes,
state.layout,
state.floatingHeaderHeights
),
};
}
state: State = {
routes: [],
scenes: [],
progress: {},
layout,
descriptors: this.props.descriptors,
// Used when card's header is null and mode is float to make transition
// between screens with headers and those without headers smooth.
// This is not a great heuristic here. We don't know synchronously
// on mount what the header height is so we have just used the most
// common cases here.
floatingHeaderHeights: {},
};
private handleLayout = (e: LayoutChangeEvent) => {
const { height, width } = e.nativeEvent.layout;
if (
height === this.state.layout.height &&
width === this.state.layout.width
) {
return;
}
const layout = { width, height };
this.setState({
layout,
floatingHeaderHeights: getFloatingHeaderHeights(
this.props.routes,
layout,
{}
),
});
};
private handleFloatingHeaderLayout = ({
route,
height,
}: {
route: Route<string>;
height: number;
}) => {
const previousHeight = this.state.floatingHeaderHeights[route.key];
if (previousHeight && previousHeight === height) {
return;
}
this.setState(state => ({
floatingHeaderHeights: {
...state.floatingHeaderHeights,
[route.key]: height,
},
}));
};
private handleTransitionStart = (
{ route }: { route: Route<string> },
closing: boolean
) =>
this.props.navigation.emit({
type: 'transitionStart',
data: { closing },
target: route.key,
});
private handleTransitionEnd = (
{ route }: { route: Route<string> },
closing: boolean
) =>
this.props.navigation.emit({
type: 'transitionEnd',
data: { closing },
target: route.key,
});
render() {
const {
mode,
descriptors,
state,
navigation,
routes,
closingRoutes,
onOpenRoute,
onCloseRoute,
onGoBack,
getPreviousRoute,
getGesturesEnabled,
renderHeader,
renderScene,
headerMode,
onPageChangeStart,
onPageChangeConfirm,
onPageChangeCancel,
} = this.props;
const { scenes, layout, progress, floatingHeaderHeights } = this.state;
const focusedRoute = state.routes[state.index];
const focusedDescriptor = descriptors[focusedRoute.key];
const focusedOptions = focusedDescriptor ? focusedDescriptor.options : {};
let defaultTransitionPreset =
mode === 'modal' ? ModalTransition : DefaultTransition;
if (headerMode === 'screen') {
defaultTransitionPreset = {
...defaultTransitionPreset,
headerStyleInterpolator: forNoAnimation,
};
}
return (
<React.Fragment>
<MaybeScreenContainer
enabled={mode !== 'modal'}
style={styles.container}
onLayout={this.handleLayout}
>
{routes.map((route, index, self) => {
const focused = focusedRoute.key === route.key;
const current = progress[route.key];
const scene = scenes[index];
const descriptor = scene.descriptor;
const next = self[index + 1]
? progress[self[index + 1].key]
: ANIMATED_ONE;
// Display current screen and a screen beneath. On Android screen beneath is hidden on animation finished bs of RNS's issue.
const isScreenActive =
index === self.length - 1
? 1
: Platform.OS === 'android'
? cond(eq(next, 1), 0, 1)
: index === self.length - 2
? 1
: 0;
const {
header,
headerTransparent,
cardTransparent,
cardShadowEnabled,
cardOverlayEnabled,
cardStyle,
gestureResponseDistance,
gestureDirection = defaultTransitionPreset.gestureDirection,
transitionSpec = defaultTransitionPreset.transitionSpec,
cardStyleInterpolator = defaultTransitionPreset.cardStyleInterpolator,
headerStyleInterpolator = defaultTransitionPreset.headerStyleInterpolator,
} = descriptor
? descriptor.options
: ({} as StackNavigationOptions);
return (
<MaybeScreen
key={route.key}
style={StyleSheet.absoluteFill}
enabled={mode !== 'modal'}
active={isScreenActive}
pointerEvents="box-none"
>
<StackItem
index={index}
active={index === self.length - 1}
focused={focused}
closing={closingRoutes.includes(route.key)}
layout={layout}
current={current}
scene={scene}
previousScene={scenes[index - 1]}
navigation={navigation}
state={state}
cardTransparent={cardTransparent}
cardOverlayEnabled={cardOverlayEnabled}
cardShadowEnabled={cardShadowEnabled}
cardStyle={cardStyle}
gestureEnabled={index !== 0 && getGesturesEnabled({ route })}
onPageChangeStart={onPageChangeStart}
onPageChangeConfirm={onPageChangeConfirm}
onPageChangeCancel={onPageChangeCancel}
gestureResponseDistance={gestureResponseDistance}
floatingHeaderHeight={floatingHeaderHeights[route.key]}
hasCustomHeader={header === null}
getPreviousRoute={getPreviousRoute}
headerMode={headerMode}
headerTransparent={headerTransparent}
renderHeader={renderHeader}
renderScene={renderScene}
onOpenRoute={onOpenRoute}
onCloseRoute={onCloseRoute}
onTransitionStart={this.handleTransitionStart}
onTransitionEnd={this.handleTransitionEnd}
onGoBack={onGoBack}
gestureDirection={gestureDirection}
transitionSpec={transitionSpec}
cardStyleInterpolator={cardStyleInterpolator}
headerStyleInterpolator={headerStyleInterpolator}
/>
</MaybeScreen>
);
})}
</MaybeScreenContainer>
{headerMode === 'float'
? renderHeader({
mode: 'float',
layout,
scenes,
state,
getPreviousRoute,
onContentHeightChange: this.handleFloatingHeaderLayout,
styleInterpolator:
focusedOptions.headerStyleInterpolator !== undefined
? focusedOptions.headerStyleInterpolator
: defaultTransitionPreset.headerStyleInterpolator,
style: styles.floating,
})
: null}
</React.Fragment>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
overflow: 'hidden',
},
floating: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
},
});