Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save robsonmobile/00e16252dfd45e4a2746e2db39b134d8 to your computer and use it in GitHub Desktop.

Select an option

Save robsonmobile/00e16252dfd45e4a2746e2db39b134d8 to your computer and use it in GitHub Desktop.

Revisions

  1. @jordanmkoncz jordanmkoncz revised this gist Feb 21, 2018. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion .react-navigation iOS 11 Navigation Bar with Large Title
    Original file line number Diff line number Diff line change
    @@ -1 +1 @@
    react-navigation iOS 11 Navigation Bar with Large Title
    #
  2. @jordanmkoncz jordanmkoncz renamed this gist Feb 21, 2018. 1 changed file with 0 additions and 0 deletions.
  3. @jordanmkoncz jordanmkoncz revised this gist Feb 21, 2018. 2 changed files with 1 addition and 1 deletion.
    1 change: 0 additions & 1 deletion !react-navigation iOS 11 Navigation Bar with Large Title
    Original file line number Diff line number Diff line change
    @@ -1 +0,0 @@
    #
    1 change: 1 addition & 0 deletions react-navigation iOS 11 Navigation Bar with Large Title
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@
    react-navigation iOS 11 Navigation Bar with Large Title
  4. @jordanmkoncz jordanmkoncz revised this gist Feb 21, 2018. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions !react-navigation iOS 11 Navigation Bar with Large Title
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@
    #
  5. @jordanmkoncz jordanmkoncz revised this gist Feb 21, 2018. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -4,6 +4,7 @@ You can check out [https://i.imgur.com/M8pv1ya.png](https://i.imgur.com/M8pv1ya.

    Notes:

    - I've used the [react-native-typography](https://github.com/hectahertz/react-native-typography) library as the source of the correct text styles for the header (title and left/right components) to match the text styles of the native iOS 11 Navigation Bar with Large Title.
    - I have intentionally made is to that the header looks the same on both iOS and Android (aside from minor differences like the fonts used on iOS vs Android). This is what I needed for my use case, but if you wanted to render headers differently on Android you'd have to implement those changes for how the header renders on Android.
    - I have provided the relevant dependencies from my `package.json` file. The versions you see in this file are the ones I'm currently using and that I've tested with. I haven't tested with newer versions of `react-native` or `react-navigation`, so it's possible that there are some changes required to support those newer versions.
    - In the cases where I've copied a component from `react-navigation` (e.g. `Header.js`) and then modified it, I've tried to leave comments explaining the changes I've made. This should make it easier to retain or implement the same changes in the situation where there is a new version of `react-navigation` which contains significant changes to the component I originally copied.
  6. @jordanmkoncz jordanmkoncz revised this gist Feb 21, 2018. 2 changed files with 5 additions and 1 deletion.
    4 changes: 3 additions & 1 deletion HeaderLargeBackButton.js
    Original file line number Diff line number Diff line change
    @@ -34,7 +34,9 @@ class HeaderLargeBackButton extends PureComponent<Props> {

    // Use the `back-icon.png` image(s) from our own project source folder. Note that these are just the back icon
    // images for iOS copied from react-navigation/src/views/assets/ and renamed so that the iOS back icon image will be
    // used on both iOS and Android.
    // used on both iOS and Android. You will need to create a new folder within your project source folder, and then
    // copy the `back-icon.png` file and also the files ending in `.ios.png`, and then rename these files so that they
    // just end in `.png`. For example, you'd copy `[email protected]`, and rename it to `[email protected]`.
    // eslint-disable-next-line global-require
    const asset = require('../assets/react-navigation/back-icon.png');

    2 changes: 2 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -6,6 +6,8 @@ Notes:

    - I have intentionally made is to that the header looks the same on both iOS and Android (aside from minor differences like the fonts used on iOS vs Android). This is what I needed for my use case, but if you wanted to render headers differently on Android you'd have to implement those changes for how the header renders on Android.
    - I have provided the relevant dependencies from my `package.json` file. The versions you see in this file are the ones I'm currently using and that I've tested with. I haven't tested with newer versions of `react-native` or `react-navigation`, so it's possible that there are some changes required to support those newer versions.
    - In the cases where I've copied a component from `react-navigation` (e.g. `Header.js`) and then modified it, I've tried to leave comments explaining the changes I've made. This should make it easier to retain or implement the same changes in the situation where there is a new version of `react-navigation` which contains significant changes to the component I originally copied.
    - You will need to copy the iOS back icon images from `react-navigation` into your own project source folder (so that the back button looks the same on Android as it does on iOS). See the comments in the `HeaderLargeBackButton.js` file for more info on this.

    Potential improvements:

  7. @jordanmkoncz jordanmkoncz created this gist Feb 21, 2018.
    35 changes: 35 additions & 0 deletions AppNavigator.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,35 @@
    import React, { Component } from 'react';
    import PropTypes from 'prop-types';
    import { addNavigationHelpers, StackNavigator } from 'react-navigation';
    import ScreenExample from './ScreenExample';

    export const NavigatorExample = StackNavigator(
    {
    'ScreenExample': { screen: ScreenExample },
    },
    {
    navigationOptions: {
    // You can customise the colours used in the header by changing these values.
    headerStyle: {
    backgroundColor: '#F7F7F7',
    borderBottomColor: 'rgba(0, 0, 0, .3)',
    },
    headerTintColor: 'rgba(0, 0, 0, .9)',
    },
    }
    );

    class AppNavigator extends Component {
    static propTypes = {
    navState: PropTypes.object.isRequired,
    dispatch: PropTypes.func.isRequired,
    };

    render() {
    const { dispatch, navState } = this.props;

    return <NavigatorExample navigation={addNavigationHelpers({ dispatch, state: navState })} />;
    }
    }

    export default AppNavigator;
    51 changes: 51 additions & 0 deletions HeaderButton.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,51 @@
    import React, { Component } from 'react';
    import { ActivityIndicator, StyleSheet, TouchableOpacity, View } from 'react-native';
    import PropTypes from 'prop-types';

    let styles;

    class HeaderButton extends Component {
    static propTypes = {
    children: PropTypes.node,
    loading: PropTypes.bool,
    disabled: PropTypes.bool,
    handlePress: PropTypes.func.isRequired,
    tintColor: PropTypes.any,
    };

    static defaultProps = {
    children: null,
    loading: false,
    disabled: false,
    tintColor: '#037aff',
    };

    render() {
    const { children, loading, disabled, handlePress, tintColor } = this.props;

    return (
    <TouchableOpacity onPress={handlePress} disabled={disabled || loading}>
    <View style={styles.container}>
    {!loading && children}

    {loading && <ActivityIndicator color={tintColor} style={{ backgroundColor: 'transparent' }} />}
    </View>
    </TouchableOpacity>
    );
    }
    }

    styles = StyleSheet.create({
    container: {
    alignItems: 'center',
    justifyContent: 'center',

    // Match left spacing of HeaderLargeBackButton.
    paddingHorizontal: 10,

    // Match height of HeaderLargeBackButton.
    height: 21 + 12 + 12,
    },
    });

    export default HeaderButton;
    36 changes: 36 additions & 0 deletions HeaderButtonExample.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,36 @@
    import React, { Component } from 'react';
    import { StyleSheet, Text } from 'react-native';
    import { iOSUIKit } from 'react-native-typography';
    import HeaderButton from './HeaderButton';

    let styles;

    class HeaderButtonExample extends Component {
    constructor() {
    super();

    this.handlePress = this.handlePress.bind(this);
    }

    handlePress() {
    console.log('Pressed header button.');
    }

    render() {
    return (
    <HeaderButton handlePress={this.handlePress}>
    <Text style={[styles.titleText, { color: this.props.tintColor }]}>Header Button</RegularText>
    </HeaderButton>
    );
    }
    }

    styles = StyleSheet.create({
    titleText: {
    // The `iOSUIKit.bodyObject` styles are required to make the header button text match the iOS 11 Navigation Bar with
    // Large Title styles.
    ...iOSUIKit.bodyObject,
    },
    });

    export default HeaderButtonExample;
    329 changes: 329 additions & 0 deletions HeaderLarge.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,329 @@
    /* @flow */

    import * as React from 'react';
    import { Animated, StyleSheet, View, ViewPropTypes } from 'react-native';
    import { HeaderTitle, SafeAreaView } from 'react-navigation';
    import HeaderStyleInterpolator from 'react-navigation/src/views/Header/HeaderStyleInterpolator';
    import withOrientation from 'react-navigation/src/views/withOrientation';
    import type { NavigationScene, NavigationStyleInterpolator, HeaderProps } from 'react-navigation/src/TypeDefinition';
    import { iOSUIKit } from 'react-native-typography';
    import HeaderLargeBackButton from './HeaderLargeBackButton';

    type SceneProps = {
    scene: NavigationScene,
    position: Animated.Value,
    progress: Animated.Value,
    style?: ViewPropTypes.style,
    };

    type SubViewRenderer<T> = (props: SceneProps) => ?React.Node;

    type SubViewName = 'left' | 'title' | 'right';

    type Props = HeaderProps & { isLandscape: boolean };

    let styles;

    /**
    * Copied from react-navigation/src/views/Header/Header.
    *
    * Modified to match the styles of an iOS 11 Navigation Bar with Large Title.
    */
    class HeaderLarge extends React.PureComponent<Props> {
    _getHeaderTitleString(scene: NavigationScene): ?string {
    const sceneOptions = this.props.getScreenDetails(scene).options;
    if (typeof sceneOptions.headerTitle === 'string') {
    return sceneOptions.headerTitle;
    }
    return sceneOptions.title;
    }

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

    _getBackButtonTitleString(scene: NavigationScene): ?string {
    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);
    }

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

    _renderTitleComponent = (props: SceneProps): ?React.Node => {
    /**
    * Modified to ignore truncation-related functionality (onLayout width calculation). Also modified to pass down
    * a `tintColor` prop when `options.headerTitle` is a valid element. Also modified so that `RenderedHeaderTitle`
    * has styles applied to make it match the iOS 11 Navigation Bar with Large Title styles, and has the `color`
    * style specified last to ensure that the `options.headerTintColor` value is applied.
    */
    // $FlowFixMe
    const { options } = this.props.getScreenDetails(props.scene);
    const { headerTitle } = options;

    if (React.isValidElement(options.headerTitle)) {
    return React.cloneElement(options.headerTitle, { tintColor: options.headerTintColor });
    }

    const titleString = this._getHeaderTitleString(props.scene);
    const tintColor = options.headerTintColor;
    const allowFontScaling = options.headerTitleAllowFontScaling;
    const RenderedHeaderTitle = headerTitle && typeof headerTitle !== 'string' ? headerTitle : HeaderTitle;

    return (
    <RenderedHeaderTitle
    allowFontScaling={allowFontScaling == null ? true : allowFontScaling}
    style={[styles.titleText, options.headerTitleStyle, !!tintColor && { color: tintColor }]}
    >
    {titleString}
    </RenderedHeaderTitle>
    );
    };

    _renderLeftComponent = (props: SceneProps): ?React.Node => {
    /**
    * Modified to ignore truncation-related functionality (truncated title, limited width), and to use our custom
    * HeaderLargeBackButton when a headerLeft is not specified. Also modified to pass down a `tintColor` prop when
    * `options.headerLeft` is a valid element.
    */
    // $FlowFixMe
    const { options } = this.props.getScreenDetails(props.scene);

    if (React.isValidElement(options.headerLeft)) {
    return React.cloneElement(options.headerLeft, { tintColor: options.headerTintColor });
    }

    if (options.headerLeft === null) {
    return options.headerLeft;
    }

    if (props.scene.index === 0) {
    return null;
    }

    const backButtonTitle = this._getBackButtonTitleString(props.scene);

    const RenderedLeftComponent = options.headerLeft || HeaderLargeBackButton;

    return (
    <RenderedLeftComponent
    onPress={this._navigateBack}
    tintColor={options.headerTintColor}
    title={backButtonTitle}
    titleStyle={options.headerBackTitleStyle}
    />
    );
    };

    _renderRightComponent = (props: SceneProps): ?React.Node => {
    /**
    * Modified to pass down a `tintColor` prop when `options.headerRight` is a valid element.
    */
    const { options } = this.props.getScreenDetails(props.scene);

    if (React.isValidElement(options.headerRight)) {
    return React.cloneElement(options.headerRight, { tintColor: options.headerTintColor });
    }

    return options.headerRight || null;
    };

    _renderLeft(props: SceneProps): ?React.Node {
    /**
    * Modified to use `HeaderStyleInterpolator.forRight` for consistency between headerLeft and headerRight
    * transition styles.
    */
    return this._renderSubView(props, 'left', this._renderLeftComponent, HeaderStyleInterpolator.forRight);
    }

    _renderTitle(props: SceneProps, options: *): ?React.Node {
    /**
    * Modified to ignore styles relating to absolute positioning of title.
    */
    return this._renderSubView(props, 'title', this._renderTitleComponent, HeaderStyleInterpolator.forCenter);
    }

    _renderRight(props: SceneProps): ?React.Node {
    return this._renderSubView(props, 'right', this._renderRightComponent, HeaderStyleInterpolator.forRight);
    }

    _renderSubView<T>(
    props: SceneProps,
    name: SubViewName,
    renderer: SubViewRenderer<T>,
    styleInterpolator: NavigationStyleInterpolator
    ): ?React.Node {
    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: SceneProps): React.Node {
    /**
    * Modified to change the header layout by wrapping header actions in a styled View, which is separate from the
    * title.
    */
    let left = this._renderLeft(props);
    const right = this._renderRight(props);

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

    // If we have a `headerRight` but don't have a `headerLeft`, we render an empty `View` as our `headerLeft` so that
    // the `headerRight` will still appear on the right side of the header.
    if (!left && !!right) {
    left = <View />;
    }

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

    {title}
    </View>
    );
    }

    render() {
    /**
    * Modified to increase the app bar height to match the height of an iOS 11 Navigation Bar with Large Title.
    */
    let appBar;

    if (this.props.mode === 'float') {
    const scenesProps: Array<SceneProps> = this.props.scenes.map((scene: NavigationScene) => ({
    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;

    // Match the height of an iOS 11 Navigation Bar with Large Title.
    const appBarHeight = 96;

    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>
    );
    }
    }

    /**
    * Modified to change the header layout, and to make the bottom border stronger to provide more separation between
    * header and content.
    */
    styles = StyleSheet.create({
    container: {
    backgroundColor: '#F7F7F7',
    borderBottomWidth: 1,
    borderBottomColor: 'rgba(0, 0, 0, .3)',
    },
    appBar: {
    flex: 1,
    },
    header: {
    flex: 1,
    flexDirection: 'column',
    },
    headerLeftRightContainer: {
    marginHorizontal: 10,
    flexDirection: 'row',
    justifyContent: 'space-between',

    // Match height of HeaderLargeBackButton. This is needed so that when we don't have a `headerLeft` or `headerRight`
    // we maintain a consistent header layout (without setting a height here, the `title` is not positioned correctly).
    // There is probably a better solution for this.
    height: 21 + 12 + 12,
    },
    item: {
    justifyContent: 'center',
    alignItems: 'flex-start',
    backgroundColor: 'transparent',
    },
    title: {
    marginHorizontal: 20,
    marginBottom: 10,
    },
    titleText: {
    ...iOSUIKit.largeTitleEmphasizedObject,
    marginHorizontal: 0,
    textAlign: 'left',
    },
    left: {
    flex: 1,
    paddingRight: 20,
    },
    right: {},
    });

    export default withOrientation(HeaderLarge);
    93 changes: 93 additions & 0 deletions HeaderLargeBackButton.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,93 @@
    /* @flow */

    import React, { PureComponent } from 'react';
    import { I18nManager, Image, Text, TouchableOpacity, View, StyleSheet } from 'react-native';
    import type { TextStyleProp } from 'react-navigation/src/TypeDefinition';
    import { iOSUIKit } from 'react-native-typography';

    type Props = {
    onPress?: () => void,
    title?: ?string,
    titleStyle?: ?TextStyleProp,
    tintColor?: ?string,
    };

    let styles;

    /**
    * Copied from react-navigation/src/views/Header/HeaderBackButton.
    *
    * The component has been modified so that it is displayed the same on both iOS and Android. It has been modified to
    * fill the entire container width, instead of shrinking to fit a small container width based on being displayed
    * alongside the header title. The title styles have been modified to use the appropriate styles from
    * `react-native-typography`.
    */
    class HeaderLargeBackButton extends PureComponent<Props> {
    static defaultProps = {
    tintColor: '#037aff',
    };

    render() {
    const { onPress, title, titleStyle, tintColor } = this.props;

    const backButtonTitle = title;

    // Use the `back-icon.png` image(s) from our own project source folder. Note that these are just the back icon
    // images for iOS copied from react-navigation/src/views/assets/ and renamed so that the iOS back icon image will be
    // used on both iOS and Android.
    // eslint-disable-next-line global-require
    const asset = require('../assets/react-navigation/back-icon.png');

    return (
    <TouchableOpacity
    accessibilityComponentType="button"
    accessibilityLabel={backButtonTitle}
    accessibilityTraits="button"
    testID="header-back"
    onPress={onPress}
    style={styles.container}
    >
    <View style={styles.contentContainer}>
    <Image style={[styles.icon, !!title && styles.iconWithTitle, !!tintColor && { tintColor }]} source={asset} />

    {typeof backButtonTitle === 'string' && (
    <Text style={[styles.title, titleStyle, !!tintColor && { color: tintColor }]} numberOfLines={1}>
    {backButtonTitle}
    </Text>
    )}
    </View>
    </TouchableOpacity>
    );
    }
    }

    styles = StyleSheet.create({
    container: {
    alignSelf: 'flex-start',
    alignItems: 'center',
    backgroundColor: 'transparent',
    },
    contentContainer: {
    alignItems: 'center',
    flexDirection: 'row',
    backgroundColor: 'transparent',
    },
    title: {
    ...iOSUIKit.bodyObject,
    paddingRight: 20,
    },
    icon: {
    height: 21,
    width: 13,
    marginLeft: 10,
    marginRight: 22,
    marginVertical: 12,
    resizeMode: 'contain',
    transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
    },
    iconWithTitle: {
    marginRight: 5,
    },
    });

    export default HeaderLargeBackButton;
    12 changes: 12 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,12 @@
    This gist provides an example of how to implement the iOS 11 Navigation Bar with Large Title for [react-navigation](https://github.com/react-navigation/react-navigation/). For more information on this navigation bar style, see [https://medium.com/@PavelGnatyuk/large-title-and-search-in-ios-11-514d5e020cee](https://medium.com/@PavelGnatyuk/large-title-and-search-in-ios-11-514d5e020cee) and [https://github.com/react-navigation/react-navigation/issues/2749](https://github.com/react-navigation/react-navigation/issues/2749).

    You can check out [https://i.imgur.com/M8pv1ya.png](https://i.imgur.com/M8pv1ya.png) to see an example of how this looks in an app where I'm using all of the components provided in this gist.

    Notes:

    - I have intentionally made is to that the header looks the same on both iOS and Android (aside from minor differences like the fonts used on iOS vs Android). This is what I needed for my use case, but if you wanted to render headers differently on Android you'd have to implement those changes for how the header renders on Android.
    - I have provided the relevant dependencies from my `package.json` file. The versions you see in this file are the ones I'm currently using and that I've tested with. I haven't tested with newer versions of `react-native` or `react-navigation`, so it's possible that there are some changes required to support those newer versions.

    Potential improvements:

    - Make the header automatically shrink/expand based on the user scrolling up/down on the screen. This is how the native iOS 11 Navigation Bar with Large Title behaves, but I have not tried to implement this same behaviour yet.
    22 changes: 22 additions & 0 deletions ScreenExample.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,22 @@
    import React, { Component } from 'react';
    import { Text } from 'react-native';
    import HeaderLarge from './HeaderLarge';
    import HeaderButtonExample from './HeaderButtonExample';

    class ScreenExample extends Component {
    static navigationOptions = {
    title: 'Screen Example',
    headerRight: <HeaderButtonExample />,

    // Specify that this screen should use our custom HeaderLarge component for the `header`.
    header: headerProps => <HeaderLarge {...headerProps} />,
    };

    render() {
    return (
    <Text>Hello World</Text>
    );
    }
    }

    export default ScreenExample;
    14 changes: 14 additions & 0 deletions package.json
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,14 @@
    {
    "name": "react-navigation iOS 11 Navigation Bar with Large Title Example",
    "version": "1.0.0",
    "private": true,
    "devDependencies": {},
    "scripts": {},
    "dependencies": {
    "prop-types": "^15.6.0",
    "react": "16.0.0",
    "react-native": "0.50.3",
    "react-native-typography": "1.0.3",
    "react-navigation": "1.0.0-beta.21"
    }
    }