React Native Expo orientation listener without changing orientation

Viewed 4284

I'm tryin to detect the current orientaiton on my React Native app without having to change the orientation. I made a camera app and I want to flip the icons only when the orientation changes. Right now I can't use ScreenOrientation.addOrientationChangeListener(listener) without the actual orientation changing. Any ways around this?

3 Answers

If I understand you correctly, the requirement is to get the device orientation even though the actual orientation has not been changed because it locked by orientation.

You can use DeviceMotion module to get the device rotation (Euler angle)

The exact calculation is taken from https://github.com/expo/expo/issues/2430#issuecomment-538074235

function orientationCalculation(gamma: number, beta: number) {
  let ABSOLUTE_GAMMA = Math.abs(gamma);
  let ABSOLUTE_BETA = Math.abs(beta);
  let isGammaNegative = Math.sign(gamma) == -1;
  let orientation = 0;

  if (ABSOLUTE_GAMMA <= 0.04 && ABSOLUTE_BETA <= 0.24) {
    //Portrait mode, on a flat surface.
    orientation = 0;
  } else if (
    (ABSOLUTE_GAMMA <= 1.0 || ABSOLUTE_GAMMA >= 2.3) &&
    ABSOLUTE_BETA >= 0.5
  ) {
    //General Portrait mode, accounting for forward and back tilt on the top of the phone.
    orientation = 0;
  } else {
    if (isGammaNegative) {
      //Landscape mode with the top of the phone to the left.
      orientation = -90;
    } else {
      //Landscape mode with the top of the phone to the right.
      orientation = 90;
    }
  }
  return orientation;
}

DeviceMotion.addListener(({ orientation, rotation }) => {
  setOrientation(
    orientationCalculation(rotation.gamma, rotation.beta) === 0
      ? 'protrait'
      : 'landscape'
  );
});

Expo demo: https://expo.dev/@moshfeu/detect-orientation-by-device-motion?serviceType=classic&distribution=expo-go

Source code: https://github.com/moshfeu/detect-orientation-by-device-motion/


Older Answer

DeviceMotion.addListener(({rotation}) => {
  const alpha = Math.abs(rotation.alpha);
  this.setState({
    orientation: alpha > 3 || (alpha > 0 && alpha < 0.5) ? 'landscape' : 'protrait'
  });
});

Live demo: https://expo.io/@moshfeu/snack-dc115508-fab4-4be4-818a-5a03f89725bd


Oldest Answer

If I understand you correctly, you want to get the current orientation on the app is load without the user changing the orientation.

If so, you can get it from ScreenOrientation.getOrientationAsync() which will return a Promise with one of the possible values

ScreenOrientation.getOrientationAsync().then(data => this.setState({data}));

Live example: https://snack.expo.io/@moshfeu/ff4c76

In order to get this working, I needed to:

  • Add code to componentDidMount:

async componentDidMount() {
  if (await DeviceMotion.isAvailableAsync()) {
    DeviceMotion.addListener(this.calculateRotation)
  }

  // ...
}

  • Add code to componentWillUnmount:

componentWillUnmount() {
  DeviceMotion.removeAllListeners()
}

calculateRotation = ({rotation: {beta, gamma}}) => {
  let absGamma = Math.abs(gamma)
  let absBeta = Math.abs(beta)
  let rotate = 0

  if (absGamma <= 0.04 && absBeta <= 0.24) {
    // Portrait mode, on a flat surface.
    rotate = 0
  } else if ((absGamma <= 1.0 || absGamma >= 2.3) && absBeta >= 0.5) {
    // General Portrait mode, accounting for forward and back tilt on the top of the phone.
    rotate = 0
  } else {
    if (gamma < 0) {
      // Landscape mode with the top of the phone to the left.
      rotate = -90
    } else {
      // Landscape mode with the top of the phone to the right.
      rotate = 90
    }
  }
  this.setState({rotate})
}

  • When I need to get the current rotation, call this.state.rotate. This tells how much a photo taken needs to be rotated for a right-side-up image.

matchPhotoRotation = (photo) => {
  const {rotate} = this.state

  if (rotate === 0) return photo

  return ImageManipulator.manipulateAsync(
    photo.uri,
    [{rotate}],
  )
}

I found another solution on GitHub, similar to Mosh Feu's answer using DeviceMotion, but using gamma and beta (x and y axis) instead of alpha (z axis) to figure out the rotation of the device. In my case I achevied a more satisfying result with this technique.

isPortrait(gamma: number, beta: number) {
 const ABSOLUTE_GAMMA = Math.abs(gamma);
 const ABSOLUTE_BETA = Math.abs(beta);
 const isGammaNegative = Math.sign(gamma) === -1;

 if (ABSOLUTE_GAMMA <= 0.04 && ABSOLUTE_BETA <= 0.24) {
  //Portrait mode, on a flat surface.
  return true;
 } else 
 if (
   (ABSOLUTE_GAMMA <= 1.0 || ABSOLUTE_GAMMA >= 2.3) &&
   ABSOLUTE_BETA >= 0.5
 ) {
   //General Portrait mode, accounting for forward and back tilt on the top of the phone.
   return true;
 } else {
   if (isGammaNegative) {
     //Landscape mode with the top of the phone to the left.
     return false;
   } else {
     //Landscape mode with the top of the phone to the right.
     return false;
   }
 }
}

And I call it in the listener from the componentDidMount method (it's good practice to check sensor availability first with DeviceMotion.isAvailableAsync()):

const sensorAvailable = await DeviceMotion.isAvailableAsync();
if (sensorAvailable) {
 DeviceMotion.addListener(({ rotation }) => {
  const { gamma, beta } = rotation;
  this.setState({
    orientation: this.isPortrait(gamma, beta) ? 'portrait' : 'landscape'
   });
  });
 }
Related