Search code examples
react-nativealignment

How to set the alignment for the header when using NavigationCardStack?


I've followed the example here and then tried to add a header. I've managed to get the title to show the key and the back navigation to show the key for the previous card in the stack.

The problem I have is that I cannot figure out how to align the icon and text for the back action. If I add flow: 1 to the style then it just disappears. If I add any align option then nothing changes. Funnilly enough, changing flexDirection works, as does changing the margins for left and right.

Right now, the icon and text appear to be aligned along the top edge.

I'm not sure if it's a text issue or an issue with the size of the container. Note that I've tried textAlignVertical: 'center' on the text to no avail.

How do I do this?

Example of alignment problem in header

Does anyone know how to solve?

The code in question:

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 *
 * Refer: http://facebook.github.io/react-native/docs/navigation.html
 */

import React, { Component } from 'react';

import { 
  AppRegistry,
  NavigationExperimental, 
  PixelRatio, 
  ScrollView, 
  StyleSheet, 
  View,
  Text, 
  TouchableHighlight 
} from 'react-native';

import Icon from 'react-native-vector-icons/Ionicons'

const {
  CardStack: NavigationCardStack,
  StateUtils: NavigationStateUtils,
  Header: NavigationHeader,
} = NavigationExperimental;

export default class BleedingEdgeApplication extends Component {
  constructor(props, context) {
    super(props, context);

    this.state = {
      navigationState: {
        index: 0,
        routes: [{key: 'Home'}]
      }
    }

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


  _onNavigationChange(type) {
    // extract the navigation state from the current state
    let {navigationState} = this.state;

    switch(type) {
      case 'push':
        // push a new route, which in our case is an object with a key value
        const route = {key: ':' + Date.now()};

        // use the push reducer provided by NavigationStateUtils
        navigationState = NavigationStateUtils.push(navigationState, route);
        break;

      case 'pop':
        // Pop the current route using the pop reducer
        navigationState = NavigationStateUtils.pop(navigationState);
        break;
    }

    if (this.state.navigationState !== navigationState) {
      this.setState({navigationState});
    }
  }

  render() {
    return (
      <MyVerySimpleNavigator
        navigationState={this.state.navigationState}
        onNavigationChange={this._onNavigationChange}
        onExit={this._exit}
        />
    );
  }
}

class TappableRow extends Component {
  render () {
    return (
      <TouchableHighlight
        style={styles.row}
        underlayColor="#D0D0D0"
        onPress={this.props.onPress}>
        <Text style={styles.buttonText}>
          {this.props.text}
        </Text>
      </TouchableHighlight>
    );
  }
}

class MyVeryComplexScene extends Component {
  render() {
    return (
      <ScrollView style={styles.scrollView}>
        <Text style={styles.row}>
          Route: {this.props.route.key}
        </Text>
        <TappableRow 
          text="Tap me to load the next scene"
          onPress={this.props.onPushRoute}
          />
        <TappableRow 
          text="Tap me to go back"
          onPress={this.props.onPopRoute}
          />
      </ScrollView>
    );
  }
}

class MyVerySimpleNavigator extends Component {
  constructor(props, context) {
    super(props, context);

    this._onPushRoute = this.props.onNavigationChange.bind(null, 'push');
    this._onPopRoute = this.props.onNavigationChange.bind(null, 'pop');

    this._renderScene = this._renderScene.bind(this);
    this._renderHeader = this._renderHeader.bind(this);
    this._renderLeftHeader = this._renderLeftHeader.bind(this);
  }

  render() {
    return (
      <NavigationCardStack
        onNavigateBack={this._onPopRoute}
        navigationState={this.props.navigationState}
        renderScene={this._renderScene}
        renderHeader={this._renderHeader}
        style={styles.navigator}
        />
    );
  }

  _renderHeader(sceneProps) {
    return (
      <NavigationHeader
        {...sceneProps}
        renderTitleComponent={() => (
          <NavigationHeader.Title>
            {sceneProps.scene.route.key}
          </NavigationHeader.Title>
        )}
        renderLeftComponent={this._renderLeftHeader}
        />
    );
  }

  _renderLeftHeader(sceneProps) {
    if(sceneProps.scene.index > 0) {
      return (
        <TouchableHighlight onPress={this._onPopRoute}>
          <View style={styles.backView}>
            <Icon style={styles.navBarIcon} name='ios-arrow-back' size={27} />
            <Text style={styles.backText}>{sceneProps.scenes[sceneProps.scene.index - 1].route.key}</Text>
          </View>
        </TouchableHighlight>
      );
    }

    return (
      <TouchableHighlight>
        <View style={styles.backView}>
          <Icon style={styles.navBarIcon} name='ios-menu' size={27} />
        </View>
      </TouchableHighlight>
    );

  }

  _renderScene(sceneProps) {
    return (
      <MyVeryComplexScene
        route={sceneProps.scene.route}
        onPushRoute={this._onPushRoute}
        onPopRoute={this._onPopRoute}
        onExit={this.props.onExit}
        />
    );
  }
}

const styles = StyleSheet.create({
  navigator: {
    flex: 1,
  },
  scrollView: {
    marginTop: 64
  },
  row: {
    padding: 15,
    backgroundColor: 'white',
    borderBottomWidth: 1 / PixelRatio.get(),
    borderBottomColor: '#CDCDCD',
  },
  backView: {
//    flex: 2,
    height: 50,
    flexDirection: 'row',
    justifyContent: 'center',
    alignItems: 'flex-start',
  },
  backText: {
    fontSize: 12,
    textAlign: 'left',
  },
  backChevron: {

  },
  navBarIcon: {
    color: '#1d1d1d',
    marginLeft: 8,
    marginRight: 8,
    justifyContent: 'center',
  },
  rowText: {
    fontSize: 17,
  },
  buttonText: {
    fontSize: 17,
    fontWeight: '500',
  },
});

AppRegistry.registerComponent('BleedingEdgeApplication', () => BleedingEdgeApplication);

I should note that I am using React Native Vector Icons. This requires copying the Ionicon font from the font's directory into the ios project and rebuilding from within xcode.


Solution

  • The problem was not applying styling to <TouchableHighlight> with a flex: 1. This makes the component take up the entire space and let me use flex and alignItems on the <View> that held the button and text. This made the whole styling a lot neater.

    The biggest confusion was understanding that the various header components within <NavigationHeader> are all independent, hence the seperate render option for title, left and right.

    The solution looks like: enter image description here

    The code, as working (with colored backgrounds to show the components):

    /**
     * Sample React Native App
     * https://github.com/facebook/react-native
     * @flow
     *
     * Refer: http://facebook.github.io/react-native/docs/navigation.html
     */
    
    import React, { Component } from 'react';
    
    import { 
      AppRegistry,
      NavigationExperimental, 
      PixelRatio, 
      ScrollView, 
      StyleSheet, 
      View,
      Text, 
      TouchableHighlight 
    } from 'react-native';
    
    import Icon from 'react-native-vector-icons/Ionicons'
    
    const {
      CardStack: NavigationCardStack,
      StateUtils: NavigationStateUtils,
      Header: NavigationHeader,
    } = NavigationExperimental;
    
    export default class BleedingEdgeApplication extends Component {
      constructor(props, context) {
        super(props, context);
    
        this.state = {
          navigationState: {
            index: 0,
            routes: [{key: 'Home'}]
          }
        }
    
        this._onNavigationChange = this._onNavigationChange.bind(this);
      }
    
    
      _onNavigationChange(type) {
        // extract the navigation state from the current state
        let {navigationState} = this.state;
    
        switch(type) {
          case 'push':
            // push a new route, which in our case is an object with a key value
            const route = {key: ':' + Date.now()};
    
            // use the push reducer provided by NavigationStateUtils
            navigationState = NavigationStateUtils.push(navigationState, route);
            break;
    
          case 'pop':
            // Pop the current route using the pop reducer
            navigationState = NavigationStateUtils.pop(navigationState);
            break;
        }
    
        if (this.state.navigationState !== navigationState) {
          this.setState({navigationState});
        }
      }
    
      render() {
        return (
          <MyVerySimpleNavigator
            navigationState={this.state.navigationState}
            onNavigationChange={this._onNavigationChange}
            onExit={this._exit}
            />
        );
      }
    }
    
    class TappableRow extends Component {
      render () {
        return (
          <TouchableHighlight
            style={styles.row}
            underlayColor="#D0D0D0"
            onPress={this.props.onPress}>
            <Text style={styles.buttonText}>
              {this.props.text}
            </Text>
          </TouchableHighlight>
        );
      }
    }
    
    class MyVeryComplexScene extends Component {
      render() {
        return (
          <ScrollView style={styles.scrollView}>
            <Text style={styles.row}>
              Route: {this.props.route.key}
            </Text>
            <TappableRow 
              text="Tap me to load the next scene"
              onPress={this.props.onPushRoute}
              />
            <TappableRow 
              text="Tap me to go back"
              onPress={this.props.onPopRoute}
              />
          </ScrollView>
        );
      }
    }
    
    class MyVerySimpleNavigator extends Component {
      constructor(props, context) {
        super(props, context);
    
        this._onPushRoute = this.props.onNavigationChange.bind(null, 'push');
        this._onPopRoute = this.props.onNavigationChange.bind(null, 'pop');
    
        this._onOpenSideNav = this._onOpenSideNav.bind(this);
    
        this._renderScene = this._renderScene.bind(this);
        this._renderHeader = this._renderHeader.bind(this);
        this._renderLeftHeader = this._renderLeftHeader.bind(this);
      }
    
      render() {
        return (
          <NavigationCardStack
            onNavigateBack={this._onPopRoute}
            navigationState={this.props.navigationState}
            renderScene={this._renderScene}
            renderHeader={this._renderHeader}
            style={styles.navigator}
            />
        );
      }
    
      _renderHeader(sceneProps) {
        return (
          <NavigationHeader
            {...sceneProps}
            style={styles.navHeader}
            renderTitleComponent={() => (
              <NavigationHeader.Title style={styles.navheadertitle}>
                <Text style={styles.headerTitle}>
                  {sceneProps.scene.route.key}
                </Text>
    
              </NavigationHeader.Title>
            )}
            renderLeftComponent={this._renderLeftHeader}
            />
        );
      }
    
      _renderLeftHeader(sceneProps) {
        if(sceneProps.scene.index > 0) {
          return (
            <TouchableHighlight onPress={this._onPopRoute} style={styles.headerBackTouchableHighlight}>
              <View style={styles.headerBackView}>
                <Icon style={styles.headerIcon} name='ios-arrow-back' size={27} />
                <Text style={styles.headerBackText}>{sceneProps.scenes[sceneProps.scene.index - 1].route.key}</Text>
              </View>
            </TouchableHighlight>
          );
        }
    
        return (
          <TouchableHighlight onPress={this._onOpenSideNav} style={styles.headerBackTouchableHighlight}>
            <View style={styles.headerBackView}>
              <Icon style={styles.headerIcon} name='ios-menu' size={27} />
            </View>
          </TouchableHighlight>
        );
    
      }
    
      _renderScene(sceneProps) {
        return (
          <MyVeryComplexScene
            route={sceneProps.scene.route}
            onPushRoute={this._onPushRoute}
            onPopRoute={this._onPopRoute}
            onExit={this.props.onExit}
            />
        );
      }
      _onOpenSideNav() {
        return null;
      }
    
    }
    
    const styles = StyleSheet.create({
      navigator: {
    
      },
      navHeader: {
        backgroundColor: 'red',
      },
      navheadertitle: {
        backgroundColor: '#00ff0099',
        flex: 3
      },
      headerTitle: {
        color: '#ffffff',
        backgroundColor: 'blue',
      },
      headerBackTouchableHighlight: {
        backgroundColor: 'purple',
        flex: 1,
      },
      headerBackView: {
        flex: 1,
        flexDirection: 'row',
        alignItems: 'center',
        backgroundColor: 'darkgreen',
      },
      headerBackText: {
        fontSize: 14,
        textAlign: 'left',
        color: '#ffffff',
      },
      headerIcon: {
        color: '#ffffff',
        marginLeft: 8,
        marginRight: 8,
        justifyContent: 'center',
      },
      scrollView: {
        marginTop: 64
      },
      row: {
        padding: 15,
        backgroundColor: 'white',
        borderBottomWidth: 1 / PixelRatio.get(),
        borderBottomColor: '#CDCDCD',
      },
      rowText: {
        fontSize: 17,
      },
      buttonText: {
        fontSize: 17,
        fontWeight: '500',
      },
    });
    
    AppRegistry.registerComponent('BleedingEdgeApplication', () => BleedingEdgeApplication);