React-native-router-flux: Scene looks bad in landscape mode

Created on 28 Feb 2020  路  8Comments  路  Source: aksonov/react-native-router-flux

Version

Tell us which versions you are using:

  • react-native-router-flux v4.2.0
  • react v16.9.0
  • react-native v0.61.5

Expected behaviour

My screen is locked in portrait mode (using the react-native-orientation-locker library). Then, I press the 'View Scores' button, as you can see below (it just executes the () => Actions.viewGroupScores() function):
Captura虇 de ecran din 2020-02-28 la 15 15 23

Then, I am redirected to 'viewGroupScores' scene, which just set the orientation to landscape mode inside componentDidMount:

  componentDidMount() {
    Orientation.lockToLandscape();
  }

I expect to see the 'viewGroupScores' scene in landscape mode.

Actual behaviour

ONLY ON IOS.
The 'viewGroupScores' scene is shown in landscape mode, but it contains some strange white box at the right side, like you can see in the following screenshots (I tried to modify header in different ways, I thought that it is a problem with my stylesheets):

Captura虇 de ecran din 2020-02-28 la 15 26 05

Captura虇 de ecran din 2020-02-28 la 16 06 06

The 'viewGroupScores' scene looks okay in portrait mode, it has this problem only in landscape.

Steps to reproduce

For non-obvious bugs, please fork this component, modify Example project to reproduce your issue and include link here.
1.
2.
3.

Reproducible Demo


Please provide a minimized reproducible demonstration of the problem you're reporting.

Issues that come with minimal repro's are resolved much more quickly than issues where a maintainer has to reproduce themselves.

package.json file:

{
  "name": "WHCC",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "android": "react-native run-android",
    "ios": "react-native run-ios",
    "start": "react-native start",
    "test": "jest",
    "lint": "eslint ."
  },
  "dependencies": {
    "@react-navigation/native": "^5.0.0",
    "lodash": "^4.17.15",
    "prop-types": "^15.7.2",
    "react": "16.9.0",
    "react-native": "0.61.5",
    "react-native-elements": "^1.2.7",
    "react-native-gesture-handler": "^1.5.6",
    "react-native-google-sheet": "0.0.5",
    "react-native-modal-dropdown": "^0.7.0",
    "react-native-modal-selector": "^1.1.4",
    "react-native-orientation-locker": "^1.1.8",
    "react-native-reanimated": "^1.7.0",
    "react-native-router-flux": "^4.2.0",
    "react-native-screens": "^2.0.0-beta.2",
    "react-native-splash-screen": "^3.2.0",
    "react-native-table-component": "^1.2.1",
    "react-native-vector-icons": "^6.6.0",
    "react-native-webview": "^5.12.1",
    "react-redux": "^7.1.3",
    "redux": "^4.0.5",
    "redux-actions": "^2.6.5",
    "redux-thunk": "^2.3.0"
  },
  "devDependencies": {
    "@babel/core": "^7.8.4",
    "@babel/runtime": "^7.8.4",
    "@bam.tech/react-native-make": "^1.0.3",
    "@react-native-community/eslint-config": "^0.0.7",
    "babel-jest": "^25.1.0",
    "eslint": "^6.8.0",
    "jest": "^25.1.0",
    "metro-react-native-babel-preset": "^0.58.0",
    "react-test-renderer": "16.9.0"
  },
  "jest": {
    "preset": "react-native"
  }
}

App.js (main) file

import React, { Component } from 'react';
import { Router, Stack, Scene } from 'react-native-router-flux';
import SplashScreen from 'react-native-splash-screen';
import Orientation from 'react-native-orientation-locker';
import InputScoresPage from './InputScores/InputScores.page';
import ViewGroupScoresPage from './ViewGroupScores/ViewGroupScores.page';

class App extends Component {
  componentDidMount() {
    setTimeout(() => {
      Orientation.lockToPortrait();
      SplashScreen.hide();
    }, 200);
  }

  render() {
    return (
        <Router>
          <Stack key='root' hideNavBar>
            <Scene key='inputScores' component={InputScoresPage} initial />
            <Scene key='viewGroupScores' component={ViewGroupScoresPage} />
          </Stack>
        </Router>
    );
  }
}

export default App;

InputScores/InputScores.page.js file:

import React, { Component } from 'react';
import { Alert } from 'react-native';
import { Actions } from 'react-native-router-flux';
import InputScores from './InputScores';

const MIN_HOLE_NUMBER = 1;
const MAX_HOLE_NUMBER = 18;
const teamNumber = 3;
const PLAYERS = [
  { id: 'gotha_guntter', name: 'Gotha Guntter', handicap: 8 },
  { id: 'aaron_wilson', name: 'Aaron Wilson', handicap: 0 },
  { id: 'bill_steimel', name: 'Bill Steimel', handicap: 9 },
  { id: 'andy_blickhan', name: 'Andy Blickhan', handicap: 8 },
  { id: 'brad_nordquist', name: 'Brad Nordquist', handicap: 6.5 },
];

class InputScoresPage extends Component {
  state = {
    players: PLAYERS.map(player => ({ ...player, scores: Array(MAX_HOLE_NUMBER - MIN_HOLE_NUMBER + 1).fill(0) })),
    currentPlayerIndex: 0,
    currentHoleIndex: 0,
    shownHoleIndex: 0,
  };

  setPlayerScore = (index, value) => {
    let { currentPlayerIndex, currentHoleIndex, shownHoleIndex } = this.state;
    const players = [...this.state.players];
    players[index].scores[shownHoleIndex] = value;
    if (index === players.length - 1 && shownHoleIndex < MAX_HOLE_NUMBER - MIN_HOLE_NUMBER) {
      currentPlayerIndex = 0;
      shownHoleIndex++;
    } else {
      currentPlayerIndex = index + 1;
    }
    currentHoleIndex = shownHoleIndex;
    this.setState({ players, currentPlayerIndex, currentHoleIndex, shownHoleIndex });
  };

  incrementShownHoleIndex = () => {
    if (this.state.shownHoleIndex < MAX_HOLE_NUMBER - MIN_HOLE_NUMBER) {
      this.setState(prevState => ({ shownHoleIndex: prevState.shownHoleIndex + 1 }));
    }
  };

  decreaseShownHoleIndex = () => {
    if (this.state.shownHoleIndex > 0) {
      this.setState(prevState => ({ shownHoleIndex: prevState.shownHoleIndex - 1 }));
    }
  };

  onPressEndRound = () => {
    Alert.alert('Are you sure?', 'Please confirm that you want to end round.', [
      { text: 'Yes', onPress: () => Actions.pop() },
      { text: 'Cancel', style: 'cancel' },
    ]);
  };

  render() {
    return (
      <InputScores
        players={this.state.players}
        teamNumber={teamNumber}
        MIN_HOLE_NUMBER={MIN_HOLE_NUMBER}
        shownHoleIndex={this.state.shownHoleIndex}
        incrementShownHoleIndex={this.incrementShownHoleIndex}
        decreaseShownHoleIndex={this.decreaseShownHoleIndex}
        currentHoleIndex={this.state.currentHoleIndex}
        currentPlayerIndex={this.state.currentPlayerIndex}
        setPlayerScore={this.setPlayerScore}
        onPressViewScores={() => Actions.viewGroupScores()}
        onPressEndRound={this.onPressEndRound}
      />
    );
  }
}

export default InputScoresPage;

InputScores/InputScores.js file:

import React, { Component } from 'react';
import { View, TouchableOpacity, ScrollView, StyleSheet } from 'react-native';
import { Text } from 'react-native-elements';
import Icon from 'react-native-vector-icons/FontAwesome';
import PropTypes from 'prop-types';
import { Container, ModalSelector, Footer } from '../common';
import { SCORE_OPTIONS } from '../constants';
import { WHITE, GRAY, GREEN } from '../styles/colors';

class InputScores extends Component {
  renderHeader = () => (
    <View style={headerStyles.container}>
      <TouchableOpacity onPress={this.props.decreaseShownHoleIndex}>
        <Icon name='arrow-left' color={WHITE} size={30} />
      </TouchableOpacity>
      <Text style={headerStyles.title}>HOLE {this.props.shownHoleIndex + this.props.MIN_HOLE_NUMBER}</Text>
      <TouchableOpacity onPress={this.props.incrementShownHoleIndex}>
        <Icon name='arrow-right' color={WHITE} size={30} />
      </TouchableOpacity>
    </View>
  );

  renderPlayer = (player, i) => {
    const { currentHoleIndex, shownHoleIndex, currentPlayerIndex } = this.props;
    const score = player.scores[shownHoleIndex];
    const pStyles =
      shownHoleIndex === currentHoleIndex && currentPlayerIndex === i ? currentPlayerStyles : playerStyles;
    return (
      <View key={i} style={pStyles.container}>
        <View>
          <Text style={pStyles.name}>{player.name}</Text>
          <Text style={pStyles.team}>Team {this.props.teamNumber}</Text>
          <Text style={pStyles.handicap}>HCP {player.handicap}</Text>
        </View>
        <ModalSelector
          title={{ id: 'select_score', value: 'SELECT SCORE' }}
          options={SCORE_OPTIONS}
          optionTextStyle={{ fontSize: 24, fontWeight: 'bold' }}
          keyExtractor={item => item.id}
          labelExtractor={item => item.value}
          onChange={({ value, id }) => this.props.setPlayerScore(i, value)}
        >
          <TouchableOpacity style={pStyles.scoreContainer}>
            <Text style={pStyles.score}>{score}</Text>
          </TouchableOpacity>
        </ModalSelector>
      </View>
    );
  };

  render() {
    return (
      <Container>
        <ScrollView style={styles.container} showsVerticalScrollIndicator={false} bounces={false}>
          {this.renderHeader()}
          {this.props.players.map((player, i) => this.renderPlayer(player, i))}
        </ScrollView>
        <Footer
          withBack={false}
          withResumeRound={false}
          onPressViewScores={this.props.onPressViewScores}
          onPressEndRound={this.props.onPressEndRound}
        />
      </Container>
    );
  }
}

InputScores.propTypes = {
  players: PropTypes.array,
  teamNumber: PropTypes.number,
  MIN_HOLE_NUMBER: PropTypes.number,
  shownHoleIndex: PropTypes.number,
  incrementShownHoleIndex: PropTypes.func,
  decreaseShownHoleIndex: PropTypes.func,
  currentHoleIndex: PropTypes.number,
  currentPlayerIndex: PropTypes.number,
  setPlayerScore: PropTypes.func,
  onPressViewScores: PropTypes.func,
  onPressEndRound: PropTypes.func,
};

const styles = StyleSheet.create({
  container: {
    width: '100%',
  },
});

const headerStyles = StyleSheet.create({
  container: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    marginBottom: 10,
    paddingVertical: 10,
  },
  title: {
    color: WHITE,
    fontSize: 30,
    fontWeight: 'bold',
    marginLeft: 40,
    marginRight: 40,
  },
});

const playerStyles = StyleSheet.create({
  container: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    paddingTop: 10,
    paddingBottom: 10,
    paddingLeft: 15,
    paddingRight: 10,
  },
  name: {
    fontWeight: 'bold',
    color: WHITE,
    fontSize: 26,
  },
  team: {
    color: WHITE,
    fontSize: 18,
  },
  handicap: {
    color: WHITE,
    fontSize: 18,
  },
  scoreContainer: {
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: GREEN,
    width: 70,
    height: 70,
    borderRadius: 100,
  },
  score: {
    color: GRAY,
    fontWeight: 'bold',
    fontSize: 28,
  },
});

const currentPlayerStyles = StyleSheet.create({
  ...playerStyles,
  container: {
    ...playerStyles.container,
    backgroundColor: GREEN,
  },
  scoreContainer: {
    ...playerStyles.scoreContainer,
    backgroundColor: WHITE,
  },
});

export default InputScores;

ViewGroupScores/ViewGroupScores.page.js file:

import React, { Component } from 'react';
import { Actions } from 'react-native-router-flux';
import Orientation from 'react-native-orientation-locker';
import ViewGroupScores from './ViewGroupScores';

const teamNumber = 3;
const PLAYERS = ['Gotha Guntter', 'Aaron Wilson', 'Bill Steimel', 'Andy Blickhan', 'Brad Nordquist'];

class ViewGroupScoresPage extends Component {
  tableHead = [
    'Players',
    '1',
    '2',
    '3',
    '4',
    '5',
    '6',
    '7',
    '8',
    '9',
    'Out',
    '10',
    '11',
    '12',
    '13',
    '14',
    '15',
    '16',
    '17',
    '18',
    'In',
    'Total',
    'Tees',
    'Hdcp',
  ];

  widthArr = [200, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 80, 80, 80];

  componentDidMount() {
    Orientation.lockToLandscape();
  }

  componentWillUnmount() {
    Orientation.lockToPortrait();
  }

  render() {
    return (
      <ViewGroupScores
        onPressBack={() => Actions.pop()}
        teamNumber={teamNumber}
        tableHead={this.tableHead}
        players={PLAYERS}
        widthArr={this.widthArr}
      />
    );
  }
}

export default ViewGroupScoresPage;

ViewGroupScores/ViewGroupScores.js file:

import React, { Component } from 'react';
import { ScrollView, View, Text, StyleSheet } from 'react-native';
import { Table, Row } from 'react-native-table-component';
import PropTypes from 'prop-types';
import { Container, BackButton } from '../common';
import { GREEN, ORANGE, WHITE } from '../styles/colors';

class ViewGroupScores extends Component {
  renderHeader = () => (
    <View style={headerStyles.container}>
      <BackButton onPress={this.props.onPressBack} />
      <Text style={headerStyles.title}>Scores - Team {this.props.teamNumber}</Text>
    </View>
  );

  render() {
    const tableData = [];
    for (let i = 0; i < this.props.players.length; i += 1) {
      const rowData = [this.props.players[i]];
      // scores for holes 1-9
      for (let j = 0; j < 9; j += 1) {
        rowData.push(0);
      }
      // Out score
      rowData.push(0);
      // scores for holes 10-18
      for (let j = 0; j < 9; j += 1) {
        rowData.push(0);
      }
      // In score
      rowData.push(0);
      // Total score
      rowData.push(0);
      // Tee
      rowData.push('Blue');
      // Hdcp
      rowData.push(5);
      tableData.push(rowData);
    }
    return (
      <Container>
        {this.renderHeader()}
        <ScrollView style={styles.container} horizontal={true} bounces={false}>
          <View>
            <Table borderStyle={{ borderWidth: 1, borderColor: WHITE }}>
              <Row
                data={this.props.tableHead}
                widthArr={this.props.widthArr}
                style={tableStyles.header}
                textStyle={tableStyles.text}
              />
            </Table>
            <ScrollView style={tableStyles.dataWrapper} bounces={false}>
              <Table borderStyle={{ borderWidth: 1, borderColor: WHITE }}>
                {tableData.map((rowData, index) => (
                  <Row
                    key={index}
                    data={rowData}
                    widthArr={this.props.widthArr}
                    style={tableStyles.row}
                    textStyle={tableStyles.text}
                  />
                ))}
              </Table>
            </ScrollView>
          </View>
        </ScrollView>
      </Container>
    );
  }
}

ViewGroupScores.propTypes = {
  onPressBack: PropTypes.func,
  teamNumber: PropTypes.number,
  tableHead: PropTypes.array,
  players: PropTypes.array,
};

export default ViewGroupScores;

const styles = StyleSheet.create({
  container: {
    width: '100%',
  },
});

const headerStyles = StyleSheet.create({
  container: {
    width: '80%',
  },
  title: {
    color: WHITE,
    fontSize: 30,
    alignSelf: 'center',
    marginBottom: 30,
  },
});

const tableStyles = StyleSheet.create({
  header: {
    height: 50,
    backgroundColor: ORANGE,
  },
  text: {
    textAlign: 'center',
    color: WHITE,
    fontWeight: 'bold',
  },
  dataWrapper: {
    marginTop: -1,
  },
  row: {
    height: 40,
    backgroundColor: GREEN,
  },
});

common/Container.js file:

import React, { Component } from 'react';
import { SafeAreaView, StyleSheet } from 'react-native';
import { WHITE, BLACK } from '../styles/colors';

class Container extends Component {

  render() {
    return (
      <SafeAreaView style={styles.container}>
        {this.props.children}
      </SafeAreaView>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    backgroundColor: BLACK,
  },
});

export { Container };

common/BackButton.js file:

import React from 'react';
import { View, TouchableOpacity, StyleSheet } from 'react-native';
import { Icon } from 'react-native-elements';
import PropTypes from 'prop-types';
import { WHITE } from '../styles/colors';

const BackButton = props => (
  <View style={[styles.container, props.containerStyle]}>
    <TouchableOpacity onPress={props.onPress}>
      <Icon name='md-arrow-back' type='ionicon' size={30} color={WHITE} />
    </TouchableOpacity>
  </View>
);

const styles = StyleSheet.create({
  container: {
    alignItems: 'flex-start',
    marginTop: 10,
    marginBottom: 10,
  },
});

BackButton.propTypes = {
  containerStyle: PropTypes.object,
  onPress: PropTypes.func,
};

BackButton.defaultProps = {
  onPress: () => {},
};

export { BackButton };

common/ModalSelector.js file:

import React, { Component } from 'react';
import { StyleSheet, Text } from 'react-native';
import DefaultModalSelector from 'react-native-modal-selector';
import PropTypes from 'prop-types';
import { BLACK, GREEN } from '../styles/colors';

class ModalSelector extends Component {
  render() {
    const data = [
      {
        ...this.props.title,
        section: true,
        component: (
          <Text style={[styles.title, this.props.titleStyle]}>{this.props.labelExtractor(this.props.title)}</Text>
        ),
      },
      ...this.props.options,
    ];
    return (
      <DefaultModalSelector
        {...this.props}
        optionTextStyle={[styles.optionText, this.props.optionTextStyle]}
        cancelTextStyle={[styles.cancelText, this.props.cancelTextStyle]}
        data={data}
      >
        {this.props.children}
      </DefaultModalSelector>
    );
  }
}

ModalSelector.propTypes = {
  title: PropTypes.object.isRequired,
  options: PropTypes.array.isRequired,
  labelExtractor: PropTypes.func.isRequired,
  cancelText: PropTypes.string,
  titleStyle: PropTypes.number,
};

ModalSelector.defaultProps = {
  cancelText: 'CANCEL',
};

export { ModalSelector };

const styles = StyleSheet.create({
  title: {
    color: GREEN,
    fontSize: 18,
    fontWeight: 'bold',
    alignSelf: 'center',
  },
  optionText: {
    color: BLACK,
  },
  cancelText: {
    color: GREEN,
    fontWeight: 'bold',
  },
});

common/Footer.js file:

import React, { Component } from 'react';
import { View, TouchableOpacity, Text, StyleSheet } from 'react-native';
import { Icon } from 'react-native-elements';
import PropTypes from 'prop-types';
import { WHITE, GREEN, GRAY } from '../styles/colors';

class Footer extends Component {
  renderFooterBox = (onPress, text, iconName, iconType) => (
    <TouchableOpacity style={boxStyles.container} onPress={onPress}>
      <Icon name={iconName} type={iconType} size={30} color={GREEN} />
      <Text style={boxStyles.text}>{text}</Text>
    </TouchableOpacity>
  );

  render() {
    return (
      <View style={styles.container}>
        {this.props.withBack && this.renderFooterBox(this.props.onPressBack, 'Back', 'md-arrow-back', 'ionicon')}
        {this.props.withViewScores &&
          this.renderFooterBox(this.props.onPressViewScores, 'View Scores', 'eye', 'font-awesome')}
        {this.props.withResumeRound &&
          this.renderFooterBox(this.props.onPressResumeRound, 'Resume Round', 'golf-course', 'material')}
        {this.props.withEndRound &&
          this.renderFooterBox(this.props.onPressEndRound, 'End Round', 'close', 'font-awesome')}
      </View>
    );
  }
}

Footer.propTypes = {
  withBack: PropTypes.bool,
  onPressBack: PropTypes.func,
  withViewScores: PropTypes.bool,
  onPressViewScores: PropTypes.func,
  withResumeRound: PropTypes.bool,
  onPressResumeRound: PropTypes.func,
  withEndRound: PropTypes.bool,
  onPressEndRound: PropTypes.func,
};

Footer.defaultProps = {
  withBack: true,
  withViewScores: true,
  withResumeRound: true,
  withEndRound: true,
};

export { Footer };

const styles = StyleSheet.create({
  container: {
    width: '100%',
    borderRadius: 5,
    overflow: 'hidden',
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: WHITE,
  },
});

const boxStyles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'flex-end',
    borderLeftWidth: 0.5,
    borderRightWidth: 0.5,
    borderColor: GRAY,
    paddingHorizontal: 20,
    paddingVertical: 5,
  },
  text: {
    color: GREEN,
    fontSize: 14,
    marginTop: 3,
  },
});

common/index.js file:

export * from './Container';
export * from './BackButton';
export * from './ModalSelector';
export * from './Footer';

constants/index.js file:

export const PLAYER = 'player';
export const ADMIN = 'admin';

export const TEES = ['Blue', 'Black', 'Gold', 'Blk/Gld', 'Red', 'Red/Wht', 'White', 'Blu/Wht'];

export const DEFAULT_TEE_INDEX = 0;

export const SCORE_OPTIONS = [
  { id: '0', value: 0 },
  { id: '1', value: 1 },
  { id: '2', value: 2 },
  { id: '3', value: 3 },
  { id: '4', value: 4 },
  { id: '5', value: 5 },
  { id: '6', value: 6 },
  { id: '7', value: 7 },
  { id: '8', value: 8 },
  { id: '9', value: 9 },
  { id: '10', value: 10 },
  { id: '11', value: 11 },
  { id: '12', value: 12 },
  { id: '13', value: 13 },
  { id: '14', value: 14 },
  { id: '15', value: 15 },
];

styles/colors.js file:

export const WHITE = '#ffffff';
export const BLACK = '#000000';
export const GREEN = '#4fa564';
export const GRAY = '#bdc6cf';
export const DARK_GRAY = '#5d6160';
export const ORANGE = '#f6af46';
export const CREAM = '#f6f5f3';

Most helpful comment

@ggunti Hey, after some procrastination, I found the solution here https://github.com/wonday/react-native-orientation-locker/issues/106, just add headerMode="none" to the stack and the problem is gone.
Like this,

<Stack
     panHandlers={null}
     hideNavBar
     key="root"
     headerMode="none"
>

All 8 comments

The App I am developing has this issue too, and this problem only occurs in iOS also

I used React native inspector to see what that box is, and it is called "KeyboardAvoidingView".

Yes, it happen only on iOS. Did you find a solution for this?

Sorry, I haven't solved this problem yet. I tried to update the version of react-navigation but the box is still there.

@ggunti Hey, after some procrastination, I found the solution here https://github.com/wonday/react-native-orientation-locker/issues/106, just add headerMode="none" to the stack and the problem is gone.
Like this,

<Stack
     panHandlers={null}
     hideNavBar
     key="root"
     headerMode="none"
>

@ming436534 Thank you very much, it works !!!

after setting headerMode to none my header is gone. After setting headerMode="screen", my problem is solved now.

@ming436534 Thank you very much, it works !!!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

xnog picture xnog  路  3Comments

GCour picture GCour  路  3Comments

wootwoot1234 picture wootwoot1234  路  3Comments

maphongba008 picture maphongba008  路  3Comments

booboothefool picture booboothefool  路  3Comments