屏幕旋转一般的解决方案
关于屏幕旋转这里,之前一直没太注意,因为根据设备会有指定的屏幕旋转策略如:
- 开发手机应用时一直使用强制竖屏布局
- 开发平板设备一直使用横屏布局
- 开发系统应用,一般给两套即横竖各一套(一般不使用适配框架)
其实这三种模式一般来说,可以为我们规避大量的问题!但是总有一些特殊情况,让我们摸不清头脑。所以这次我就从头到尾的做一次实验。将屏幕旋转这里的知识点和要点归纳出来。
推荐写法
先上推荐写法,不用看原理,也不用知道什么切换场景的。可以直接按照建议和自己的需求复制粘贴即可。
- 旋转后是否希望Activity不被销毁,希望不被销毁添加如下属性;希望被销毁不添加configChanges属性。
android:configChanges="orientation|screenSize"
- 如果是不销毁的策略在onConfigurationChanged可以做一个参数的监听,如果发现和onCreate时的屏幕宽高不一致,需要刷新适配框架的适配参数(如AutoSize),然后刷新布局。注入的数据和逻辑需要自行处理。
AutoSizeCompat.autoConvertDensityOfGlobal(resources)
setContentView(R.layout.activity_main)
事前准备
适配框架使用:
implementation 'me.jessyan:autosize:1.2.1'
<meta-data
android:name="design_width_in_dp"
android:value="600"/>
布局文件:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_centerInParent="true"
android:gravity="center"
android:text="Hello World!"
android:background="#000000"
android:textColor="#ffffff"
/>
</RelativeLayout>
Activity文件:
class MainActivity : AppCompatActivity() {
val TAG: String = MainActivity::javaClass.name.toString();
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
getScreenSize()
Log.e(TAG, "onCreate current 100 dp is ="+AutoSizeUtils.dp2px(this,100.0f))
}
fun getScreenSize(){
val display: Display = getWindowManager().getDefaultDisplay()
val outSize = Point()
display.getSize(outSize)
val x: Int = outSize.x
val y: Int = outSize.y
Log.e(TAG, "Current screen size widtth="+x+" height="+y)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
getScreenSize()
Log.e(TAG, "onConfigurationChanged current 100 dp is ="+AutoSizeUtils.dp2px(this,100.0f))
}
override fun onResume() {
super.onResume()
Log.e(TAG,"onResume")
}
override fun onStart() {
super.onStart()
Log.e(TAG,"onStart")
}
override fun onPause() {
super.onPause()
Log.e(TAG,"onPause")
}
override fun onDestroy() {
super.onDestroy()
Log.e(TAG,"onDestroy")
}
}
测试系统版本
Android 7.0 第五部分会给出多系统的测试结论,和Android7.0作为对比。
一、Activiy即不声明指定方向也不添加configChanges
预置代码
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
测试条件
开启自动旋转,竖屏进入,然后进入横屏,再进入竖屏。
下面我们来看下具体发生了那些事情:
//竖屏启动
Current screen size widtth=800 height=1216
onCreate current 100 dp is =133
savedInstanceState =null
onStart
onResume
//切换横屏
Current screen size widtth=1216 height=800
onConfigurationChanged current 100 dp is =200
onDestroy
Current screen size widtth=1216 height=800
onCreate current 100 dp is =203
onStart
onResume
//再次切换回竖屏
Current screen size widtth=800 height=1216
onConfigurationChanged current 100 dp is =200
onPause
onDestroy
Current screen size widtth=800 height=1216
onCreate current 100 dp is =133
onStart
onResume
测试结论
从上面我们可以看出,无论是横屏切竖屏还是竖屏切横屏切换后的生命流程都是:
- 屏幕宽高改变->调用onConfigurationChanged
- 当前的Activity会被销毁!然后从新走onCreate流程,此时显示新的适配结果。
这里面需要注意的点有两个:
-
第一次竖屏切横屏时,在 onConfigurationChanged 这里换算参数是变化的。但是相同屏幕分辩率下同样是100dp,计算出来的值是不一样的!切换后的值为200px,横屏显示时是203px。
-
第二次横屏切竖屏时,在onConfigurationChanged 这里换算参数是也是有变化的,不过从换算结果可以看出,并不是竖屏下的换算结果(竖屏下为133px),而是第一次切换后的结果(此时为200px)。
由以上可知:在onConfigurationChanged这个阶段,我们直接靠AutoSize自身监听屏幕旋转后刷新换算比例是不可靠的!
备注:AutoSize在初始化的时候监通过Application监听了onConfigurationChanged然后更改了换算比例。
意见
上面的实验场景一般在Activity内没有耗时的数据操作时是可以接受的。但是一旦有耗时或者较大的数据缓存时,就变得不可接受了,因为按照生命周期重新装载一遍数据显然是很糟糕的用户体验。
当有耗时的数据操作时我们就需要保存现场数据,在旋转后,直接把数据再次注入。
那么我首先想到的是使用:onSaveInstanceState 保存数据环境。
但是此时onSaveInstanceState 是不能起作用的,因为不会被调用!旋转导致的Activity销毁不被认为是异常销毁,所以想通过onSaveInstanceState 保存当前的页面数据是行不通的。
换句话说,此时我们如果还想恢复数据,就需要使用持久化的数据缓存,不能再依赖于Activity自身的方法了。
二、Activity#configChanges 选项 orientation
预置代码
<activity
android:name=".MainActivity"
android:configChanges="orientation">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
测试条件
开启自动旋转,竖屏进入,然后进入横屏,再进入竖屏。
测试结果
此时和场景一,测试结果完全相同。
三、Activity#configChanges 选项 orientation|screenSize
预置代码
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
测试条件
开启自动旋转,竖屏进入,然后进入横屏,再进入竖屏。
测试结论
这时我们再看一下日志:
//竖屏启动
Current screen size widtth=800 height=1216
onCreate current 100 dp is =133
savedInstanceState =null
onStart
onResume
//切换横屏
Current screen size widtth=1216 height=800
onConfigurationChanged current 100 dp is =200
//横屏切回竖屏
Current screen size widtth=800 height=1216
onConfigurationChanged current 100 dp is =200
此时我们发现,在屏幕进行切换时,我们的Activity生命周期并没有被调用。被调用的只有onConfigurationChanged 。此时我们可以看到,屏幕旋转后,返回的屏幕分辨率是对的,但是基于AutoSize的换算出错了。同样的100dp在不同宽高下返回了相同的值。
此时产生了BUG,你会发现,旋转前后,控件的大小没有产生变化。
解决BUG
废话不多说直接上代码:
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
getScreenSize()
AutoSizeCompat.autoConvertDensityOfGlobal(resources)
Log.e(TAG, "onConfigurationChanged current 100 dp is ="+AutoSizeUtils.dp2px(this,100.0f))
setContentView(R.layout.activity_main)
}
这里面主要解决了两个问题:
-
换算结果不正确的问题。这个由
AutoSizeCompat.autoConvertDensityOfGlobal(resources)
来解决,即刷新当前的全局换算参数。根据测试结果,在屏幕旋转后,调用此方法后,再进行换算,此时的换算结果和onCreate中输出的换算结果一致。 -
横竖屏来回切换,切换后控件大小不改变。这个由
setContentView(R.layout.activity_main)
来解决。简单来说,此时就是更新AutoSize全局换算参数后,重新进行布局。经测试此时的控件大小变化正常。
意见
如果我们使用AutoSize进行屏幕适配时,又涉及到了横竖屏切换的功能,那么此时建议,在 onConfigurationChanged 回调中增加 AutoSizeCompat.autoConvertDensityOfGlobal(resources) 以此来刷新当前的全局换算参数。当我们不希望Activity被销毁时,需要手动的在新参数下刷新布局。
四、复杂场景
1. 应用强制设置为横屏,开启开机自启功能,重启后锁屏页为竖屏应用。在应用启动后,解锁。此场景也会触发横竖屏切换。
预置代码:
<activity
android:name=".MainActivity"
android:screenOrientation="landscape"
android:configChanges="orientation|screenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
增加开机自启,JAVA代码为解决BUG后的代码。
测试条件
重启机器,重启完成后不做任何操作,让屏幕进入竖屏锁屏状态。观察日志,待日志输出自启动信息后,解锁查看完整日志。
测试结论
还是先看下日志:
E/BootReceiver: 自启动了 !!!!!
E/javaClass: Current screen size widtth=800 height=1216
E/javaClass: onCreate current 100 dp is =133
E/javaClass: onStart
E/javaClass: onResume
E/javaClass: onPause
//解锁后的日志
E/javaClass: onStart
E/javaClass: onResume
E/javaClass: Current screen size widtth=1216 height=800
E/javaClass: onConfigurationChanged current 100 dp is =203
由以上日志可以看出,虽然我们设置了强制横屏,但是在以上场景中,我们的页面却是按竖屏启动的,并且进行了竖屏的参数适配。当我们解锁后,屏幕发生旋转,这个时候回调 onConfigurationChanged 更换宽高,刷新了适配参数。同样为了解决之前的问题,我们需要刷新布局。
2. 请求系统相机,系统相机为竖屏拍照,然后返回
此场景可理解为我们的应用请求外部应用功能,外部应用和我们的屏幕方向不一致
还是先上日志:
E/javaClass: Current screen size widtth=1216 height=800
E/javaClass: onCreate current 100 dp is =203
E/javaClass: savedInstanceState =null
E/javaClass: onStart
E/javaClass: onResume
E/javaClass: onPause
E/javaClass: onResume
E/javaClass: onPause
E/javaClass: onStart
E/javaClass: onResume
从上面可以看出,我们在竖屏下启动我们强制横屏的应用,启动后屏幕的适配参数是正确的。
和开机自启不一样的地方在,在竖屏拍照完成后,onActivityResult的时候虽然我们的屏幕发生了旋转,但是并没用调用onConfigurationChanged,同样的我们的布局也没有发生变化。
3. Android 8.1 系统在某主题下,横屏请求系统相机竖屏拍照后旋转两次
说这个问题之前要先讲一下Android8.0上存在一个特别坑的问题。
Only fullscreen opaque activities can request orientation
这个问题为什么会和旋转扯上关系那,事情是这样的。在Android 8.1上,以请求拍照这个功能举例,onResult 会发现屏幕连续旋转两次,但是在 Android 8.0 上就不会。开始的时候是在8.1上开发的,安装到 8.0 上后回崩溃,然后我们解决了上面的问题,而上面问题的解决间接的帮我们解决了 8.1 上拍照后旋转两次的问题 。
针对解决这个问题,我们付出了很多的精力,主要是这里并不报错,也没有找到很好的触发条件,但是解决这个问题的时候我们很幸运,通过一个尝试找到了解决方案,又根据不同的条件进行设置,找到了起作用的代码,当然以上都是在没有理论支撑的情况下,所以特意把这种场景列出来方便大家参考。
首先我们先看下:Only fullscreen opaque activities can request orientation
必现条件:
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize"
android:screenOrientation="landscape"
android:theme="@style/ActivityTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<style name="ActivityTheme" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowIsTranslucent">false</item>
</style>
没错仅需要这两个条件即可在8.0上发生这个崩溃。
官方源码判定条件:
根据崩溃日志:
Caused by: java.lang.IllegalStateException: Only fullscreen opaque activities can request orientation
at android.app.Activity.onCreate(Activity.java:1081)
at androidx.core.app.ComponentActivity.onCreate(ComponentActivity.java:85)
at androidx.activity.ComponentActivity.onCreate(ComponentActivity.java:149)
at androidx.fragment.app.FragmentActivity.onCreate(FragmentActivity.java:313)
at androidx.appcompat.app.AppCompatActivity.onCreate(AppCompatActivity.java:115)
at com.xjl.screenrotationtest.MainActivity.onCreate(MainActivity.kt:28)
可以知道,崩溃点发生在Activity的onCreate方法中。
第一层判定条件:
- 适配的版本大于26
- 请求了屏幕方向
再看一下 isTranslucentOrFloating 的判定源码:
这里面的判定条件有三个都和Style有关:
- 当前Window是否是透明的 windowIsTranslucent
- 当前Window是否是右滑退出 windowSwipeToDismiss
- 当前Window是否是悬浮的 windowIsFloating
如果你抱有怀疑态度,那么我已经为大家实验过了,实验结果非常的准确。
只要我们设置了屏幕方向,并在Style中出现以上三个属性之一并设置为true时就会发生这个崩溃。
解决方案:
- 适配版本不大于26(显然不合适)
- 不设置
- windowIsTranslucent
- windowSwipeToDismiss
- windowIsFloating
为 true
注意事项:
虽然报错信息是 Only fullscreen opaque activities can request orientation 但是从源码中我们可以知道和是否全屏是没有关系的!
经测试适配版本大于26时不管是否开启windowFullscreen属性只要开启windowIsTranslucent,并请求方向就会崩溃!
Android 8.1上的表现
Android8.1 PAD 不崩溃 无异常
Android8.1 1+ 手机不崩溃,但是会发生旋转 onCreate 中会按竖屏适配。onConfigurationChanged按照横屏适配。
Android 10.0 上的表现
Android10 华为手机 不崩溃 无异常
五、多系统测试结果
测试结果与Android7.0系统机型比较一致的不在赘述,不一致的会在下面单独的列出。
多系统测试结果对照表
系统版本 | 场景一 | 场景二 | 场景三 | 复杂场景一 | 复杂场景二 |
---|---|---|---|---|---|
Android 8.0 | 一致 | 不一致1 | 一致 | 无法自启 | 一致 |
Android 8.1 PAD | 一致 | 一致 | 一致 | 一致 | 不一致2 |
Android 8.1 1+手机 | 一致 | 一致 | 一致 | 不一致3 | 一致 |
Android 10.0 | 一致 | 一致 | 一致 | 不一致4 | 一致 |
不一致1
Android 8.0 场景二不一致现象:没有销毁当前Activity,而是在每次屏幕旋转的时候只调用了onConfigurationChanged。
不一致2
Android 8.1 PAD 复杂场景二不一致现象:不设置style时会发生2次旋转。
//横屏启动
E/javaClass: Current screen size widtth=1812 height=1200
E/javaClass: onCreate current 100 dp is =302
E/javaClass: onStart
E/javaClass: onResume
//启动系统相机 横屏
E/javaClass: onPause
//拍照后回调 横屏页面
E/javaClass: onStart
E/javaClass: onResume
E/javaClass: Current screen size widtth=1200 height=1812
E/javaClass: onConfigurationChanged current 100 dp is =200
E/javaClass: Current screen size widtth=1812 height=1200
E/javaClass: onConfigurationChanged current 100 dp is =302
E/javaClass: onPause
这里我们可以看到,屏幕连续旋转了两次,显示旋转为竖屏,后有旋回横屏!
此处不排除测试PAD系统问题。
和 Only fullscreen opaque activities can request orientation 应该无关
添加属性
- windowIsTranslucent
- windowSwipeToDismiss
- windowIsFloating
- windowFullscreen
为true后不发生旋转!
不一致3
Android 8.1 1+ 手机 开启自启后 直接适配 横屏适配参数。
不一致4
Android 10.0 华为手机 可以自启,但是不会启动首页。启动首页无旋转,直接适配横屏参数。