/* @flow */
import * as React from 'react';
import PropTypes from 'prop-types';
import { Animated, View, StyleSheet } from 'react-native';
import TabBar from './TabBar';
import PagerDefault from './PagerDefault';
import { NavigationStatePropType } from './PropTypes';
import type {
Scene,
SceneRendererProps,
NavigationState,
Layout,
PagerCommonProps,
PagerExtraProps,
} from './TypeDefinitions';
import type { ViewStyleProp } from 'react-native/Libraries/StyleSheet/StyleSheet';
type Props<T> = PagerCommonProps<T> &
PagerExtraProps & {
navigationState: NavigationState<T>,
onIndexChange: (index: number) => mixed,
initialLayout?: Layout,
renderPager: (props: *) => React.Node,
renderScene: (props: SceneRendererProps<T> & Scene<T>) => React.Node,
renderTabBar: (props: SceneRendererProps<T>) => React.Node,
tabBarPosition: 'top' | 'bottom',
useNativeDriver?: boolean,
style?: ViewStyleProp,
};
type State = {|
layout: Layout & { measured: boolean },
layoutXY: Animated.ValueXY,
panX: Animated.Value,
offsetX: Animated.Value,
position: any,
renderUnfocusedScenes: boolean,
|};
export default class TabView<T: *> extends React.Component<Props<T>, State> {
static propTypes = {
navigationState: NavigationStatePropType.isRequired,
onIndexChange: PropTypes.func.isRequired,
initialLayout: PropTypes.shape({
height: PropTypes.number.isRequired,
width: PropTypes.number.isRequired,
}),
canJumpToTab: PropTypes.func.isRequired,
renderPager: PropTypes.func.isRequired,
renderScene: PropTypes.func.isRequired,
renderTabBar: PropTypes.func,
tabBarPosition: PropTypes.oneOf(['top', 'bottom']),
};
static defaultProps = {
canJumpToTab: () => true,
tabBarPosition: 'top',
renderTabBar: (props: *) => <TabBar {...props} />,
renderPager: (props: *) => <PagerDefault {...props} />,
getTestID: ({ route }: Scene<*>) =>
typeof route.testID === 'string' ? route.testID : undefined,
initialLayout: {
height: 0,
width: 0,
},
useNativeDriver: false,
};
constructor(props: Props<T>) {
super(props);
const { navigationState } = this.props;
const layout = {
...this.props.initialLayout,
measured: false,
};
const panX = new Animated.Value(0);
const offsetX = new Animated.Value(-navigationState.index * layout.width);
const layoutXY = new Animated.ValueXY({
// This is hacky, but we need to make sure that the value is never 0
x: layout.width || 0.001,
y: layout.height || 0.001,
});
const position = Animated.multiply(
Animated.divide(Animated.add(panX, offsetX), layoutXY.x),
-1
);
this.state = {
layout,
layoutXY,
panX,
offsetX,
position,
renderUnfocusedScenes: false,
};
}
componentDidMount() {
this._mounted = true;
// Delay rendering of unfocused scenes for improved startup
setTimeout(() => this.setState({ renderUnfocusedScenes: true }), 0);
}
componentWillUnmount() {
this._mounted = false;
}
_mounted: boolean = false;
_nextIndex: ?number;
_renderScene = (props: SceneRendererProps<T> & Scene<T>) => {
return this.props.renderScene(props);
};
_handleLayout = (e: any) => {
const { height, width } = e.nativeEvent.layout;
if (
this.state.layout.width === width &&
this.state.layout.height === height
) {
return;
}
this.state.panX.setValue(0);
this.state.offsetX.setValue(-this.props.navigationState.index * width);
this.state.layoutXY.setValue({
// This is hacky, but we need to make sure that the value is never 0
x: width || 0.001,
y: height || 0.001,
});
this.setState({
layout: {
measured: true,
height,
width,
},
});
};
_buildSceneRendererProps = (): SceneRendererProps<*> => ({
panX: this.state.panX,
offsetX: this.state.offsetX,
position: this.state.position,
layout: this.state.layout,
navigationState: this.props.navigationState,
jumpTo: this._jumpTo,
useNativeDriver: this.props.useNativeDriver === true,
});
_jumpTo = (key: string) => {
if (!this._mounted) {
// We are no longer mounted, this is a no-op
return;
}
const { canJumpToTab, navigationState } = this.props;
const index = navigationState.routes.findIndex(route => route.key === key);
if (!canJumpToTab(navigationState.routes[index])) {
return;
}
if (index !== navigationState.index) {
this.props.onIndexChange(index);
}
};
render() {
const {
/* eslint-disable no-unused-vars */
navigationState,
onIndexChange,
initialLayout,
renderScene,
/* eslint-enable no-unused-vars */
renderPager,
renderTabBar,
tabBarPosition,
...rest
} = this.props;
const props = this._buildSceneRendererProps();
return (
<View collapsable={false} style={[styles.container, this.props.style]}>
{tabBarPosition === 'top' && renderTabBar(props)}
<View onLayout={this._handleLayout} style={styles.pager}>
{renderPager({
...props,
...rest,
panX: this.state.panX,
offsetX: this.state.offsetX,
children: navigationState.routes.map((route, index) => {
const isFocused = this.props.navigationState.index === index;
let scene;
if (isFocused || this.state.renderUnfocusedScenes) {
scene = this._renderScene({
...props,
route,
});
} else {
scene = <View />;
}
if (React.isValidElement(scene)) {
/* $FlowFixMe: https://github.com/facebook/flow/issues/4775 */
scene = React.cloneElement(scene, { key: route.key });
}
return scene;
}),
})}
</View>
{tabBarPosition === 'bottom' && renderTabBar(props)}
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
overflow: 'hidden',
},
pager: {
flex: 1,
},
});