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 华为手机 可以自启,但是不会启动首页。启动首页无旋转,直接适配横屏参数。

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值