React Native: 屏幕方向改变时应用不同的样式

53

我正在开发一个 React Native 应用,希望能够在 iOS 和 Android 上部署本地应用程序(如果可能的话,也在 Windows 上部署)。

问题是我们希望布局可以根据不同的屏幕尺寸和方向进行不同的排版。

我编写了一些函数来返回样式对象,并在每个组件渲染函数中调用它们,因此我能够在应用程序启动时应用不同的样式。但是,如果初始化后方向(或屏幕大小)发生更改,它们就不会重新计算或重新应用。

我已经添加了侦听器到顶级元素上,以便在方向更改时更新其状态(并强制对整个应用程序进行重新渲染),但是子组件不会重新渲染(因为实际上它们没有被更改)。

所以,我的问题是:如何使样式可以根据屏幕尺寸和方向完全不同,就像使用CSS媒体查询一样(在运行时呈现)?

我已经尝试了react-native-responsive模块,但没有成功。

谢谢!


请尝试此链接:https://dev59.com/o7voa4cB1Zd3GeqP87bI#64559463 - shammi
16个回答

64

如果使用Hooks,您可以参考这个解决方案:https://dev59.com/CVYN5IYBdhLWcg3wRmeh#61838183

在React Native中,将应用从纵向切换到横向或者反之似乎很简单,但是当视图需要随着屏幕方向的改变而改变时,可能会出现一些棘手的问题。换句话说,在两种不同的屏幕方向下拥有不同的视图,可以通过以下两个步骤来实现。

从React Native导入Dimensions

import { Dimensions } from 'react-native';

识别当前方向并相应地呈现视图

/**
 * Returns true if the screen is in portrait mode
 */
const isPortrait = () => {
    const dim = Dimensions.get('screen');
    return dim.height >= dim.width;
};
 
/**
 * Returns true of the screen is in landscape mode
 */
const isLandscape = () => {
    const dim = Dimensions.get('screen');
    return dim.width >= dim.height;
};

了解屏幕方向何时更改以相应地更改视图

// Event Listener for orientation changes
    Dimensions.addEventListener('change', () => {
        this.setState({
            orientation: Platform.isPortrait() ? 'portrait' : 'landscape'
        });
    });

汇集所有零件

import React from 'react';
import {
  StyleSheet,
  Text,
  Dimensions,
  View
} from 'react-native';

export default class App extends React.Component {
  constructor() {
    super();

    /**
    * Returns true if the screen is in portrait mode
    */
    const isPortrait = () => {
      const dim = Dimensions.get('screen');
      return dim.height >= dim.width;
    };

    this.state = {
      orientation: isPortrait() ? 'portrait' : 'landscape'
    };

    // Event Listener for orientation changes
    Dimensions.addEventListener('change', () => {
      this.setState({
        orientation: isPortrait() ? 'portrait' : 'landscape'
      });
    });

  }

  render() {
    if (this.state.orientation === 'portrait') {
      return (
          //Render View to be displayed in portrait mode
       );
    }
    else {
      return (
        //Render View to be displayed in landscape mode
      );
    }

  }
}

当定义用于检测方向改变的事件时,该命令使用“this.setState()”,此方法会自动再次调用“render()”,因此我们不必担心再次渲染它,所有都已经处理好了。


1
请在答案中以文本形式发布代码,而不是图像。您可以使用Ctrl + K或编辑器顶部的“ {}”按钮将代码格式化为代码块。 - eesiraed
1
感谢@FeiXiang。我已经做出了更改。 - Mridul Tripathi
如果设备的宽度大于高度,这个解决方案将无法在其上运行。 - ICW
1
这个也适用于Web吗?它能和Expo一起使用吗?他们似乎使用了不同的API来获取尺寸:https://docs.expo.io/versions/latest/sdk/screen-orientation/#screenorientationgetorientationasync - Dimitri Kopriwa
你没有在卸载时移除监听器。 - Karatekid430

42

这是 @Mridul Tripathi 的可重用钩子答案:

// useOrientation.tsx
import {useEffect, useState} from 'react';
import {Dimensions} from 'react-native';

/**
 * Returns true if the screen is in portrait mode
 */
const isPortrait = () => {
  const dim = Dimensions.get('screen');
  return dim.height >= dim.width;
};

/**
 * A React Hook which updates when the orientation changes
 * @returns whether the user is in 'PORTRAIT' or 'LANDSCAPE'
 */
export function useOrientation(): 'PORTRAIT' | 'LANDSCAPE' {
  // State to hold the connection status
  const [orientation, setOrientation] = useState<'PORTRAIT' | 'LANDSCAPE'>(
    isPortrait() ? 'PORTRAIT' : 'LANDSCAPE',
  );

  useEffect(() => {
    const callback = () => setOrientation(isPortrait() ? 'PORTRAIT' : 'LANDSCAPE');

    Dimensions.addEventListener('change', callback);

    return () => {
      Dimensions.removeEventListener('change', callback);
    };
  }, []);

  return orientation;
}

您可以使用以下方式来消费它:

import {useOrientation} from './useOrientation';

export const MyScreen = () => {
    const orientation = useOrientation();

    return (
        <View style={{color: orientation === 'PORTRAIT' ? 'red' : 'blue'}} />
    );
}

2
你可能想将[]作为第二个参数添加到你的useEffect中,否则effect hook将会无限制地运行。 - adolfosrs
如果我想在没有组件的情况下使用方向,该怎么办?有任何解决方案吗? - Oğulcan Karayel
应该这样写: const dim = Dimensions.get('window'); - Azeezat Raheem
1
@AzeezatRaheem 不一定。window 是可见应用程序窗口的大小。screen 是设备屏幕的大小。来源 - Eric Wiener
这个几乎可以工作,但我不得不像这个视频中展示的那样进行修改 https://www.youtube.com/watch?v=1dpyHY--bVQ - Variag

16

您可以使用 onLayout 属性:

export default class Test extends Component {

  constructor(props) {
    super(props);
    this.state = {
      screen: Dimensions.get('window'),
    };
  }

  getOrientation(){
    if (this.state.screen.width > this.state.screen.height) {
      return 'LANDSCAPE';
    }else {
      return 'PORTRAIT';
    }
  }

  getStyle(){
    if (this.getOrientation() === 'LANDSCAPE') {
      return landscapeStyles;
    } else {
      return portraitStyles;
    }
  }
  onLayout(){
    this.setState({screen: Dimensions.get('window')});
  }

  render() {
    return (
      <View style={this.getStyle().container} onLayout = {this.onLayout.bind(this)}>

      </View>
      );
    }
  }
}

const portraitStyles = StyleSheet.create({
 ...
});

const landscapeStyles = StyleSheet.create({
  ...
});

6

最后,我已经成功了。不知道它能承载什么性能问题,但因为它只在调整大小或方向更改时被调用,所以这应该不是问题。

我创建了一个全局控制器,在其中有一个函数接收组件(容器、视图)并向其添加事件侦听器:

const getScreenInfo = () => {
    const dim = Dimensions.get('window');
    return dim;
}    

const bindScreenDimensionsUpdate = (component) => {
    Dimensions.addEventListener('change', () => {
        try{
            component.setState({
                orientation: isPortrait() ? 'portrait' : 'landscape',
                screenWidth: getScreenInfo().width,
                screenHeight: getScreenInfo().height
            });
        }catch(e){
            // Fail silently
        }
    });
}

通过这种方式,当屏幕方向或窗口大小发生改变时,我强制重新渲染组件。

然后,在每个组件的构造函数中:

import ScreenMetrics from './globalFunctionContainer';

export default class UserList extends Component {
  constructor(props){
    super(props);

    this.state = {};

    ScreenMetrics.bindScreenDimensionsUpdate(this);
  }
}

这样做可以在窗口大小调整或方向更改时重新渲染组件。

但是需要注意的是,我们必须将其应用于每个要监听方向更改的组件,因为如果父容器更新了但子组件的state(或props)没有更新,它们不会被重新渲染,因此如果我们有一个大的子组件树在监听方向更改,这可能会影响性能

希望对某些人有所帮助!


你可能存在内存泄漏,事件监听器没有被移除?这也可能会影响性能,因为随着时间的推移,未被移除的回调可能会变得越来越多。 - oldwizard
是的,正如我在我的回答中已经解释过的那样,这可能会导致性能问题,具体取决于侦听器附加的项目数量。但如果您删除侦听器,它将不会在下次更改方向时更新UI,因此删除它们也没有用。 - Unapedra
2
当组件卸载时移除监听器是很有用的。如果不这样做,最终会耗尽内存。 - oldwizard

2

我做了一个超轻量级的组件来解决这个问题。 https://www.npmjs.com/package/rn-orientation-view

这个组件会在方向改变时重新渲染内容。你可以传递landscapeStyles和portraitStyles来以不同的方式显示这些方向。 适用于iOS和Android。 使用起来很容易。试试吧。


2

React Native还有useWindowDimensions钩子,返回设备的宽度和高度。
通过比较宽度和高度,您可以轻松检查设备是否处于“纵向”或“横向”状态。
更多信息请参见此处


2

我遇到了同样的问题。在屏幕方向改变后,布局没有改变。后来我明白了一个简单的想法 - 布局应该依赖于渲染函数内计算出的屏幕宽度。

getScreen = () => {
  return Dimensions.get('screen');
}

render () {
  return (
    <View style={{ width: this.getScreen().width }>
      // your code
    </View>
  );
}

在这种情况下,宽度将在渲染时计算。

1

这是我的解决方案:

const CheckOrient = () => {
  console.log('screenHeight:' + Dimensions.get('screen').height + ', screenWidth: ' + Dimensions.get('screen').width);
}

return ( <
    View onLayout = {
      () => CheckOrient()
    } >
    ............
    <
    /View>


1
我正在使用这种逻辑来处理我的横屏和竖屏逻辑。 如果我首先在横屏中启动应用程序,我将获得设备的真实高度,并相应地管理标题的高度。
const [deviceOrientation, setDeviceOrientation] = useState(
    Dimensions.get('window').width < Dimensions.get('window').height
      ? 'portrait'
      : 'landscape'
  );
  const [deviceHeight, setDeviceHeight] = useState(
    Dimensions.get('window').width < Dimensions.get('window').height
      ? Dimensions.get('window').height
      : Dimensions.get('window').width
  );

  useEffect(() => {
    const setDeviceHeightAsOrientation = () => {
      if (Dimensions.get('window').width < Dimensions.get('window').height) {
        setDeviceHeight(Dimensions.get('window').height);
      } else {
        setDeviceHeight(Dimensions.get('window').width);
      }
    };
    Dimensions.addEventListener('change', setDeviceHeightAsOrientation);
    return () => {
      //cleanup work
      Dimensions.removeEventListener('change', setDeviceHeightAsOrientation);
    };
  });

  useEffect(() => {
    const deviceOrientation = () => {
      if (Dimensions.get('window').width < Dimensions.get('window').height) {
        setDeviceOrientation('portrait');
      } else {
        setDeviceOrientation('landscape');
      }
    };
    Dimensions.addEventListener('change', deviceOrientation);
    return () => {
      //cleanup work
      Dimensions.removeEventListener('change', deviceOrientation);
    };
  });
  console.log(deviceHeight);
  if (deviceOrientation === 'landscape') {
    return (
      <View style={[styles.header, { height: 60, paddingTop: 10 }]}>
        <TitleText>{props.title}</TitleText>
      </View>
    );
  } else {
    return (
      <View
        style={[
          styles.header,
          {
            height: deviceHeight >= 812 ? 90 : 60,
            paddingTop: deviceHeight >= 812 ? 36 : 10
          }
        ]}>
        <TitleText>{props.title}</TitleText>
      </View>
    );
  }

0

你所需要的只有:

import { useWindowDimensions } from 'react-native';

export default function useOrientation() {
  const window = useWindowDimensions();

  return window.height >= window.width ? 'portrait' : 'landscape';
}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接