April 9, 2018

Android N多窗口适配

前言

从Android 7.0开始,系统就开始支持多窗口模式,对于多窗口模式的适配,主要是两方面,UI适配和生命周期的调整,适配的工作量因App而异,下面就从几个方面来谈谈多窗口模式的适配。

多窗口模式规则

这里要注意,多窗口模式分为两种,split-screen模式和freeform模式,前者是单纯的屏幕一分为二,而后者允许用户自由调整两个activity的大小。根据官方文档的说法,所有Android N的设备都是支持split-screen模式的,而freeform模式由厂商决定是否开启,一般屏幕比较大的设备都支持这个模式。
AndroidManifest.xml中的android:resizeableActivity属性和应用的targetSDK共同影响着应用在分屏模式下的行为,这里先讨论应用没有指定orientation的情况。如果应用的targetSDK是24及以上,那么android:resizeableActivity属性默认为true,应用默认支持多窗口模式,当然应用可以手动覆盖掉这个默认值。如果应用的targetSDK是23及其以下,那么要分为三种情况。一是这个属性没有被指定,那么在进入多窗口模式时,系统会强制缩放应用,并且弹出一个提示框,告诉用户当前应用在多窗口模式下可能表现异常;二是这个属性为false,那么这个应用无法进入多窗口模式,一定是以全屏状态启动的;三是这个属性被手动指定为true,那么应用将正常进入多窗口模式,并且不会有情况一中的提示框。
如果一个task栈中的根activity支持多窗口模式,那么这个栈中的所有activity都支持多窗口模式。这条规则就意味着,如果我们的应用launcher activity支持多窗口模式,那么所有可能由内部调用呼起的activity也必须适配多窗口模式。

orientation的影响

上一节只讨论了没有activity没有指定orientation的情况,其实orientation相关的属性,对应用在多窗口模式下的表现也有很大的影响。很多页面会指定android:screenOrientation属性来锁定屏幕方向,这个属性根据targetSDK的不同,在多窗口模式下会有截然不同的表现。如果targetSDK是23及以下,所有指定了这个属性的activity都不支持多窗口模式,只能以全屏状态启动。如果targetSDK是24及以上,那么即使指定了这个属性,也可以进入多窗口模式,但是在多窗口模式中,这个属性会被忽略。这里要注意的是,一旦进入了多窗口模式,那么在代码中就无法使用setRequestedOrientation() 来锁定屏幕方向了,无论targetSDK是什么版本都没有效果。
一些System UI相关的特性在多窗口模式下也是无效的,如果是在Manifest里配置的属性,可能还会影响应用无法进入多窗口模式,比如android:immersive,规则同android:screenOrientation。

多窗口模式的生命周期

多窗口模式的引入,并没有改变activity的生命周期,但是更强调了RESUMED状态和STARTED状态的区别。之前我对于STARTED状态的理解是,activity对用户可见,而RESUMED是获取了焦点。如果没有多窗口模式,那么这两个状态一般没有什么区别,但是在多窗口模式下,就必然会出现一个activity对用户可见,但是并没有获取焦点的情况。永远都只有一个activity会处于RESUMED状态,但是另一个activity即使只是处于STARTED状态,优先级还是比不可见的activity要高的。
当activity在多窗口模式和全屏状态下互相切换,或者freeform模式下被调整大小时,系统会给activity一个配置变化的通知,如果Manifest里没有监听对应的配置变化,那么activity就会被销毁重新创建,否则activity的onConfigurationChanged() 回调会被调用。

生命周期适配

生命周期适配主要是两方面,一是对于配置变化的处理,二是对于STARTED和RESUMED状态的处理。配置变化至少要监听如下四个:android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation",然后在onConfigurationChanged() 回调中,可以用isInMultiWindowMode() 方法来判断当前Activity是否处于多窗口模式,并作相应的处理。当用户在多窗口模式和全屏模式切换,或者调整窗口大小时,都会调用onConfigurationChanged()回调方法。如果是一些和用户有持续性交互的Activity,比如音频和视频等,那么要注意,开始和暂停播放要分别放在onStart()onStop() 回调中,而不是onResume()onPause() 中。

UI适配

因为freeform模式的引入,activity可用的窗口大小可以以任意比例变化,这就要求我们的布局是响应式的,能够适应不同的屏幕大小和比例。总结了一下,主要有以下几条建议:

  1. 避免hard-code的宽高,如果不是ConstraintLayout的话,尽量用wrap_content和match_parent来描述宽高。
  2. 如果是LinearLayout,可以尝试设置layout_weight来自适应屏幕,但是这样会降低性能,系统在layout时需要更多的计算,最好用ConstraintLayout替代。
  3. 使用最小宽度描述符来提供不同的layout,比如可以提供一个layout-sw320dp来适配正常非多窗口模式下的情况,这里要注意一下适配规则,swdp指定的数字,是宽和高的最小值,叫smallest possible width更准确一点。如果提供了多个不同数值的swdp,系统会使用最接近,但是没有超过设备屏幕大小的那一个。freeform模式下调整窗口大小会触发配置变化,系统会在这个时候调整使用的layout。
  4. 尽量使用fragment来实现UI相关的逻辑,这样可以尽可能地避免重复代码,在适配多种屏幕大小时复用现有的逻辑。
  5. 可能被拉伸的图片使用nine-patch切图。
  6. 最后一条,尽量使用ConstraintLayout吧,这个Layout基本上可以实现所有用LinearLayout,RelativeLayout和FrameLayout实现的布局,而且有助于减少View的层级。因为ConstraintLayout本身的机制,写出来的布局基本上天然是响应式的,可以减少很多适配工作。

layout标签

针对多窗口模式,Android还提供了<layout>标签,这个标签提供了5个属性用来控制activity在多窗口模式下的表现。android:defaultWidthandroid:defaultHeight用来指定在freeform模式下启动时的默认宽高,android:gravity用来指定在freeform模式下启动时activity的初始位置,android:minHeightandroid:minWidth用来指定在多窗口模式下activity的最小宽高,如果用户手动调整分界线导致窗口大小小于这个数值,当在split-screen模式下,activity将被裁剪,canvas依然维持minWidth和minHeight的大小,如果在freeform模式下,那么activity是无法被缩放到minWidth和minHeight以下的,这两个属性在freeform模式下更符合字面意思。

多窗口模式下启动activity

多窗口模式下,activity可以被启动在另一个窗口,但是要同时加两个flag:

Intent intent = new Intent(this, ActivityB.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT);
startActivity(intent);

这里一定要加FLAG_ACTIVITY_NEW_TASK,否则FLAG_ACTIVITY_LAUNCH_ADJACENT会被忽略。

适配验证

  1. 全屏模式下启动应用,然后切换到多窗口模式,看是否表现正常。
  2. 直接以多窗口模式启动应用,看是否表现正常。
  3. 多窗口模式下拖动分界线,应用不能崩溃,并且UI展示正常,这里还要关注UI重绘是否存在性能问题。
  4. 在指定了layout标签的时候,观察拖动分界线时的表现是否符合预期。
  5. 如果是和用户有持续交互的应用,比如音视频播放,浏览器等,检查多窗口模式下状态是否正常。
  6. 快速拖动分界线,检查是否存在内存泄漏。