/* @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, }, });