支持多窗口模式

多窗口模式允许多个应用同时共享同一屏幕。应用可以并排或上下排列(分屏模式),也可以是在其他应用上方叠加小窗口形式的应用(画中画模式),还可以是各自处于可移动且可调整大小的独立窗口中(桌面窗口模式)。

图 1. 在分屏模式下并排显示两个应用。

如需了解有关如何在手机上进入分屏模式的用户说明,请参阅在 Pixel 手机上同时查看两个应用

各版本专属的多窗口功能

多窗口用户体验取决于 Android 版本和设备类型

  • Android 7.0(API 级别 24)在小屏幕设备上引入了分屏模式,并在特定设备上引入了画中画模式。

    • 分屏模式会让两个应用填满屏幕,并排或上下显示。用户可以拖动分隔两个应用的分割线,使一个应用变大,另一个应用变小。

    • 画中画模式可让用户在与其他应用互动时继续播放视频(请参阅画中画支持)。

    • 桌面窗口模式允许用户自由调整每个 Activity 的大小,可由大屏设备的制造商启用。

      您可以通过指定 Activity 允许的最小尺寸来配置应用如何处理多窗口模式。您还可以通过设置 resizeableActivity="false" 来停用应用的多窗口模式,以确保系统始终全屏显示您的应用。

  • Android 8.0(API 级别 26)将画中画模式扩展到了小屏幕设备。

  • Android 12(API 级别 31)将多窗口模式设为标准行为。

    • 在大屏幕上中等展开窗口大小类别),无论应用如何配置,平台都会支持所有多窗口模式下的应用。如果 resizeableActivity="false",应用在必要时会被置于兼容模式,以适应显示尺寸。

    • 在小屏幕上紧凑窗口大小类别),系统会检查 Activity 的 minWidthminHeight,以确定该 Activity 能否在多窗口模式下运行。如果 resizeableActivity="false",则无论最小宽度和高度是多少,应用都无法在多窗口模式下运行。

  • Android 16(API 级别 36)会替换屏幕方向、宽高比和可调整大小限制。

    • 在大屏幕上(最小宽度 >= 600dp),系统会忽略用于限制应用方向、宽高比和可调整大小的清单属性及运行时 API,从而优化所有设备类型上的用户体验。

      如需了解如何让游戏不受 Android 16 变更的影响,请参阅应用方向、宽高比和可调整大小例外情况。

实现策略

可以通过多种方式启动多窗口模式。

支持用户启动的分屏

  1. 打开“最近使用”屏幕
  2. 滑动应用使其可见
  3. 点按应用标题栏中的应用图标
  4. 选择分屏菜单选项
  5. 从“最近使用”屏幕中选择另一个应用

用户可以通过将窗口分割线拖动到屏幕边缘(向上或向下、向左或向右)来退出分屏模式。

以编程方式分屏(启动相邻 Activity)

如果您的应用需要将另一个 Activity 启动到相邻窗口中,请使用 FLAG_ACTIVITY_LAUNCH_ADJACENT Intent 标志。在 Android 12L(API 级别 32)及更高版本上,该标志允许全屏运行的应用进入分屏模式,并在相邻窗口中启动目标 Activity。FLAG_ACTIVITY_LAUNCH_ADJACENT 是在 Android 7.0(API 级别 24)中引入的。

若要启动相邻 Activity,请将 FLAG_ACTIVITY_LAUNCH_ADJACENTFLAG_ACTIVITY_NEW_TASK 结合使用,例如

Kotlin

fun openUrlInAdjacentWindow(url: String) {
    Intent(Intent.ACTION_VIEW).apply {
        data = Uri.parse(url)
        addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT or Intent.FLAG_ACTIVITY_NEW_TASK)
    }.also { intent -> startActivity(intent) }
}

Java

public void openUrlInAdjacentWindow(String url) {
    Intent intent = new Intent(Intent.ACTION_VIEW);
    intent.setData(Uri.parse(url));
    intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT | Intent.FLAG_ACTIVITY_NEW_TASK);
    startActivity(intent);
}

Activity 嵌入(在同一任务中分屏显示 Activity)

Activity 嵌入允许由多个 Activity 组成的应用在同一应用任务内分屏显示 Activity,例如在大屏幕上的列表-详情布局。作为 Jetpack WindowManager 的一部分,Activity 嵌入允许您使用 XML 或 API 调用来定义配置规则(例如分屏对规则),以确定 Activity 是并排显示还是堆叠显示。如需了解详情,请参阅 Activity 嵌入

多窗口模式下的 Activity 生命周期

多窗口模式不会改变 Activity 生命周期。但是,在不同版本的 Android 上,多个窗口中应用的多项恢复状态有所不同。

多项恢复 (Multi-resume)

Android 10(API 级别 29)及更高版本支持多项恢复——当设备处于多窗口模式时,所有 Activity 都保持在 RESUMED 状态。如果某个透明 Activity 位于该 Activity 之上,或者该 Activity 无法获取焦点(例如该 Activity 处于画中画模式),则该 Activity 可能会暂停。在特定时间也可能没有任何 Activity 获得焦点,例如当通知栏打开时。onStop() 方法照常工作:每当 Activity 离开屏幕时,该方法都会被调用。

多项恢复功能也可在运行 Android 9(API 级别 28)的特定设备上使用。要在 Android 9 设备上选用多项恢复,请添加以下清单元数据

<meta-data android:name="android.allow_multiple_resumed_activities" android:value="true" />

要验证给定设备是否支持此清单元数据,请参阅该设备的技术规范。

Android 9

在 Android 9(API 级别 28)及更低版本的多窗口模式下,在任意给定的时间,只有用户最近与之互动的 Activity 处于活动状态。此 Activity 被视为最顶部的,并且是唯一处于 RESUMED 状态的 Activity。所有其他可见的 Activity 处于 STARTED 状态,但并未处于 RESUMED 状态。不过,系统赋予这些可见但未恢复的 Activity 比不可见 Activity 更高的优先级。如果用户与其中一个可见 Activity 互动,该 Activity 就会恢复,而之前最顶部的 Activity 则进入 STARTED 状态。

当单个活动应用进程中存在多个 Activity 时,系统会恢复具有最高 Z 轴顺序的 Activity,并暂停其他 Activity。

配置变更

当用户将应用放入多窗口模式时,系统会按照处理配置变更中所述通知 Activity 发生了配置变更。当用户调整应用大小或将应用放回全屏模式时,也会发生这种情况。

从本质上讲,此变更对 Activity 生命周期的影响与系统通知应用设备已从纵向转为横向时的影响相同,不同之处在于,这里改变的是应用的尺寸,而不仅仅是交换方向。您的 Activity 可以自行处理配置变更,或者您的应用可以允许系统销毁该 Activity 并使用新尺寸重新创建它。

如果用户正在调整窗口大小并在任一维度上使其变大,系统会调整 Activity 的大小以匹配用户操作,并根据需要发出配置变更。如果应用在绘制新暴露的区域时出现延迟,系统会暂时使用 windowBackground 属性或默认的 windowBackgroundFallback 样式属性所指定的颜色填充这些区域。

独占资源访问

为了帮助支持多项恢复功能,请使用 onTopResumedActivityChanged() 生命周期回调。

当 Activity 获得或失去最顶部已恢复 Activity 位置时会调用此回调。当 Activity 使用共享的单例资源(如麦克风或相机)时,这一点非常重要。

Kotlin

override fun onTopResumedActivityChanged(topResumed: Boolean) {
    super.onTopResumedActivityChanged(topResumed)
    if (topResumed) {
        // Top resumed activity.
        // Can be a signal to re-acquire exclusive resources.
    } else {
        // No longer the top resumed activity.
    }
}

Java

@Override
public void onTopResumedActivityChanged(boolean topResumed) {
    if (topResumed) {
        // Top resumed activity.
        // Can be a signal to re-acquire exclusive resources.
    } else {
        // No longer the top resumed activity.
    }
}

请注意,应用可能因其他原因丢失资源,例如移除了某件共享的硬件。

在任何情况下,应用都应正常处理影响可用资源的事件和状态变更。

对于使用相机的应用,CameraManager.AvailabilityCallback#onCameraAccessPrioritiesChanged() 提示这可能是尝试获取相机访问权限的好时机。此方法自 Android 10(API 级别 29)起可用。

请记住,resizeableActivity=false 并不能保证独占相机访问权限,因为在其他显示屏上可以打开使用相机的其他应用。

图 2. 多窗口模式下的相机。

当应用失去焦点时,并不一定必须释放相机。例如,当用户与新获得焦点的最顶部已恢复应用互动时,您可能希望继续进行相机预览。当您的应用不是最顶部已恢复应用时,继续运行相机是完全可以的,但它必须正确处理断开连接的情况。当最顶部已恢复应用想要使用相机时,它可以将其打开,而您的应用将失去访问权限。当您的应用重新获得焦点时,可以重新打开相机。

应用收到 CameraDevice.StateCallback#onDisconnected() 回调后,对相机设备的后续调用将抛出 CameraAccessException

窗口指标

Android 11(API 级别 30)引入了以下 WindowManager 方法,以提供在多窗口模式下运行的应用的范围

Jetpack WindowManager 库方法 computeCurrentWindowMetrics()computeMaximumWindowMetrics() 分别提供了类似的功能,但向后兼容到 API 级别 14。

要获取当前显示屏以外其他显示屏的指标,请执行以下操作(如代码段所示)

  • 创建显示屏上下文
  • 为显示屏创建窗口上下文
  • 获取窗口上下文的 WindowManager
  • 获取应用可用最大显示区域的 WindowMetrics

Kotlin

val windowMetrics = context.createDisplayContext(display)
    .createWindowContext(WindowManager.LayoutParams.TYPE_APPLICATION, null)
    .getSystemService(WindowManager::class.java)
    .maximumWindowMetrics

Java

WindowMetrics windowMetrics = context.createDisplayContext(display)
                                     .createWindowContext(WindowManager.LayoutParams.TYPE_APPLICATION, null)
                                     .getSystemService(WindowManager.class)
                                     .getMaximumWindowMetrics();

已弃用的方法

在 API 级别 30 中,Display 方法 getSize()getMetrics() 已被弃用,取而代之的是新的 WindowManager 方法。

Android 12(API 级别 31)弃用了 Display 方法 getRealSize()getRealMetrics(),并更新了它们的行为,使其更接近 getMaximumWindowMetrics() 的行为。

多窗口模式配置

如果您的应用适配 Android 7.0(API 级别 24)或更高版本,您可以配置应用中的 Activity 是否支持以及如何支持多窗口模式。您可以在清单中设置属性以控制大小和布局。根 Activity 的属性设置适用于其任务栈中的所有 Activity。例如,如果根 Activity 具有 android:resizeableActivity="true",那么任务栈中的所有 Activity 都是可调整大小的。在某些较大的设备(如 Chromebook)上,即使您指定了 android:resizeableActivity="false",您的应用也可能在可调整大小的窗口中运行。如果这导致应用运行出错,您可以使用 Google Play 上的过滤器来限制您的应用在此类设备上的可用性。

Android 12(API 级别 31)默认启用多窗口模式。在大屏幕上(中等展开窗口大小类别),无论应用如何配置,所有应用都会在多窗口模式下运行。在小屏幕上,系统会检查 Activity 的 minWidthminHeight 以及 resizeableActivity 设置,以确定该 Activity 能否在多窗口模式下运行。

resizeableActivity

在清单的 <activity><application> 元素中设置此属性,以针对 API 级别 24 至 30 启用或停用多窗口模式(此属性在 API 级别 24 中引入)

<application
  android:name=".MyActivity"
  android:resizeableActivity=["true" | "false"] />;

如果此属性设置为 true,则可以在分屏和桌面窗口模式下启动该 Activity。如果属性设置为 false,则该 Activity 不支持多窗口模式。如果值为 false,且用户尝试在多窗口模式下启动该 Activity,该 Activity 将接管整个屏幕。

如果您的应用适配 API 级别 24 或更高版本,但您没有指定此属性的值,则该属性的值默认为 true。

如果您的应用适配 API 级别 31 或更高版本,则此属性在小屏幕和大屏幕上的工作方式有所不同

  • 大屏幕中等展开窗口大小类别):所有应用都支持多窗口模式。该属性指示 Activity 是否可以调整大小。如果 resizeableActivity="false",应用在必要时会被置于兼容模式,以符合显示尺寸。
  • 小屏幕紧凑窗口大小类别):如果 resizeableActivity="true" 且 Activity 的最小宽度和最小高度符合多窗口要求,则该 Activity 支持多窗口模式。如果 resizeableActivity="false",则无论 Activity 的最小宽度和高度是多少,它都不支持多窗口模式。

如果您的应用适配 API 级别 36 或更高版本,此属性在最小宽度 >= 600dp 的显示屏上将被忽略。但是,应用会完全尊重用户对宽高比的选择(请参阅用户针对每个应用设置的替换项)。

如果您正在开发游戏,请参阅应用方向、宽高比和可调整大小,了解如何使您的游戏不受 Android 16(API 级别 36)变更的影响。

supportsPictureInPicture

在清单的 <activity> 节点中设置此属性,以指示该 Activity 是否支持画中画模式。

<activity
  android:name=".MyActivity"
  android:supportsPictureInPicture=["true" | "false"] />

configChanges

要自行处理多窗口配置变更(例如在用户调整窗口大小时),请将 android:configChanges 属性添加到应用清单的 <activity> 节点中,并且至少包含以下值

<activity
  android:name=".MyActivity"
  android:configChanges="screenSize | smallestScreenSize

      | screenLayout | orientation" />

添加 android:configChanges 后,您的 Activity 和 Fragment 将收到对 onConfigurationChanged() 的回调,而不是被销毁并重新创建。然后,您可以根据需要手动更新视图、重新加载资源并执行其他操作。

<layout>

在 Android 7.0(API 级别 24)及更高版本中,<layout> 清单元素支持影响 Activity 在多窗口模式下行为的几个属性

  • android:defaultHeightandroid:defaultWidth:在桌面窗口模式下启动时,Activity 的默认高度和宽度。

  • android:gravity:在桌面窗口模式下启动时,Activity 的初始位置。有关合适的值,请参阅 Gravity 类。

  • android:minHeightandroid:minWidth:Activity 在分屏和桌面窗口模式下的最小高度和最小宽度。如果用户在分屏模式下移动分割线,使 Activity 小于指定的最小值,系统会根据用户要求的尺寸对 Activity 进行裁剪。

以下代码展示了如何在桌面窗口模式下显示 Activity 时指定其默认大小、位置和最小尺寸

<activity android:name=".MyActivity">
    <layout android:defaultHeight="500dp"
          android:defaultWidth="600dp"
          android:gravity="top|end|..."
          android:minHeight="450dp"
          android:minWidth="300dp" />
</activity>

运行时多窗口模式

从 Android 7.0 开始,系统提供了支持可在多窗口模式下运行的应用的功能。

多窗口模式中停用的功能

在多窗口模式下,对于与其它 Activity 或应用共享设备屏幕的 Activity,Android 可能会停用或忽略不适用的功能。

此外,某些系统界面自定义选项将被停用。例如,在多窗口模式下运行的应用无法隐藏状态栏(请参阅控制系统界面的可见性)。

系统会忽略对 android:screenOrientation 属性的更改。

多窗口模式查询和回调

Activity 类提供了以下方法以支持多窗口模式

  • isInMultiWindowMode():指示该 Activity 是否处于多窗口模式。

  • isInPictureInPictureMode():指示该 Activity 是否处于画中画模式。

  • onMultiWindowModeChanged():每当 Activity 进入或退出多窗口模式时,系统就会调用此方法。如果 Activity 正在进入多窗口模式,系统会向该方法传递一个 true 值;如果 Activity 正在离开多窗口模式,则传递 false 值。

  • onPictureInPictureModeChanged():每当 Activity 进入或退出画中画模式时,系统就会调用此方法。如果 Activity 正在进入画中画模式,系统会向该方法传递一个 true 值;如果 Activity 正在离开画中画模式,则传递 false 值。

Fragment 类公开了其中许多方法的对应版本;例如,Fragment.onMultiWindowModeChanged()

画中画模式

要使 Activity 进入画中画模式,请调用 enterPictureInPictureMode()。如果设备不支持画中画模式,此方法将不起作用。如需了解详情,请参阅使用画中画 (PiP) 添加视频

在多窗口模式下启动新 Activity

当您启动新 Activity 时,您可以指定新 Activity 在可能的情况下显示在当前 Activity 的相邻位置。请使用 Intent 标志 FLAG_ACTIVITY_LAUNCH_ADJACENT,它会指示系统尝试在相邻窗口中创建新 Activity,以便这两个 Activity 共享屏幕。系统会尽最大努力实现这一点,但不保证一定会成功。

如果设备处于桌面窗口模式,并且您正在启动新 Activity,您可以通过调用 ActivityOptions.setLaunchBounds() 来指定新 Activity 的尺寸和屏幕位置。如果设备未处于多窗口模式,该方法将不起作用。

在 API 级别 30 及更低版本上,如果您在任务栈中启动 Activity,该 Activity 会替换屏幕上的 Activity,并继承其所有的多窗口属性。如果您希望在多窗口模式下将新 Activity 作为独立窗口启动,则必须在新的任务栈中启动它。

Android 12(API 级别 31)允许应用在多个 Activity 之间拆分应用的任务窗口。您可以通过创建 XML 配置文件或调用 Jetpack WindowManager API,来确定应用如何显示其 Activity(全屏、并排或堆叠)。

拖放

当两个 Activity 共享屏幕时,用户可以将数据从一个 Activity 拖放到另一个 Activity。(在 Android 7.0 之前,用户只能在单个 Activity 内拖放数据。)如需快速添加对接收拖放内容的支持,请参阅 DropHelper API。如需获得完整的拖放指南,请参阅启用拖放功能

多实例

每个根 Activity 都有自己的任务,并在自己的窗口中显示。要在单独的窗口中启动应用的新实例,请使用 FLAG_ACTIVITY_NEW_TASK 标志启动新的 Activity。您可以将此设置与多窗口属性相结合,以请求新窗口的具体位置。例如,购物应用可以显示多个相邻的窗口来对比商品。

Android 12(API 级别 31)及更高版本允许您通过 Activity 嵌入同一个任务窗口中并排启动一个 Activity 的两个实例。

如果您希望允许用户从应用启动器或任务栏启动应用的另一个实例,请在启动 Activity 的清单中设置 android:resizeableActivity="true",且不要使用会阻止多实例的启动模式。例如,在设置了 FLAG_ACTIVITY_MULTIPLE_TASKFLAG_ACTIVITY_NEW_DOCUMENT 时,singleInstancePerTask Activity 可以在不同任务中实例化多次。

在 Android 15(API 级别 35)及更高版本上,PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI 允许您声明支持多实例。该属性是系统界面向用户展示用于创建应用多实例的控制项的明确信号。该属性独立于启动模式,但应仅在 Activity 或应用的启动模式与该属性兼容时使用,例如,当启动模式不是 singleInstance 时。

当应用的多个实例在折叠屏设备的独立窗口中运行时,如果设备姿态发生变化,可能有一个或多个实例会被送入后台。例如,假设设备处于展开状态,并且折叠线两侧的独立窗口中运行着两个应用实例。如果设备折叠起来,其中一个实例可能会被终止,而不是尝试在更小的屏幕上容纳两个实例的窗口。

多窗口模式验证

无论您的应用是否适配 API 级别 24 或更高版本,您都应该验证它在多窗口模式下的行为,以防用户尝试在运行 Android 7.0 或更高版本的设备上在多窗口模式下启动它。

测试设备

运行 Android 7.0(API 级别 24)或更高版本的设备支持多窗口模式。

API 级别 23或更低版本

当用户尝试在多窗口模式下使用该应用时,除非该应用声明了固定方向,否则系统会强制调整该应用的大小。

如果您的应用没有声明固定方向,您应该在运行 Android 7.0 或更高版本的设备上启动您的应用,并尝试将应用放入分屏模式。验证在强制调整应用大小时,用户体验是否可以接受。

如果应用声明了固定方向,您应该尝试将应用放入多窗口模式。验证当您这样做时,应用是否保持在全屏模式。

API 级别 24 至 30

如果您的应用适配 API 级别 24 至 30 且未停用多窗口支持,请在分屏和桌面窗口模式下验证以下行为

  • 全屏启动应用,然后通过长按最近使用按钮切换到多窗口模式。验证应用是否能正常切换。

  • 直接在多窗口模式下启动应用,并验证应用能否正常启动。您可以通过按最近使用按钮启动多窗口模式下的应用,然后长按应用的标题栏并将其拖动到屏幕上的某个突出显示区域。

  • 在分屏模式下,通过拖动屏幕分割线来调整应用的大小。验证应用在调整大小时是否没有崩溃,并且必要的界面元素是否可见。

  • 如果您指定了应用的最小尺寸,请尝试调整应用大小,使其窗口尺寸小于这些最小尺寸。验证您是否无法将应用调整为小于指定的最小尺寸。

  • 在所有测试中,请验证应用性能是否可接受。例如,验证在调整应用大小后,更新界面是否不会出现过长的延迟。

API 级别 31 或更高版本

如果您的应用适配 API 级别 31 或更高版本,且主 Activity 的最小宽度和最小高度小于或等于可用显示区域的相应尺寸,请验证针对 API 级别 24 至 30 列出的所有行为。

测试核对清单

要在多窗口模式下验证应用的性能,请尝试以下操作。除非另有说明,否则您应该在分屏和桌面窗口模式下都尝试这些操作。

  • 进入和退出多窗口模式。

  • 从您的应用切换到另一个应用,并验证该应用在可见但未活动时的行为是否正常。例如,如果您的应用正在播放视频,请验证在用户与其他应用进行互动时视频是否继续播放。

  • 在分屏模式下,尝试移动屏幕分割线,使您的应用变大和变小。在并排和上下排列的配置中都尝试这些操作。验证应用不会崩溃、核心功能保持可见,并且调整大小的操作不会花费太长时间。

  • 连续快速执行多次调整大小的操作。验证您的应用是否不会崩溃或发生内存泄漏。Android Studio 的内存分析器 (Memory Profiler) 提供了有关应用内存使用情况的信息(请参阅使用内存分析器检查应用的内存使用情况)。

  • 在许多不同的窗口配置中正常使用应用,并验证应用行为是否正常。验证文本是否清晰可读,以及界面元素是否不会因过小而无法交互。

多窗口支持已停用

在 API 级别 24 至 30 上,如果您通过设置 android:resizeableActivity="false" 停用了多窗口支持,您应该在运行 Android 7.0 至 11 的设备上启动应用,并尝试将应用放入分屏和桌面窗口模式。验证当您这样做时,应用是否仍保持全屏模式。

其他资源

如需了解有关 Android 中多窗口支持的更多信息,请参阅