Android 屏幕旋转 全解析

屏幕旋转一般的解决方案

关于屏幕旋转这里,之前一直没太注意,因为根据设备会有指定的屏幕旋转策略如:

  1. 开发手机应用时一直使用强制竖屏布局
  2. 开发平板设备一直使用横屏布局
  3. 开发系统应用,一般给两套即横竖各一套(一般不使用适配框架)

其实这三种模式一般来说,可以为我们规避大量的问题!但是总有一些特殊情况,让我们摸不清头脑。所以这次我就从头到尾的做一次实验。将屏幕旋转这里的知识点和要点归纳出来。

推荐写法

先上推荐写法,不用看原理,也不用知道什么切换场景的。可以直接按照建议和自己的需求复制粘贴即可。

  1. 旋转后是否希望Activity不被销毁,希望不被销毁添加如下属性;希望被销毁不添加configChanges属性。
android:configChanges="orientation|screenSize"
  1. 如果是不销毁的策略在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

测试结论

从上面我们可以看出,无论是横屏切竖屏还是竖屏切横屏切换后的生命流程都是:

  1. 屏幕宽高改变->调用onConfigurationChanged
  2. 当前的Activity会被销毁!然后从新走onCreate流程,此时显示新的适配结果。

这里面需要注意的点有两个

  1. 第一次竖屏切横屏时,在 onConfigurationChanged 这里换算参数是变化的。但是相同屏幕分辩率下同样是100dp,计算出来的值是不一样的!切换后的值为200px,横屏显示时是203px。

  2. 第二次横屏切竖屏时,在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)
    }

这里面主要解决了两个问题:

  1. 换算结果不正确的问题。这个由AutoSizeCompat.autoConvertDensityOfGlobal(resources)来解决,即刷新当前的全局换算参数。根据测试结果,在屏幕旋转后,调用此方法后,再进行换算,此时的换算结果和onCreate中输出的换算结果一致。

  2. 横竖屏来回切换,切换后控件大小不改变。这个由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方法中。
在这里插入图片描述
第一层判定条件:

  1. 适配的版本大于26
  2. 请求了屏幕方向

再看一下 isTranslucentOrFloating 的判定源码:
在这里插入图片描述
这里面的判定条件有三个都和Style有关:

  1. 当前Window是否是透明的 windowIsTranslucent
  2. 当前Window是否是右滑退出 windowSwipeToDismiss
  3. 当前Window是否是悬浮的 windowIsFloating

如果你抱有怀疑态度,那么我已经为大家实验过了,实验结果非常的准确。

只要我们设置了屏幕方向,并在Style中出现以上三个属性之一并设置为true时就会发生这个崩溃。

解决方案:

  1. 适配版本不大于26(显然不合适)
  2. 不设置
  • 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 华为手机 可以自启,但是不会启动首页。启动首页无旋转,直接适配横屏参数。

猜你喜欢

转载自blog.csdn.net/u010451990/article/details/109155182