React Native:方向变化时应用不同的样式

React Native: Different styles applied on orientation change

我正在开发一个 React Native 应用程序,将作为本机应用程序部署在 iOS 和 Android(以及 Windows,如果可能的话)。

问题是我们希望布局根据屏幕尺寸和方向不同

我制作了一些 return 样式对象 的函数,并在每个组件渲染函数上调用,因此我能够在应用程序启动时应用不同的样式,但如果方向(或屏幕大小)在应用程序初始化后发生变化,则不会重新计算或重新应用它们。

我已将侦听器添加到顶部渲染,以便它在方向更改时更新其状态(并强制渲染应用程序的其余部分),但子组件不会重新渲染(因为,事实上,他们没有被改变).

所以,我的问题是:我怎样才能根据屏幕尺寸和方向使完全不同的样式,就像 CSS 媒体查询(即时呈现)一样?

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

谢谢!

我终于做到了。不知道它可能带来的性能问题,但它们应该不是问题,因为它只在调整大小或方向更改时调用。

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

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

有了这个,当方向发生变化或 window 调整大小时,我强制重新渲染组件。

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

import ScreenMetrics from './globalFunctionContainer';

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

    this.state = {};

    ScreenMetrics.bindScreenDimensionsUpdate(this);
  }
}

这样,每次 window 调整大小或方向更改时都会重新呈现。

你应该注意,然而,这必须应用于我们想要监听方向变化的每个组件,因为如果 parent 容器更新但children 的 state(或 props)不更新,它们不会被重新渲染,所以 它可能是一个性能杀手 如果我们有一棵大 children 树在听它。

希望对大家有所帮助!

您可以使用 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({
  ...
});

如果使用 Hooks。可以参考这个解决方案:

应用程序的方向从纵向到横向,反之亦然是一项听起来很容易的任务,但当方向改变时必须改变视图时,在 React Native 中可能会很棘手。换句话说,可以通过考虑这两个步骤来实现为两个方向定义不同的视图。

从 React Native 导入维度

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()' 这样我们就不用再担心渲染了,一切都搞定了。

到目前为止,我使用这个库取得了最大的成功:https://github.com/axilis/react-native-responsive-layout 它可以满足您的要求以及更多。简单的组件实现几乎没有像上面一些更复杂的答案那样的逻辑。我的项目通过 RNW 使用 Phone、平板电脑、 和网络 - 并且实现完美无缺。此外,在调整浏览器大小时,它是真正响应式的,而不仅仅是在初始渲染时 - 完美处理 phone 方向变化。

示例代码(将任何组件作为块的子项):

<Grid>
  <Section> {/* Light blue */}
    <Block xsSize="1/1" smSize="1/2" />
    <Block xsSize="1/1" smSize="1/2" />
    <Block xsSize="1/1" smSize="1/2" /> 
  </Section>
  <Section> {/* Dark blue */}
    <Block size="1/1" smSize="1/2" />
    <Block size="1/1" smSize="1/2" />
    <Block size="1/1" smSize="1/2" />
    <Block size="1/1" smSize="1/2" />
    <Block size="1/1" smSize="1/2" />
  </Section>
</Grid>

给这个:

我已经为我的 expo SDK36 项目编写了一个 HoC 解决方案,它支持方向更改并根据 ScreenOrientation.Orientation 值传递 props.orientation

import React, { Component } from 'react';
import { ScreenOrientation } from 'expo';

export default function withOrientation(Component) {
  class DetectOrientation extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        orientation: '',
      };
      this.listener = this.listener.bind(this);
    }

    UNSAFE_componentWillMount() {
      this.subscription = ScreenOrientation.addOrientationChangeListener(this.listener);
    }

    componentWillUnmount() {
      ScreenOrientation.removeOrientationChangeListener(this.subscription);
    }

    listener(changeEvent) {
      const { orientationInfo } = changeEvent;
      this.setState({
        orientation: orientationInfo.orientation.split('_')[0],
      });
    }

    async componentDidMount() {
      await this.detectOrientation();
    }

    async detectOrientation() {
      const { orientation } = await ScreenOrientation.getOrientationAsync();
      this.setState({
        orientation: orientation.split('_')[0],
      });
    }

    render() {
      return (
        <Component
          {...this.props}
          {...this.state}
          onLayout={this.detectOrientation}
        />
      );
    }
  }
  return (props) => <DetectOrientation {...props} />;
}

为了实现更高性能的集成,我将以下内容用作我的每个反应导航屏幕的超类:

export default class BaseScreen extends Component {
  constructor(props) {
    super(props)

    const { height, width } = Dimensions.get('screen')

    // use this to avoid setState errors on unmount
    this._isMounted = false

    this.state = {
      screen: {
        orientation: width < height,
        height: height,
        width: width
      }
    }
  }

  componentDidMount() {
    this._isMounted = true
    Dimensions.addEventListener('change', () => this.updateScreen())
  }

  componentWillUnmount() {
    this._isMounted = false
    Dimensions.removeEventListener('change', () => this.updateScreen())
  }

  updateScreen = () => {
    const { height, width } = Dimensions.get('screen')

    if (this._isMounted) {
      this.setState({
        screen: {
          orientation: width < height,
          width: width, height: height
        }
      })
    }
  }

将任何根组件设置为从此组件扩展,然后将屏幕状态从继承的根组件传递给您的 leaf/dumb 组件。

此外,为避免增加性能开销,请更改样式对象而不是向组合中添加更多组件:

const TextObject = ({ title }) => (
  <View style={[styles.main, screen.orientation ? styles.column : styles.row]}>
    <Text style={[styles.text, screen.width > 600 ? {fontSize: 14} : null ]}>{title}</Text>
  </View>
)

const styles = StyleSheet.create({
  column: {
    flexDirection: 'column'
  },
  row: {
    flexDirection: 'row'
  },
  main: {
    justifyContent: 'flex-start'
  },
  text: {
    fontSize: 10
  }
}

我希望这对以后的任何人都有帮助,您会发现它在开销方面非常理想。

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

组件在方向改变时重新呈现它的内容。 例如,您可以传递 landscapeStyles 和 portraitStyles 以不同方式显示这些方向。 适用于 iOS 和 Android。 它易于使用。看看吧。

我遇到了同样的问题。方向改变后布局没有改变。 然后我明白了一个简单的想法——布局应该取决于应该在渲染函数中计算的屏幕宽度,即

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

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

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

这是@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'}} />
    );
}

React Native 也有 useWindowDimensions 钩子,returns 设备的宽度和高度。
这样,您可以通过比较宽度和高度轻松检查设备是 'Portrait' 还是 'Landscape'。
查看更多 here

** 我将此逻辑用于横向和纵向逻辑。** ** 如果我首先在横向启动我的应用程序,我将获得设备的真实高度。并相应地管理 header 的高度。**

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

我正在使用 styled-components,这就是我 re-render UI 方向改变的方式。

import React, { useState } from 'react';
import { View } from 'react-native';
import { ThemeProvider } from 'styled-components';
import appTheme from 'constants/appTheme';

const App = () => {
  // Re-Layout on orientation change
  const [theme, setTheme] = useState(appTheme.getTheme());
  const onLayout = () => {
    setTheme(appTheme.getTheme());
  }

  return (
    <ThemeProvider theme={theme}>
      <View onLayout={onLayout}/>
      {/* Components */}
    </ThemeProvider>
  );
}

export default App;

即使您没有使用 styled-components,您也可以创建状态并将其更新到 onLayout 到 re-render UI。

这是我的解决方案:

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

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

纯成分案例的注意事项。 @mridul-tripathi 答案工作正常,但如果使用纯组件,那么可能只有 parent/top-level 组件对方向变化做出反应是不够的。您还需要在方向更改时单独更新纯组件。