屏幕支持

我们对这些与屏幕相关的部分进行了更新,详情见下文:

调整 activity 和屏幕的尺寸

为指明某个应用可能不支持多窗口模式或调整尺寸,activity 使用 resizeableActivity=false 属性。在调整 activity 尺寸时,应用遇到的常见问题包括:

  • activity 可以具有与应用或其他非视觉组件不同的配置。一个很常见的错误是从应用环境中读取屏幕指标。系统不会根据用于显示 activity 的可见区域指标来调整返回的值。
  • activity 可能不会处理调整尺寸和崩溃问题、显示失真界面,或者由于在未保存实例状态的情况下重新启动而丢失状态。
  • 应用可能会尝试使用绝对输入坐标(而不是相对于窗口位置的坐标),这可能会破坏多窗口模式下的输入内容。

在 Android 7(及更高版本)中,您可以将应用设置为 resizeableActivity=false,使其始终在全屏模式下运行。在这种情况下,Android 平台会阻止不可调整尺寸的 Activity 进入分屏模式。如果用户尝试在分屏模式下从启动器调用不可调整尺寸的 activity,Android 平台会退出分屏模式,并在全屏模式下启动不可调整尺寸的 activity。

在清单中明确将该属性设置为 false 的应用不得在多窗口模式下启动,除非应用了兼容模式:

  • 相同的配置将应用于该进程,其中包含所有 activity 组件和非 activity 组件。
  • 已应用的配置符合应用兼容屏幕的 CDD 要求。

Android 10 平台仍然会阻止不可调整尺寸的 activity 进入分屏模式,但如果 activity 声明了固定的屏幕方向或宽高比,则可以暂时调整尺寸。如果没有声明,activity 会调整自身尺寸以占满整个屏幕,正如在 Android 9 及更低版本中一样。

默认实现会应用以下政策:

如果通过使用 android:resizeableActivity 属性声明某个 activity 与多窗口模式不兼容,且该 activity 满足下面描述的条件之一,那么当已应用的屏幕配置必须要更改时,activity 和进程将与原始配置一起保存,并且系统会向用户提供一种方式来重新启动应用进程以使用更新后的屏幕配置。

  • 通过应用 android:screenOrientation 固定屏幕方向
  • 通过定位 API 级别或明确声明宽高比,应用具有默认的最大或最小宽高比

此数据显示了已声明宽高比的不可调整尺寸的 activity。折叠设备时,窗口尺寸会按比例缩小以适合相应区域,同时使用适当的信箱模式保持宽高比。此外,每次更改 activity 的显示区域时,系统都会向用户提供重启 activity 的选项。

展开设备时,activity 的配置、尺寸和宽高比不会发生变化,但系统会显示重启 activity 的选项。

如果未设置 resizeableActivity(或将其设置为 true),应用完全支持调整尺寸。

实现

具有固定屏幕方向或宽高比的不可调整尺寸的 activity 在代码中称为尺寸兼容模式 (SCM)。该条件是在 ActivityRecord#shouldUseSizeCompatMode() 中定义的。启动 SCM activity 时,屏幕相关配置(例如尺寸或密度)在请求的覆盖配置中是固定的,因此该 activity 不再依赖于当前的屏幕配置。

如果 SCM activity 无法占满整个屏幕,则会与顶部对齐且水平居中。Activity 边界通过 AppWindowToken#calculateCompatBoundsTransformation() 计算得出。

如果 SCM activity 使用与其容器不同的屏幕配置(例如,调整了屏幕尺寸或将 activity 移至其他屏幕),则 ActivityRecord#inSizeCompatMode() 为 true,并且 SizeCompatModeActivityController(位于系统界面中)会收到回调以显示进程重启按钮。

屏幕尺寸和宽高比

Android 10 支持新的宽高比,既包括高宽高比的长屏幕和薄屏幕,也包括 1:1 的屏幕。应用可以定义自身能够处理的屏幕的 ApplicationInfo#maxAspectRatioApplicationInfo#minAspectRatio

Android 10 中的应用宽高比

图 1. Android 10 支持的应用宽高比示例

设备实现可以具有尺寸和分辨率小于 Android 9 所需及更低的辅助屏幕(宽度或高度最小值为 2.5 英寸,smallestScreenWidth 最小值为 320dp),但只有选择支持这些小型屏幕的 activity 才可以执行。

应用可以通过声明小于或等于目标屏幕尺寸的最小支持尺寸来选择支持。使用 AndroidManifest 中的 android:minHeightandroid:minWidth Activity 布局属性来执行此操作。

屏幕政策

Android 10 将某些屏幕政策从 PhoneWindowManager 的默认 WindowManagerPolicy 实现中分离出来并移至每屏幕类,例如:

  • 屏幕状态和旋转
  • 某些键和动作事件跟踪
  • 系统界面和装饰窗口

在 Android 9(及更低版本)中,PhoneWindowManager 类处理了屏幕政策、状态和设置、旋转、装饰窗口框架跟踪等。Android 10 将大部分内容移至 DisplayPolicy 类,旋转跟踪除外(移至 DisplayRotation)。

屏幕窗口设置

在 Android 10 中,可配置的每屏幕窗口设置已经过扩展,目前包含以下内容:

  • 默认屏幕窗口模式
  • 过扫描值
  • 用户旋转和旋转模式
  • 强制的尺寸、密度和缩放模式
  • 内容移除模式(移除屏幕时)
  • 对系统装饰和 IME 的支持

DisplayWindowSettings 类包含这些选项的设置。每次设置发生更改时,它们都将被保存到 display_settings.xml/data 分区中的磁盘内。如需了解详情,请参阅 DisplayWindowSettings.AtomicFileStorageDisplayWindowSettings#writeSettings()。设备制造商可以在 display_settings.xml 中为其设备配置提供默认值。不过,由于文件存储在 /data 中,如果被擦除操作清空,则可能需要额外的逻辑来恢复文件。

默认情况下,Android 10 在保存这些设置时使用 DisplayInfo#uniqueId 作为屏幕的标识符。应该为所有屏幕填充 uniqueId。此外,它对于物理和网络屏幕都很稳定。您也可以使用物理屏幕的端口作为标识符,该标识符可以在 DisplayWindowSettings#mIdentifier 中设置。每次写入时,系统都会写入所有设置,因此可以放心更新用于存储中屏幕条目的键。如需了解详情,请参阅静态屏幕标识符

由于历史原因,设置将保存在 /data 目录中。最初,该目录用于保存用户设定的设置,例如屏幕旋转。

静态屏幕标识符

Android 9(及更低版本)没有为框架中的屏幕提供稳定的标识符。向系统添加某个屏幕后,系统会通过递增静态计数器为该屏幕生成 Display#mDisplayIdDisplayInfo#displayId。如果系统添加屏幕后又移除了同一屏幕,则会生成不同的 ID。

如果设备在启动时有多个可用屏幕,系统会根据时间为这些屏幕分配不同的标识符。虽然 Android 9(及更低版本)包含 DisplayInfo#uniqueId,但它没有足够的信息来区分各个屏幕,因为物理屏幕被标识为 local:0local:1 以表示内置和外部屏幕。

Android 10 更改了 DisplayInfo#uniqueId,以添加稳定的标识符并区分本地、网络和虚拟屏幕。

屏幕类型 格式
本地

local:<stable-id>
网络

network:<mac-address>
虚拟

virtual:<package-name-and-name>

除了对 uniqueId 的更新之外,DisplayInfo.address 还包含 DisplayAddress,这是一种在重新启动过程中保持稳定的屏幕标识符。在 Android 10 中,DisplayAddress 支持物理和网络屏幕。DisplayAddress.Physical 包含一个稳定的屏幕 ID(与 uniqueId 中的相同),可以使用 DisplayAddress#fromPhysicalDisplayId() 进行创建。

Android 10 还提供了一种获取端口信息的便捷方法 (Physical#getPort())。您可以在框架中使用此方法来静态标识屏幕。例如,该方法在 DisplayWindowSettings 中使用。DisplayAddress.Network 包含 MAC 地址,可以使用 DisplayAddress#fromMacAddress() 进行创建。

借助这些新增功能,设备制造商可以在静态多屏幕设置中标识各种屏幕,还可以使用静态屏幕标识符(例如物理屏幕的端口)配置不同的系统设置和功能。这些方法处于隐藏状态,仅限在 system_server 中使用。

根据 HWC 屏幕 ID(可能是不透明的且并非始终稳定),此方法会返回(特定于平台的)8 位端口号,该端口号可标识用于屏幕输出的物理连接器,以及屏幕的 EDID blob。SurfaceFlinger 从 EDID 中提取制造商或型号信息,用于生成提供给框架的稳定 64 位屏幕 ID。如果此方法不受支持或出现错误,SurfaceFlinger 将回退到旧版 MD 模式,其中 DisplayInfo#address 为 null 且 DisplayInfo#uniqueId 经过硬编码,如上所述。

如需验证此功能是否受支持,请运行以下命令:

$ dumpsys SurfaceFlinger --display-id
# Example output.
Display 21691504607621632 (HWC display 0): port=0 pnpId=SHP displayName="LQ123P1JX32"
Display 9834494747159041 (HWC display 2): port=1 pnpId=HWP displayName="HP Z24i"
Display 1886279400700944 (HWC display 1): port=2 pnpId=AUS displayName="ASUS MB16AP"

使用两个以上的屏幕

在 Android 9 及更低版本中,SurfaceFlinger 和 DisplayManagerService 假设最多存在两个物理屏幕,其硬编码 ID 分别为 0 和 1。

从 Android 10 开始,SurfaceFlinger 可以利用 Hardware Composer (HWC) API 生成稳定的屏幕 ID,使其能够管理任意数量的物理屏幕。如需了解详情,请参阅静态屏幕标识符

在从 SurfaceControl#getPhysicalDisplayIdsDisplayEventReceiver 热插拔事件获取 64 位屏幕 ID 后,框架可以通过 SurfaceControl#getPhysicalDisplayToken 查找物理屏幕的 IBinder 令牌。

在 Android 10 及更低版本中,主内部屏幕为 TYPE_INTERNAL,所有辅助屏幕都将标记为 TYPE_EXTERNAL(无论连接类型如何)。因此,其他内部屏幕被视为外部屏幕。临时解决方法是,如果 HWC 已知且端口分配逻辑可预测,特定于设备的代码可以对 DisplayAddress.Physical#getPort 作出假设。

Android 11(及更高版本)中已取消此限制。

  • 在 Android 11 中,启动期间报告的第一个屏幕是主屏幕。这与连接类型是内部还是外部并不相关。不过,主屏幕是不能断开连接的,由此可见,实际操作中的主屏幕必须是内部屏幕。请注意,一些可折叠手机有多个内部屏幕。
  • 辅助屏幕已正确分类为 Display.TYPE_INTERNALDisplay.TYPE_EXTERNAL(以前分别称为 Display.TYPE_BUILT_INDisplay.TYPE_HDMI),具体取决于连接类型。

实现

在 Android 9 及更低版本中,屏幕使用 32 位 ID 表示,其中 0 表示内部屏幕,1 表示外部屏幕,[2, INT32_MAX] 表示 HWC 虚拟屏幕,而 -1 表示无效屏幕或非 HWC 虚拟屏幕。

从 Android 10 开始,屏幕会获得稳定持久的 ID,从而使 SurfaceFlinger 和 DisplayManagerService 可以跟踪两个以上的屏幕并识别以前看到的屏幕。如果 HWC 支持 IComposerClient.getDisplayIdentificationData 并提供屏幕标识数据,SurfaceFlinger 将会解析 EDID 结构并为物理屏幕和 HWC 虚拟屏幕分配稳定的 64 位屏幕 ID。系统会使用选项类型表示 ID,其中 null 值表示无效屏幕或非 HWC 虚拟屏幕。如果 HWC 不支持,SurfaceFlinger 会回退到最多只存在两个物理屏幕的旧版行为。

每屏幕焦点

为了同时支持多个以单个屏幕为目标的输入源,可以将 Android 10 配置为支持多个聚焦窗口,每个屏幕最多支持一个。当多个用户同时与同一设备交互并使用不同的输入方法或设备(例如 Android Automotive)时,此功能仅适用于特殊类型的设备。

强烈建议不要为常规设备启用此功能,包括跨屏设备或用于类似桌面设备体验的设备。这主要是出于安全方面的考虑,因为这样做可能会导致用户不确定哪个窗口具有输入焦点。

想象一下,用户在文本输入字段中输入安全信息,也许是登录某个银行应用或者输入包含敏感信息的文本。恶意应用可以创建一个虚拟的屏幕外屏幕用于执行 activity,也可以使用文本输入字段执行 activity。合法 activity 和恶意 activity 均具有焦点,并且都显示一个有效的输入指示符(闪烁光标)。

不过,键盘(硬件或软件)的输入只能进入最顶层的 activity(最近启动的应用)。通过创建隐藏的虚拟屏幕,即使在主设备屏幕上使用软件键盘,恶意应用也可以获取用户输入。

使用 com.android.internal.R.bool.config_perDisplayFocusEnabled 设置每屏幕焦点。

兼容性

问题:在 Android 9 及更低版本中,系统中一次最多只有一个窗口具有焦点。

解决方案:在极少数情况下,来自同一进程的两个窗口都处于聚焦状态,则系统仅向在 Z 轴顺序中较高的窗口提供焦点。对于以 Android 10 为目标平台的应用,目前已取消这一限制,此时预计这些应用可以支持同时聚焦多个窗口。

实现

WindowManagerService#mPerDisplayFocusEnabled 用于控制此功能的可用性。在 ActivityManager 中,系统现在使用的是 ActivityDisplay#getFocusedStack(),而不是利用变量进行全局跟踪。ActivityDisplay#getFocusedStack() 根据 Z 轴顺序确定焦点,而不是通过缓存值来确定。这样一来,只有一个来源 WindowManager 需要跟踪 activity 的 Z 轴顺序。

如果必须要确定系统中最顶层的聚焦堆栈,ActivityStackSupervisor#getTopDisplayFocusedStack() 会采用类似的方法处理这些情况。系统将从上到下遍历这些堆栈,搜索第一个符合条件的堆栈。

InputDispatcher 现在可以有多个聚焦窗口(每个屏幕一个)。如果某个输入事件特定于屏幕,则该事件会被分派到相应屏幕中的聚焦窗口。否则,它会被分派到聚焦屏幕(即用户最近与之交互的屏幕)中的聚焦窗口。

请参阅 InputDispatcher::mFocusedWindowHandlesByDisplayInputDispatcher::setFocusedDisplay()。聚焦应用也会通过 NativeInputManager::setFocusedApplication() 在 InputManagerService 中分别更新。

WindowManager 中,系统还会单独跟踪聚焦窗口。请参阅 DisplayContent#mCurrentFocusDisplayContent#mFocusedApp 以及各自的用途。相关的焦点跟踪和更新方法已从 WindowManagerService 移至 DisplayContent