aksonov / react-native-router-flux

The first declarative React Native router
MIT License
8.99k stars 2.11k forks source link

Scene looks bad in landscape mode #3636

Open ggunti opened 4 years ago

ggunti commented 4 years ago

Version

Tell us which versions you are using:

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):

Captură 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):

Captură de ecran din 2020-02-28 la 15 26 05 Captură 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';
ming436534 commented 4 years ago

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

ming436534 commented 4 years ago

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

ggunti commented 4 years ago

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

ming436534 commented 4 years ago

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

ming436534 commented 4 years ago

@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"
>
ggunti commented 4 years ago

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

yestay90 commented 4 years ago

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

zi6xuan commented 4 years ago

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