系统装饰支持

下文介绍对这些与显示相关的部分进行的更新:

系统装饰

Android 10 增加了对于配置辅助屏幕的支持,以显示特定的系统装饰(例如壁纸、导航栏和启动器)。默认情况下,主屏幕会显示所有系统装饰项,而辅助屏幕会显示选择性启用的装饰项。对输入法 (IME) 的支持可以与其他系统装饰项分开设置。

使用 DisplayWindowSettings#setShouldShowSystemDecorsLocked() 在特定屏幕上添加对系统装饰的支持,或在 /data/system/display_settings.xml 中提供默认值。如需查看相关示例,请参阅屏幕窗口设置

实现

DisplayWindowSettings#setShouldShowSystemDecorsLocked() 也会显示在 WindowManager#setShouldShowSystemDecors() 中,用于测试。出于支持系统装饰的目的触发此方法,既不会添加先前没有的装饰窗口,也不会移除先前已存在的装饰窗口。在大多数情况下,有关系统装饰支持的更改只有在设备重启后才会完全生效。

通常通过 DisplayContent#supportsSystemDecorations() 在 WindowManager 代码库中检查对于系统装饰的支持,而检查外部服务(例如系统界面,以检查是否应显示导航栏)则使用 WindowManager#shouldShowSystemDecors()。 如需了解此设置控制的内容,请查看这些方法的调用点。

系统界面装饰窗口

Android 10 仅针对导航栏增加了系统装饰窗口支持,因为导航栏对于在 activity 和应用之间导航是必不可少的。默认情况下,导航栏会显示“返回”和“主屏幕”图标。仅当目标屏幕支持系统装饰时才包含此功能(请参阅 DisplayWindowSettings)。

状态栏是一个更为复杂的系统窗口,因为它还包含通知栏、快捷设置和锁定屏幕。在 Android 10 中,辅助屏幕不支持状态栏。 因此,通知、设置和完整锁屏仅在主屏幕上可用。

辅助屏幕不支持“概览/最近”系统窗口。在 Android 10 中,AOSP 仅在默认屏幕上显示“最近”,并包含所有屏幕中的 activity。从“最近”启动时,默认会将辅助屏幕上的 activity 放到默认屏幕的最前端。此方法存在一些已知的问题,例如当应用显示在其他屏幕上时不会立即更新。

实现

要实现其他系统界面功能,设备制造商应使用单个系统界面组件来侦听添加/移除屏幕并显示适当的内容。

支持多屏幕 (MD) 的系统界面组件需要处理以下情况:

  • 在启动时执行多屏幕初始化
  • 在运行时添加屏幕
  • 在运行时移除屏幕

如果系统界面先于 WindowManager 检测到添加屏幕,就会创建一个竞态条件。可以通过以下方法来避免发生这种情况,即在添加屏幕时实现一个从 WindowManager 到系统界面的自定义回调,而不是订阅 DisplayManager.DisplayListener 事件。有关参考实现,请参阅 CommandQueue.Callbacks#onDisplayReady 获取导航栏支持和 WallpaperManagerInternal#onDisplayReady(适用于壁纸)。

此外,Android 10 还提供了以下更新:

  • NavigationBarController 类可控制特定于导航栏的所有功能。
  • 要查看自定义导航栏,请参阅 CarStatusBar
  • TYPE_NAVIGATION_BAR 不再局限于单个实例,而是可以在每个屏幕中使用。
  • IWindowManager#hasNavigationBar() 已更新为仅包含系统界面的 displayId 参数。

启动器

在 Android 10 中,每个配置为支持系统装饰的屏幕,默认情况下都有一个专用于启动器 activity 的主屏幕堆栈,该堆栈的类型为 WindowConfiguration#ACTIVITY_TYPE_HOME。每个屏幕使用一个单独的启动器 activity 实例。

图 1. platform/development/samples/MultiDisplay 的多屏幕启动器示例

大多数现有的启动器都不支持多个实例,也没有针对大屏幕尺寸进行过优化。此外,在辅助/外部屏幕上通常需要不同类型的体验。为了提供针对辅助屏幕的专用 activity,Android 10 在 intent 过滤器中引入了 SECONDARY_HOME 类别。此 activity 的实例将在支持系统装饰的所有屏幕上使用,每个屏幕对应一个实例。

<activity>
    ...
    <intent-filter>
        <category android:name="android.intent.category.SECONDARY_HOME" />
        ...
    </intent-filter>
</activity>

该 activity 的启动模式必须符合以下条件:不会阻止多个实例,并且可以适应不同屏幕尺寸。启动模式不能是 singleInstancesingleTask

实现

在 Android 10 中,RootActivityContainer#startHomeOnDisplay() 会根据主屏幕所在的屏幕自动选择所需的组件和 intent。RootActivityContainer#resolveSecondaryHomeActivity() 包含根据当前选定的启动器查询启动器 activity 组件的逻辑,并且可以根据需要使用系统默认设置(请参阅 ActivityTaskManagerService#getSecondaryHomeIntent())。

安全限制

为了避免恶意应用创建启用了系统装饰的虚拟屏幕并从 surface 读取用户敏感信息,除了对辅助屏幕上的 activity 施加限制外,启动器还仅会在系统所有的虚拟屏幕上显示。启动器不会在非系统虚拟屏幕上显示内容。

壁纸

在 Android 10 及更高版本中,辅助屏幕可支持壁纸:

图 2. 内部(上方)和外部(下方)屏幕上的动态壁纸

开发者可以通过在 WallpaperInfo XML 定义中提供 android:supportsMultipleDisplays="true" 来声明对壁纸功能的支持。壁纸开发者还可以使用 WallpaperService.Engine#getDisplayContext() 中的显示上下文来加载素材资源。

框架会为每个屏幕创建一个 WallpaperService.Engine 实例,因此每个引擎都有自己的 surface 和显示上下文。开发者需要确保每个引擎都能够根据 VSYNC 以不同的帧频独立绘制。

针对单个屏幕选择壁纸

Android 10 平台无法直接支持针对单个屏幕选择壁纸。为了实现此功能,每个屏幕都需要一个稳定的屏幕标识符来保留壁纸设置。Display#getDisplayId() 是动态的,因此无法保证物理屏幕在重启后仍具有相同的 ID。

不过,Android 10 新增了 DisplayInfo.mAddress,其中包含用于物理屏幕的稳定标识符,且将来可用于完整实现。遗憾的是,现在为 Android 10 实现该逻辑已经太迟了。建议采用以下解决方案:

  1. 使用 WallpaperManager API 设置壁纸。
  2. WallpaperManager 是从 Context 对象获得的,每个 Context 对象均具有相应屏幕 (Context#getDisplay()/getDisplayId()) 的相关信息。因此,您可以从 WallpaperManager 实例获取 displayId,而无需添加新方法。
  3. 在框架端,使用从 Context 对象获得的 displayId,并将其映射到静态标识符(例如物理屏幕的端口)。使用该静态标识符来保留所选壁纸。

此解决方案利用了壁纸选择器的现有实现。如果在特定屏幕上打开壁纸选择器并使用正确的 Context,则当选择器请求设置壁纸时,系统可以自动识别该屏幕。

如果需要为当前屏幕以外的屏幕设置壁纸,请为目标屏幕创建一个新的 Context 对象 (Context#createDisplayContext),并从该屏幕中获取 WallpaperManager 实例。

安全限制

系统不会在不属于自己的虚拟屏幕上显示壁纸。 这是出于安全考虑,因为恶意应用可能会创建启用了系统装饰支持的虚拟屏幕,并从 surface 读取用户敏感信息(例如个人照片)。

实现

在 Android 10 中,IWallpaperConnection#attachEngine()IWallpaperService#attach() 接口接受 displayId 参数来创建与每个屏幕之间的连接。 WallpaperManagerService.DisplayConnector 封装了每个屏幕的壁纸引擎和连接。在 WindowManager 中,会在构建时为每个 DisplayContent 对象创建壁纸控制器,而不是为所有屏幕创建一个 WallpaperController

一些公共 WallpaperManager 方法实现(如 WallpaperManager#getDesiredMinimumWidth())已更新,以计算并提供相应屏幕的信息。添加了 WallpaperInfo#supportsMultipleDisplays() 和相应的资源属性,以便应用开发者可以报告哪些壁纸已可供多个屏幕使用。

如果默认屏幕上显示的壁纸服务不支持多个屏幕,则系统会在辅助屏幕上显示默认壁纸。

图 3. 辅助屏幕的壁纸回退逻辑