Header.js 8.46 KB
'no babel-plugin-flow-react-proptypes';

import * as React from 'react';

import { Animated, Dimensions, Platform, StyleSheet, View, ViewPropTypes } from 'react-native';

import HeaderTitle from './HeaderTitle';
import HeaderBackButton from './HeaderBackButton';
import HeaderStyleInterpolator from './HeaderStyleInterpolator';
import SafeAreaView from '../SafeAreaView';
import withOrientation from '../withOrientation';

const APPBAR_HEIGHT = Platform.OS === 'ios' ? 44 : 56;
const STATUSBAR_HEIGHT = Platform.OS === 'ios' ? 20 : 0;
const TITLE_OFFSET = Platform.OS === 'ios' ? 70 : 56;

class Header extends React.PureComponent {
  static defaultProps = {
    leftInterpolator: HeaderStyleInterpolator.forLeft,
    titleInterpolator: HeaderStyleInterpolator.forCenter,
    rightInterpolator: HeaderStyleInterpolator.forRight
  };

  static get HEIGHT() {
    console.warn('Header.HEIGHT is deprecated and will be removed before react-navigation comes out of beta.');
    return APPBAR_HEIGHT + STATUSBAR_HEIGHT;
  }

  state = {
    widths: {}
  };

  _getHeaderTitleString(scene) {
    const sceneOptions = this.props.getScreenDetails(scene).options;
    if (typeof sceneOptions.headerTitle === 'string') {
      return sceneOptions.headerTitle;
    }
    return sceneOptions.title;
  }

  _getLastScene(scene) {
    return this.props.scenes.find(s => s.index === scene.index - 1);
  }

  _getBackButtonTitleString(scene) {
    const lastScene = this._getLastScene(scene);
    if (!lastScene) {
      return null;
    }
    const { headerBackTitle } = this.props.getScreenDetails(lastScene).options;
    if (headerBackTitle || headerBackTitle === null) {
      return headerBackTitle;
    }
    return this._getHeaderTitleString(lastScene);
  }

  _getTruncatedBackButtonTitle(scene) {
    const lastScene = this._getLastScene(scene);
    if (!lastScene) {
      return null;
    }
    return this.props.getScreenDetails(lastScene).options.headerTruncatedBackTitle;
  }

  _navigateBack = () => {
    this.props.navigation.goBack(null);
  };

  _renderTitleComponent = props => {
    // $FlowFixMe
    const details = this.props.getScreenDetails(props.scene);
    const headerTitle = details.options.headerTitle;
    if (React.isValidElement(headerTitle)) {
      return headerTitle;
    }
    const titleString = this._getHeaderTitleString(props.scene);

    const titleStyle = details.options.headerTitleStyle;
    const color = details.options.headerTintColor;
    const allowFontScaling = details.options.headerTitleAllowFontScaling;

    // On iOS, width of left/right components depends on the calculated
    // size of the title.
    const onLayoutIOS = Platform.OS === 'ios' ? e => {
      this.setState({
        widths: {
          ...this.state.widths,
          [props.scene.key]: e.nativeEvent.layout.width
        }
      });
    } : undefined;

    const RenderedHeaderTitle = headerTitle && typeof headerTitle !== 'string' ? headerTitle : HeaderTitle;
    return <RenderedHeaderTitle onLayout={onLayoutIOS} allowFontScaling={allowFontScaling == null ? true : allowFontScaling} style={[color ? { color } : null, titleStyle]}>
        {titleString}
      </RenderedHeaderTitle>;
  };

  _renderLeftComponent = props => {
    // $FlowFixMe
    const { options } = this.props.getScreenDetails(props.scene);
    if (React.isValidElement(options.headerLeft) || options.headerLeft === null) {
      return options.headerLeft;
    }
    if (props.scene.index === 0) {
      return null;
    }
    const backButtonTitle = this._getBackButtonTitleString(props.scene);
    const truncatedBackButtonTitle = this._getTruncatedBackButtonTitle(props.scene);
    const width = this.state.widths[props.scene.key] ? (this.props.layout.initWidth - this.state.widths[props.scene.key]) / 2 : undefined;
    const RenderedLeftComponent = options.headerLeft || HeaderBackButton;
    return <RenderedLeftComponent onPress={this._navigateBack} pressColorAndroid={options.headerPressColorAndroid} tintColor={options.headerTintColor} title={backButtonTitle} truncatedTitle={truncatedBackButtonTitle} titleStyle={options.headerBackTitleStyle} width={width} />;
  };

  _renderRightComponent = props => {
    const details = this.props.getScreenDetails(props.scene);
    const { headerRight } = details.options;
    return headerRight || null;
  };

  _renderLeft(props) {
    return this._renderSubView(props, 'left', this._renderLeftComponent, this.props.leftInterpolator);
  }

  _renderTitle(props, options) {
    const style = {};

    if (Platform.OS === 'android') {
      if (!options.hasLeftComponent) {
        style.left = 0;
      }
      if (!options.hasRightComponent) {
        style.right = 0;
      }
    } else if (Platform.OS === 'ios' && !options.hasLeftComponent && !options.hasRightComponent) {
      style.left = 0;
      style.right = 0;
    }

    return this._renderSubView({ ...props, style }, 'title', this._renderTitleComponent, this.props.titleInterpolator);
  }

  _renderRight(props) {
    return this._renderSubView(props, 'right', this._renderRightComponent, this.props.rightInterpolator);
  }

  _renderSubView(props, name, renderer, styleInterpolator) {
    const { scene } = props;
    const { index, isStale, key } = scene;

    const offset = this.props.navigation.state.index - index;

    if (Math.abs(offset) > 2) {
      // Scene is far away from the active scene. Hides it to avoid unnecessary
      // rendering.
      return null;
    }

    const subView = renderer(props);

    if (subView == null) {
      return null;
    }

    const pointerEvents = offset !== 0 || isStale ? 'none' : 'box-none';

    return <Animated.View pointerEvents={pointerEvents} key={`${name}_${key}`} style={[styles.item, styles[name], props.style, styleInterpolator({
      // todo: determine if we really need to splat all this.props
      ...this.props,
      ...props
    })]}>
        {subView}
      </Animated.View>;
  }

  _renderHeader(props) {
    const left = this._renderLeft(props);
    const right = this._renderRight(props);
    const title = this._renderTitle(props, {
      hasLeftComponent: !!left,
      hasRightComponent: !!right
    });

    return <View style={[StyleSheet.absoluteFill, styles.header]} key={`scene_${props.scene.key}`}>
        {title}
        {left}
        {right}
      </View>;
  }

  render() {
    let appBar;

    if (this.props.mode === 'float') {
      const scenesProps = this.props.scenes.map(scene => ({
        position: this.props.position,
        progress: this.props.progress,
        scene
      }));
      appBar = scenesProps.map(this._renderHeader, this);
    } else {
      appBar = this._renderHeader({
        position: new Animated.Value(this.props.scene.index),
        progress: new Animated.Value(0),
        scene: this.props.scene
      });
    }

    // eslint-disable-next-line no-unused-vars
    const {
      scenes,
      scene,
      position,
      screenProps,
      progress,
      isLandscape,
      ...rest
    } = this.props;

    const { options } = this.props.getScreenDetails(scene);
    const { headerStyle } = options;
    const appBarHeight = Platform.OS === 'ios' ? isLandscape ? 32 : 44 : 56;
    const containerStyles = [styles.container, {
      height: appBarHeight
    }, headerStyle];

    return <Animated.View {...rest}>
        <SafeAreaView style={containerStyles} forceInset={{ top: 'always', bottom: 'never' }}>
          <View style={styles.appBar}>{appBar}</View>
        </SafeAreaView>
      </Animated.View>;
  }
}

let platformContainerStyles;
if (Platform.OS === 'ios') {
  platformContainerStyles = {
    borderBottomWidth: StyleSheet.hairlineWidth,
    borderBottomColor: 'rgba(0, 0, 0, .3)'
  };
} else {
  platformContainerStyles = {
    shadowColor: 'black',
    shadowOpacity: 0.1,
    shadowRadius: StyleSheet.hairlineWidth,
    shadowOffset: {
      height: StyleSheet.hairlineWidth
    },
    elevation: 4
  };
}

const styles = StyleSheet.create({
  container: {
    backgroundColor: Platform.OS === 'ios' ? '#F7F7F7' : '#FFF',
    ...platformContainerStyles
  },
  appBar: {
    flex: 1
  },
  header: {
    flexDirection: 'row'
  },
  item: {
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'transparent'
  },
  title: {
    bottom: 0,
    left: TITLE_OFFSET,
    right: TITLE_OFFSET,
    top: 0,
    position: 'absolute',
    alignItems: Platform.OS === 'ios' ? 'center' : 'flex-start'
  },
  left: {
    left: 0,
    bottom: 0,
    top: 0,
    position: 'absolute'
  },
  right: {
    right: 0,
    bottom: 0,
    top: 0,
    position: 'absolute'
  }
});

export default withOrientation(Header);