Android页面方向、角度及旋转小结

手上的项目原本设计之初是基于手机竖屏的,但由于近期某些订制版本需要适配横屏和默认横屏设备,因此对页面的朝向和角度获取要进行一些研究。其中也踩了不少坑,这里拿出来给大家乐呵乐呵。

从传感器角度讲起

通常,我们会使用OrientationEventListener来获取传感器角度,其一般用法如下:

new OrientationEventListener(this, SensorManager.SENSOR_DELAY_NORMAL){
@Override
public void onOrientationChanged(int orientation) {
if (orientation > 350 || orientation < 10) { //0度
} else if (orientation > 80 && orientation < 100) { //90度
} else if (orientation > 170 && orientation < 190) { //180度
} else if (orientation > 260 && orientation < 280) { //270度
}
}
}.enable();

当设备平放时,将返回一个无效值,当设备垂直桌面且与自然状态(即开机时页面的默认方向)一致时,返回0度,所谓自然角度是指系统在编译之初设定的默认角度,有兴趣的同学可以搜索“修改Android系统默认横竖屏”,这里不做赘述。通常对于手机,正常竖屏垂直于桌面时返回0度,对于默认横屏设备则是横屏垂直于桌面时返回0度。

这个角度我在项目中用于对相机回调数据进行旋转、SurfaceView绘制等地方。

Surface.ROTATION

关于页面方向存在两种定义,一种是由Surface类中的声明的ROTATION:

/**
* 转动0度 (自然角度)
*/
public static final int ROTATION_0 = 0;
/**
* 转动90度
*/
public static final int ROTATION_90 = 1;
/**
* 转动180度
*/
public static final int ROTATION_180 = 2;
/**
* 转动270度
*/
public static final int ROTATION_270 = 3;

顾名思义,它是一种旋转角度。而其中的ROTATION_0在注释中(这里为了方便阅读已经翻译,下同)已指明它是页面的自然角度。

当设备平放时,不论启动时默认是横屏还是竖屏状态,它总是以ROTATION_0状态首先展示,其余三个值都是以此为参照进行的旋转角度。

当锁定屏幕方向时,转动手机只会造成传感器角度变化,ROTATION并不会发生变化,这意味着ROTATION总是与页面方向绑定而不与传感器方向绑定。它们的关系仅停留在自然状态下(比如手机竖直放置)ROTATION_0与传感器的0度角一致。

因此我们虽然可以通过getWindowManager().getDefaultDisplay().getRotation();方法来获取当前页面的ROTATION值,但不能以此反推传感器角度,也不能获取屏幕横竖屏状态。

在实际使用时,我用作矫正相机预览角度。

SCREEN_ORIENTATION

上文的另一种定义则是在ActivityInfo中声明的SCREEN_ORIENTATION,我们可以将其与manifest.xml中activity的android:screenOrientation属性相匹配,共有16种,这里我们只取采坑踩到的4种:

/**
* 页面横屏
*/
public static final int SCREEN_ORIENTATION_LANDSCAPE = 0;
/**
* 页面竖屏
*/
public static final int SCREEN_ORIENTATION_PORTRAIT = 1;
/**
* 页面横屏翻转
*/
public static final int SCREEN_ORIENTATION_REVERSE_LANDSCAPE = 8;
/**
* 页面竖屏翻转
*/
public static final int SCREEN_ORIENTATION_REVERSE_PORTRAIT = 9;

顾名思义,它是指页面方向,并且这四个值一定是与横竖屏相关联的,ROTATION_0可以是横屏也可以是竖屏,但SCREEN_ORIENTATION_PORTRAIT只会是竖屏,其它同理。

实际开发时我们通过Activity.setRequestedOrientation(int);来手动设置页面方向,虽然可以通过Activity.getRequestedOrientation();来获取方向, 但其返回值受到manifest.xml的配置和上一次手动设置的影响,我们拿到的返回值可能并不是上述4个值之一,而是SCREEN_ORIENTATION_UNSPECIFIED(默认,指跟随系统)、SCREEN_ORIENTATION_SENSOR(跟随传感器)等其它取值,并不能知道其具体朝向。

与Surface.ROTATION相同的是,它也只有一个默认方向在自然角度时是与传感器的0度角相匹配的,不同的是正常来说我们无法获知这个方向的具体值。

错误示范

我的需求是将原本竖屏的应用调整为横屏设备上只允许横屏,竖屏设备只有视频页面允许横竖屏切换且两种状态下界面不一致,在我已经用到了传感器角度的情况下,我很自然的想到能否根据传感器角度主动切换页面具体方向。

于是我在Manifest.xml中把页面方向配置为screenOrientation=‘nosener’,同时根据角度切调用

setRequestedOrientation()设置上面四个值,在手机上由于0度角一定匹配SCREEN_ORIENTATION_PORTRAIT,其余三个方向以此推算,是可以实现的,但到横屏设备上就有问题了,尤其以下两点:

横屏设备的默认朝向不一致,可能是LANDSCAPE也可能是REVERSE_LANDSCAPE,无法对应具体的角度,因此也无法推算其它三个朝向的角度。

部分横屏设备屏蔽SCREEN_ORIENTATION_REVERSE_LANDSCAPE(实际部分手机也屏蔽SCREEN_ORIENTATION_PORTRAIT),因此强制固定一个角度也不可取。

综上,想在所有设备上适用是不可行的,所以还是老老实实的抛开传感器角度,单独使用setRequestedOrientation(int);来实现需求。

方案修正

这次抛开传感器角度交给系统处理,首先我们判断设备是否默认横屏以区分效果,这里Manifest.xml中就不再配置页面方向了,也就是默认的userLandscape,在setContentView调用之前我们通过以下代码判断设备方向:

/**
* 判断设备是否默认横屏设备
*/
public static boolean isLandDevice(Activity activity){
boolean bLand = false;
int orientation = activity.getResources().getConfiguration().orientation;
switch (activity.getWindowManager().getDefaultDisplay().getRotation()){
case Surface.ROTATION_0:
case Surface.ROTATION_180:
bLand = orientation == Configuration.ORIENTATION_LANDSCAPE;
break;
case Surface.ROTATION_90:
case Surface.ROTATION_270:
bLand = orientation != Configuration.ORIENTATION_LANDSCAPE;
break;
default:
break;
}
return bLand;
}

代码很简单不再多说了,拿到默认横竖屏状态之后,我们再来设置页面的朝向。

/**
* 切换页面方向
* @param bLandDevice 是否是默认横屏设备
* @param bVideoPage 是否是视频页
*/
protected void setOrientation(boolean bLandDevice, boolean bVideoPage){
if (bLandDevice){
//Android 4.3及以上允许跟随系统的横屏切换
if (DeviceUtil.isAndroidJELLY_BEAN_MR2()) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE);
} else {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
}
} else {
//视频界面允许全角度
if (bVideoPage){
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
} else {
//部分手机屏蔽竖屏翻转,这里强制只有竖屏
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
}
}

目标达成,所以花费了那么多时间到底是为嘛呢,想法太多也不是好事,还是多多走正道比较好,希望大家也能少走些弯路。

值得注意的问题

页面的横竖屏切换是会导致Acitivity重启的,如果不希望重启,在Manifest.xml的activity属性中增加android:configChanges="orientation|screenSize|keyboardHidden|locale"即可。

使用上一条在切换后会回调onConfigurationChanged(Configuration newConfig)方法,其参数只包含了横竖屏状态,没有具体朝向,同时只是横屏切横屏或竖屏切竖屏不会被回调。

相机预览角度必须在切换横竖屏状态后重新设置,此时Surface.ROTATION发生了变化。

额外的知识

上文中用到了Context.getResources().getConfiguration().orientation;

众所周知,它通常只有Configuration.ORIENTATION_LANDSCAPE和Configuration.ORIENTATION_PORTRAIT两个取值分别代表横竖屏状态。

由于使用场景通常在onConfigurationChanged中,因此会忽略它实际并不是当前页面的横竖屏状态,而是整个设备最顶端页面的横竖屏状态,因此用来检测并根据横竖屏状态切换后台资源是非常可靠的,例如录屏场景下自动切换横竖屏分辨率。