Ionic/Cordova设备旋转时获取正确罗盘航向的问题
我开发了一款Ionic/Cordova应用,需要获取设备指向的方向。目前使用Cordova Plugin Device Orientation获取罗盘数据,同时借助Cordova Plugin Screen Orientation获取屏幕方向,以此修正设备旋转(竖屏/横屏)时的航向,规则如下:
- 竖屏模式下直接使用罗盘数据
- 向左/向右旋转至横屏时分别加90/270度
- 设备倒置(竖屏)时加180度
- 数值超过360则减去360
但根据Screen Orientation插件文档,Android设备在倒置或从横屏底部旋转至另一横屏时,screen.orientation属性不会更新,导致无法获取PORTRAIT_SECONDARY等状态,无法计算正确航向。
补充信息:三星平板上功能正常,三星手机上因系统不允许应用倒置,问题复现。
测试页面代码如下:
import { Component, OnDestroy, OnInit } from '@angular/core'; import { distinctUntilChanged } from 'rxjs/operators'; import { DeviceOrientationCompassHeading } from '@awesome-cordova-plugins/device-orientation'; import { DeviceOrientation } from '@awesome-cordova-plugins/device-orientation/ngx'; import { ScreenOrientation } from '@awesome-cordova-plugins/screen-orientation/ngx'; import { Subscription } from 'rxjs'; @Component({ selector: 'app-orientation', template: 'Heading: {{compassHeading}}/{{calculatedHeading}}' }) export class OrientationPage implements OnInit, OnDestroy { compassHeading: number; calculatedHeading: number; private sub: Subscription; constructor(private deviceOrientation: DeviceOrientation, private screenOrientation: ScreenOrientation) { } ngOnInit(): void { this.sub = this.deviceOrientation.watchHeading().pipe(distinctUntilChanged()).subscribe({ next: (currentHeading: DeviceOrientationCompassHeading) => { this.compassHeading = currentHeading?.trueHeading; this.calculatedHeading = this.calcHeading(currentHeading, this.screenOrientation); }, error: (error) => console.error('Cannot subscribe to device orientation: ', error) } ); } ngOnDestroy() { this.sub?.unsubscribe(); } calcHeading(currentHeading: DeviceOrientationCompassHeading, screenOrientation: ScreenOrientation): number { if (currentHeading?.trueHeading) { const rotationDelta = this.getScreenRotationDelta(screenOrientation); const heading = currentHeading.trueHeading + rotationDelta; // limit to 360 return heading % 360; } return undefined; } getScreenRotationDelta(screenOrientation: ScreenOrientation): number { switch (screenOrientation.type) { case screenOrientation.ORIENTATIONS.LANDSCAPE_PRIMARY: return 90; case screenOrientation.ORIENTATIONS.PORTRAIT_SECONDARY: // I never get this value on android phone :-( return 180; case screenOrientation.ORIENTATIONS.LANDSCAPE_SECONDARY: return 270; default: return 0; } } }
期望计算值在设备旋转时保持稳定,但当前无法实现。请问在Ionic/Cordova中有什么解决方案?希望避免自定义插件。
方法1:直接使用screen.orientation.angle计算修正值
Screen Orientation插件的type属性可能因Android系统限制不更新,但angle属性通常能准确返回设备当前相对于原生竖屏的旋转角度(0/90/180/270)。直接利用这个角度作为修正值即可,无需依赖type判断。
修改getScreenRotationDelta方法:
getScreenRotationDelta(screenOrientation: ScreenOrientation): number { // 处理角度为负数的情况,确保结果在0-359范围内 return (screenOrientation.angle + 360) % 360; }
这个方案无需额外插件,改动最小,能直接解决PORTRAIT_SECONDARY无法获取的问题。
方法2:结合Device Motion传感器判断设备姿态
如果angle属性也不可靠,可以借助Device Motion插件获取设备的姿态数据(gamma/beta值),结合屏幕宽高判断横竖屏,再计算修正值:
- 安装Device Motion插件:
ionic cordova plugin add cordova-plugin-device-motion npm install @awesome-cordova-plugins/device-motion
- 修改组件代码:
import { Component, OnDestroy, OnInit } from '@angular/core'; import { distinctUntilChanged } from 'rxjs/operators'; import { DeviceOrientationCompassHeading } from '@awesome-cordova-plugins/device-orientation'; import { DeviceOrientation } from '@awesome-cordova-plugins/device-orientation/ngx'; import { ScreenOrientation } from '@awesome-cordova-plugins/screen-orientation/ngx'; import { DeviceMotion, DeviceMotionAccelerationData } from '@awesome-cordova-plugins/device-motion/ngx'; import { Subscription } from 'rxjs'; @Component({ selector: 'app-orientation', template: 'Heading: {{compassHeading}}/{{calculatedHeading}}' }) export class OrientationPage implements OnInit, OnDestroy { compassHeading: number; calculatedHeading: number; private rotationDelta = 0; private sub: Subscription; private motionSub: Subscription; constructor( private deviceOrientation: DeviceOrientation, private screenOrientation: ScreenOrientation, private deviceMotion: DeviceMotion ) { } ngOnInit(): void { // 监听罗盘数据 this.sub = this.deviceOrientation.watchHeading().pipe(distinctUntilChanged()).subscribe({ next: (currentHeading: DeviceOrientationCompassHeading) => { this.compassHeading = currentHeading?.trueHeading; this.calculatedHeading = this.calcHeading(currentHeading); }, error: (error) => console.error('Cannot subscribe to device orientation: ', error) } ); // 监听设备姿态,更新旋转修正值 this.motionSub = this.deviceMotion.watchAcceleration({ frequency: 500 }).subscribe((motionData: DeviceMotionAccelerationData) => { const isLandscape = window.innerWidth > window.innerHeight; const beta = motionData.beta; const gamma = motionData.gamma; // 判断是否倒置(beta值接近±180) const isUpsideDown = Math.abs(beta) > 160; if (isUpsideDown) { this.rotationDelta = 180; } else if (isLandscape) { // gamma>0为向右横屏,gamma<0为向左横屏 this.rotationDelta = gamma > 0 ? 90 : 270; } else { this.rotationDelta = 0; } }); } ngOnDestroy() { this.sub?.unsubscribe(); this.motionSub?.unsubscribe(); } calcHeading(currentHeading: DeviceOrientationCompassHeading): number { if (currentHeading?.trueHeading) { let heading = currentHeading.trueHeading + this.rotationDelta; // 限制在0-359范围内 heading = (heading + 360) % 360; return heading; } return undefined; } }
这种方案不依赖Screen Orientation插件的状态,通过原生传感器直接判断设备姿态,兼容性更强,但需要处理传感器噪声(比如添加数据平滑逻辑)。
方法3:强制应用支持所有屏幕方向(Android)
如果设备系统允许,修改应用配置让其支持所有屏幕方向,这样Screen Orientation插件就能正确返回PORTRAIT_SECONDARY等状态:
修改config.xml:
<!-- 允许所有方向 --> <preference name="orientation" value="all" /> <!-- 针对Android,添加fullSensor属性 --> <platform name="android"> <edit-config file="AndroidManifest.xml" target="/manifest/application/activity[@android:name='MainActivity']" mode="merge"> <activity android:screenOrientation="fullSensor" /> </edit-config> </platform>
注意:部分设备厂商可能限制了应用的倒置权限,这种方法在三星手机上可能因系统设置失效,需测试验证。
内容的提问来源于stack exchange,提问作者TomStroemer

