显示屏支持

以下是针对这些特定于显示屏的区域所做的更新

调整 Activity 和显示屏的大小

为了表明应用可能不支持多窗口模式或调整大小,Activity 使用了 resizeableActivity=false 属性。当 Activity 调整大小时,应用遇到的常见问题包括

  • Activity 的配置可能与应用或其他非视觉组件的配置不同。一个常见的错误是从应用上下文读取显示屏指标。返回的值不会针对 Activity 显示所在的可见区域指标进行调整。
  • Activity 可能无法处理调整大小,并且可能崩溃、显示失真的界面,或者由于重新启动而丢失状态,但未保存实例状态。
  • 应用可能会尝试使用绝对输入坐标(而不是相对于窗口位置的坐标),这可能会破坏多窗口中的输入。

在 Android 7(及更高版本)中,可以将应用设置为 resizeableActivity=false 以始终在全屏模式下运行。在这种情况下,平台会阻止不可调整大小的 Activity 进入分屏模式。如果用户尝试从启动器调用不可调整大小的 Activity,但已处于分屏模式,则平台会退出分屏模式,并在全屏模式下启动不可调整大小的 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(在 System UI 中)接收回调以显示进程重启按钮。

显示屏尺寸和宽高比

Android 10 提供了对从细长屏幕的高纵横比到 1:1 纵横比的新纵横比的支持。应用可以定义它们能够处理的屏幕的 ApplicationInfo#maxAspectRatioApplicationInfo#minAspectRatio

app ratios in Android 10

图 1. Android 10 中支持的示例应用纵横比

设备实现可以具有尺寸和分辨率小于 Android 9 及更低版本要求的辅助显示屏(宽度或高度至少 2.5 英寸,smallestScreenWidth 至少 320 DP),但只有选择支持这些小型显示屏的 Activity 才能放置在这些显示屏上。

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

显示屏政策

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

  • 显示屏状态和旋转
  • 某些按键和运动事件跟踪
  • 系统 UI 和装饰窗口

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

显示屏窗口设置

在 Android 10 中,可配置的每显示屏窗口设置已扩展为包括

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

DisplayWindowSettings 类包含这些选项的设置。每次设置更改时,这些设置都会持久保存到 /data 分区中的 display_settings.xml 中。有关详细信息,请参阅 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 可以利用硬件合成器 (HWC) API 生成稳定的显示屏 ID,这使其能够管理任意数量的物理显示屏。要了解更多信息,请参阅静态显示屏标识符

框架可以通过 SurfaceControl#getPhysicalDisplayToken 查找物理显示屏的 IBinder 令牌,方法是从 SurfaceControl#getPhysicalDisplayIdsDisplayEventReceiver 热插拔事件中获取 64 位显示屏 ID。

在 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 中(即最近启动的应用),因此通过创建隐藏的虚拟显示屏,恶意应用可以获取用户输入,即使在使用主设备显示屏上的软件键盘时也是如此。

使用 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