activity 嵌入可以将应用的一个任务窗口拆分到两个 activity 中,或者拆分到同一个 activity 的两个实例中,从而优化大屏设备上的应用。

更新旧版代码库以支持大屏幕需要耗费大量人力和时间。使用 fragment 将基于 activity 的应用转换为多窗格布局需要进行重大重构。
activity 嵌入只需要对应用进行很少的重构或根本不需要对应用进行重构。至于应用如何显示其 activity(是并排,还是堆叠)时,可以通过创建 XML 配置文件或进行 Jetpack WindowManager API 调用来确定。
系统会自动维护对小屏幕的支持。当应用在配备小屏幕的设备上时,activity 会相互堆叠。在大屏幕上,activity 会并排显示。系统会根据您已创建的配置(不需要分支逻辑)来确定呈现方式。
activity 嵌入支持设备屏幕方向的变化,并且可以在可折叠设备上无缝工作,该功能会随着设备折叠和展开而堆叠和取消堆叠 activity。
Modern Android Development 使用单 activity 架构,其中包含 fragment、导航组件和多功能布局管理器,如 SlidingPaneLayout
。
但如果应用由多个 activity 组成,activity 嵌入可让您轻松地在平板电脑、可折叠设备和 ChromeOS 设备上提供增强的用户体验。
Android 12L(API 级别 32)及更高版本支持 activity 嵌入。
拆分任务窗口
activity 嵌入会将应用任务窗口拆分成两个容器:主要容器和辅助容器。这些容器存放从主 activity 或从已在容器中的其他 activity 启动的 activity。
当 activity 启动时,它们堆叠在辅助容器中,而在小屏幕上,辅助容器堆叠在主要容器之上,因此 activity 堆叠和返回导航与应用中已内置的 activity 顺序一致。
activity 嵌入可让您以各种方式显示 activity。应用可以通过同时启动两个并排的 activity 来拆分任务窗口:

或者,占据整个任务窗口的 activity 可以通过在侧面启动一个新的 activity 来创建分屏:

已在分屏中且共享任务窗口的 activity 可以通过以下方式启动其他 activity:
在侧面的另一个 activity 之上:
图 4. activity A 在侧面的 activity B 之上启动 activity C。 在侧面启动一个 activity 并使分屏向一旁位移,从而隐藏之前的主要 activity:
图 5. activity B 在侧面启动 activity C,并使分屏向一旁位移。 在原来的 activity 之上原位启动一个 activity;即,在同一 activity 堆栈中:
图 6. activity B 启动 activity C,并且没有额外的 intent 标志。 在同一任务中启动一个 activity 全窗口:
图 7. activity A 或 activity B 启动 activity C,activity C 将填满任务窗口。
返回导航
不同类型的应用在拆分任务窗口状态下可以有不同的返回导航规则,具体取决于 activity 之间的依赖关系或用户如何触发返回事件,例如:
- 一起执行:如果 activity 相关,并且一个 activity 不应在没有另一个 activity 的情况下显示,则可以将返回导航配置为结束这两者。
- 单独执行:如果 activity 完全独立,则一个 activity 上的返回导航不影响任务窗口中另一个 activity 的状态。
使用按钮导航时,系统会将返回事件发送到上次聚焦的 activity。对于基于手势的导航,系统会将返回事件发送到发生手势的 activity。
多窗格布局
Jetpack WindowManager 1.0 Beta03 可让您在搭载 12L(API 级别 32)的大屏幕设备上和某些搭载早期平台版本的设备上构建包含 activity 的多窗格布局。基于多个 activity 而非 fragment 或基于视图的布局(如 SlidingPaneLayout
)的现有应用可以提供改进的大屏幕用户体验,而无需进行重大重构。
一个常见的示例是列表-详情分屏。为了确保高质量的呈现,系统先启动列表 activity,然后应用立即启动详情 activity。过渡系统等到这两个 activity 都绘制完成后再将它们一起显示出来。对用户来说,这两个 activity 作为一个启动。

分屏比
应用可以通过分屏配置的 ratio
属性来指定如何按比例划分任务窗口(请参阅下文的分屏配置)。

占位符
占位符 activity 是空的辅助 activity,这些 activity 占据 activity 分屏的一个区域。它们最终会替换为另一个包含内容的 activity。例如,一个占位符 activity 可以在列表详情布局中占据 activity 分屏的辅助一侧,直到用户从列表中选择了一项,此时一个包含选定列表项的详情的 activity 会替换该占位符。
仅当有足够的空间来显示分屏时,才会显示占位符。当显示大小变得宽度太小以至无法显示 activity 分屏时,占位符会自动结束,但当空间允许时,占位符会自动重新启动(处于重新初始化状态)。

窗口大小变化
当设备配置变更减小任务窗口宽度,使得宽度不够大而无法显示多窗格布局时(例如,当大屏幕可折叠设备从平板电脑大小折叠成手机大小时或者应用窗口在多窗口模式下调整大小时),任务窗口的辅助窗格中的非占位符 activity 会堆叠在主要窗格中的 activity 之上。
仅当有足够的显示宽度来显示分屏时,才会显示占位符 activity。在较小的屏幕上,系统会自动关闭占位符。当显示区域再次变得足够大时,系统会重新创建占位符。(请参阅上文的占位符。)
之所以能够堆叠 activity,是因为 WindowManager
会将辅助窗格中的 activity 的叠置顺序设置在主要窗格中的 activity 之上。
辅助窗格中的多个 activity
activity B 原位启动 activity C,并且没有额外的 intent 标志:
结果是同一任务中 activity 的叠置顺序如下:
因此,在较小的任务窗口中,应用会缩小到单个 activity,其中 activity C 在堆栈的顶部:
在较小的窗口中进行返回导航时,会沿着相互堆叠的 activity 原路返回。
如果任务窗口配置恢复为可以容纳多个窗格的较大大小,系统会再次并排显示 activity。
堆叠的分屏
activity B 在侧面启动 activity C,并使分屏向一旁位移:
结果是同一任务中 activity 的叠置顺序如下:
在较小的任务窗口中,应用会缩小到单个 activity,其中 activity C 在顶部:
分屏配置
WindowManager 库可以根据分屏规则创建容器和分屏。配置分屏规则涉及到下面几个步骤:
将 WindowManager 库依赖项添加到 build.gradle 文件中:
implementation("androidx.window:window:1.0.0-beta03")
创建一个具有以下用途的资源文件:
- 定义应使用过滤器拆分哪些 activity
- 为共享分屏的所有 activity 配置分屏选项
- 指定绝不应放置在分屏中的 activity
例如:
<!-- The split configuration for activities. --> <resources xmlns:window="http://schemas.android.com/apk/res-auto"> <!-- Automatically split the following activity pairs. --> <SplitPairRule window:splitRatio="0.3" window:splitMinWidth="600dp" window:finishPrimaryWithSecondary="true" window:finishSecondaryWithPrimary="true"> <SplitPairFilter window:primaryActivityName=".SplitActivityList" window:secondaryActivityName=".SplitActivityDetail"/> <SplitPairFilter window:primaryActivityName="*" window:secondaryActivityName="*/*" window:secondaryActivityAction="android.intent.action.VIEW"/> </SplitPairRule> <!-- Automatically launch a placeholder for the list activity. --> <SplitPlaceholderRule window:placeholderActivityName=".SplitActivityListPlaceholder" window:splitRatio="0.3" window:splitMinWidth="600dp"> <ActivityFilter window:activityName=".SplitActivityList"/> </SplitPlaceholderRule> </resources>
将规则定义通知库。
在本例中,我们使用 Jetpack Startup 库在加载应用的其他组件和启动 activity 之前执行初始化。如需启用启动功能,请在应用的 build 文件中添加库依赖项:
implementation("androidx.startup:startup-runtime:1.1.0")
并在应用清单中添加以下条目:
<!-- AndroidManifest.xml --> <provider android:name="androidx.startup.InitializationProvider" android:authorities="${applicationId}.androidx-startup" android:exported="false" tools:node="merge"> <!-- This entry makes ExampleWindowInitializer discoverable. --> <meta-data android:name="androidx.window.sample.embedding.ExampleWindowInitializer" android:value="androidx.startup" /> </provider>
最后,添加初始化程序类实现。
通过将包含定义 (
main_split_config
) 的 xml 资源文件的 ID 提供给SplitController.initialize()
来设置规则:Kotlin
class ExampleWindowInitializer : Initializer<SplitController> { override fun create(context: Context): SplitController { SplitController.initialize(context, R.xml.main_split_config) return SplitController.getInstance(context) } override fun dependencies(): List<Class<out Initializer<*>>> { return emptyList() } }
Java
class ExampleWindowInitializer extends Initializer<SplitController> { @Override SplitController create(Context context) { SplitController.initialize(context, R.xml.main_split_config); return SplitController.getInstance(context); } @Override List<Class<? extends Initializer<?>>> dependencies() { return emptyList(); } }
分屏示例
从全窗口分屏

无需重构。您可以静态地或在运行时定义分屏的配置,然后调用 Context#startActivity()
而不必指定任何额外的参数。
<SplitPairRule>
<SplitPairFilter
window:primaryActivityName=".A"
window:secondaryActivityName=".B"/>
</SplitPairRule>
默认分屏
如果应用的着陆页设计为在大屏幕上拆分成两个容器,当同时创建和呈现两个 activity 时,用户体验最佳。不过,在用户与主要容器中的 activity 互动(例如,用户从导航菜单中选择一项)之前,分屏的辅助容器可能没有可用的内容。占位符 activity 可以填补这一空白,直到可以在分屏的辅助容器中显示内容(请参阅上文的占位符)。

如需创建带有占位符的分屏,请创建一个占位符并将其与主要 activity 相关联:
<SplitPlaceholderRule
window:placeholderIntentName=".Placeholder">
<ActivityFilter
window:activityName=".Main"/>
</SplitPlaceholderRule>
深层链接分屏
当应用收到 intent 时,目标 activity 可以显示为 activity 分屏的辅助部分;例如,请求显示详情屏幕,该屏幕包含有关列表中某一项的信息。在小显示屏上,详情显示在完整的任务窗口中;在较大的设备上,详情显示在列表旁边。

启动请求应传送到主 activity,并且目标详情 activity 应在分屏中启动。SplitController
会根据可用的显示宽度自动选择正确的呈现方式(堆叠或并排)。
Kotlin
override fun onCreate(savedInstanceState Bundle?) { … splitController.registerRule(SplitPairRule(newFilters)) startActivity(Intent(this, DetailActivity::class.java)) }
Java
@Override protected void onCreate(@Nullable Bundle savedInstanceState) { … splitController.registerRule(new SplitPairRule(newFilters)); startActivity(new Intent(this, DetailActivity.class)); }
深层链接目的地可能是返回导航堆栈中应可供用户使用的唯一一个 activity,并且您可能希望避免关闭详情 activity 而只留下主 activity:
您可以使用 finishPrimaryWithSecondary
属性来同时结束这两个 activity:
<SplitPairRule
window:finishPrimaryWithSecondary="true">
<SplitPairFilter
window:primaryActivityName=".List"
window:secondaryActivityName=".Detail"/>
</SplitPairRule>
分屏容器中的多个 activity
将多个 activity 堆叠在分屏容器中使用户能够访问深层内容。例如,对于列表-详情分屏,用户可能需要进入子详情部分,但让主要 activity 留在原地:

Kotlin
class DetailActivity { … fun onOpenSubDetail() { startActivity(Intent(this, SubDetailActivity::class.java)) } }
Java
public class DetailActivity { … void onOpenSubDetail() { startActivity(new Intent(this, SubDetailActivity.class)); } }
子详情 activity 被置于详情 activity 之上,从而将详情 activity 隐藏起来:
然后,用户可以通过在堆栈中进行返回导航来回到之前的详情级别:

当从同一辅助容器中的一个 activity 启动多个 activity 时,相互堆叠 activity 是默认行为。从活跃分屏的主要容器中启动的 activity 最终也会在 activity 堆栈顶部的辅助容器中。
新任务中的 activity
当分屏任务窗口中的 activity 启动新任务中的 activity 时,新任务将与包含分屏的任务分开并显示在全窗口中。“最近使用的应用”屏幕显示两项任务:分屏中的任务和新任务。

activity 替换
可以在辅助容器堆栈中替换 activity;例如,当主要 activity 用于顶级导航而辅助 activity 是选定的目的地时。每当从顶级导航中选择一项时,都应在辅助容器中启动一个新的 activity,并移除之前在辅助容器中的一个或多个 activity。

如果在导航选择发生变化时应用未结束辅助容器中的 activity,那么在分屏收起后(设备折叠后),返回导航可能会令人感到困惑。例如,如果主要窗格中有一个菜单,并且屏幕 A 和屏幕 B 堆叠在辅助窗格中,当用户折叠手机时,屏幕 B 在屏幕 A 之上,屏幕 A 又在菜单之上。当用户从屏幕 B 进行返回导航时,系统会显示屏幕 A 而不是菜单。
在此类情况下,必须从返回堆栈中移除屏幕 A。
在现有分屏之上的新容器中启动到侧面时的默认行为是将新的辅助容器置于顶部,并将旧的辅助容器保留在返回堆栈中。您可以将分屏配置为通过 clearTop
清除之前的辅助容器,并正常启动新的 activity。
<SplitPairRule
window:clearTop="true">
<SplitPairFilter
window:primaryActivityName=".Menu"
window:secondaryActivityName=".ScreenA"/>
<SplitPairFilter
window:primaryActivityName=".Menu"
window:secondaryActivityName=".ScreenB"/>
</SplitPairRule>
Kotlin
class MenuActivity { … fun onMenuItemSelected(selectedMenuItem: Int) { startActivity(Intent(this, classForItem(selectedMenuItem))) } }
Java
public class MenuActivity { … void onMenuItemSelected(int selectedMenuItem) { startActivity(new Intent(this, classForItem(selectedMenuItem))); } }
或者,使用相同的辅助 activity,并从主要(菜单)activity 发送新的 intent,这些 intent 解析为相同的实例,但会在辅助容器中触发状态或界面更新。
多重分屏
应用可以通过在侧面启动额外的 activity 来提供多级深层导航。
当辅助容器中的 activity 在侧面启动一个新的 activity 时,系统会在现有分屏之上创建一个新的分屏。

返回堆栈包含之前打开的所有 activity,因此用户在结束 activity C 之后可以导航到 activity A/activity B 分屏。
如需创建新的分屏,请从现有辅助容器中在侧面启动新的 activity。声明 activity A/activity B 和 activity B/activity C 分屏的配置,并正常从 activity B 启动 activity C:
<SplitPairRule>
<SplitPairFilter
window:primaryActivityName=".A"
window:secondaryActivityName=".B"/>
<SplitPairFilter
window:primaryActivityName=".B"
window:secondaryActivityName=".C"/>
</SplitPairRule>
Kotlin
class B { fun onOpenC() { startActivity(Intent(this, C::class.java)) } }
Java
public class B { … void onOpenC() { startActivity(new Intent(this, C.class)); } }
响应分屏状态变化
应用中的不同 activity 可以具有执行相同功能的界面元素;例如,一个用于打开包含帐号设置的窗口的控件。

如果分屏中有两个 activity 具有共同的界面元素,那么这两个 activity 中都显示该元素就是多余的,而且可能会令人感到困惑。

为了知道 activity 何时在分屏中,请向 SplitController
注册一个监听器来监听分屏状态的变化。然后,相应地调整界面:
Kotlin
override fun onCreate(savedInstanceState: Bundle?) { splitController .addSplitListener(this, mainThreadExecutor, SplitInfoChangeCallback()) } inner class SplitInfoChangeCallback : Consumer<List<SplitInfo>> { override fun accept(splitInfoList: List<SplitInfo>) { findViewById<View>(R.id.infoButton).visibility = if (!splitInfoList.isEmpty()) View.GONE else View.VISIBLE } }
Java
@Override protected void onCreate(@Nullable Bundle savedInstanceState) { splitController .addSplitListener(this, mainThreadExecutor, SplitInfoChangeCallback()); } class SplitInfoChangeCallback extends Consumer<List<SplitInfo>> { public void accept(List<SplitInfo> splitInfoList) { findViewById<View>(R.id.infoButton).visibility = !splitInfoList.isEmpty()) ? View.GONE : View.VISIBLE; } }
可以在任何生命周期状态下进行回调,包括当 activity 停止时。通常应在 onStart()
中注册监听器,在 onStop()
中取消注册监听器。
全窗口模态
某些 activity 会阻止用户与应用互动,直到执行了指定的操作;例如,登录屏幕 activity、政策确认屏幕或错误消息。应防止模态 activity 出现在分屏中。
您可以使用展开配置来强制 activity 始终填满任务窗口:
<ActivityRule
window:alwaysExpand="true">
<ActivityFilter
window:activityName=".FullWidthActivity"/>
</ActivityRule>
结束 activity
用户可以通过从显示屏的边缘滑动,在分屏的任意一侧结束 activity:


如果设备设置为使用返回按钮而不是手势导航,则系统会将输入发送到聚焦的 activity,即上次轻触或启动的 activity。
结束分屏中的一个 activity 所产生的结果取决于分屏配置。
默认配置
当分屏中的一个 activity 结束时,剩下的 activity 会占据整个窗口:
<SplitPairRule>
<SplitPairFilter
window:primaryActivityName=".A"
window:secondaryActivityName=".B"/>
</SplitPairRule>
一起结束 activity
当辅助 activity 结束时,自动结束主要 activity:
<SplitPairRule
window:finishPrimaryWithSecondary="true">
<SplitPairFilter
window:primaryActivityName=".A"
window:secondaryActivityName=".B"/>
</SplitPairRule>
当主要 activity 结束时,自动结束辅助 activity:
<SplitPairRule
window:finishSecondaryWithPrimary="true">
<SplitPairFilter
window:primaryActivityName=".A"
window:secondaryActivityName=".B"/>
</SplitPairRule>
当主要 activity 或辅助 activity 结束时,一起结束 activity:
<SplitPairRule
window:finishPrimaryWithSecondary="true"
window:finishSecondaryWithPrimary="true">
<SplitPairFilter
window:primaryActivityName=".A"
window:secondaryActivityName=".B"/>
</SplitPairRule>
结束容器中的多个 activity
如果多个 activity 堆叠在分屏容器中,结束堆栈底层的 activity 时,不会自动结束它上面的 activity。
例如,如果辅助容器中有两个 activity,其中 activity C 在 activity B 之上:
并且分屏的配置由 activity A 和 activity B 的配置定义:
<SplitPairRule>
<SplitPairFilter
window:primaryActivityName=".A"
window:secondaryActivityName=".B"/>
</SplitPairRule>
那么,结束顶层 activity 时,会保留分屏。
结束辅助容器的底层(根)activity 时,不会移除它上面的 activity;因此,也会保留分屏。
也会执行关于一起结束 activity 的其他任何规则,如将辅助 activity 与主要 activity 一起结束:
<SplitPairRule
window:finishSecondaryWithPrimary="true">
<SplitPairFilter
window:primaryActivityName=".A"
window:secondaryActivityName=".B"/>
</SplitPairRule>
将分屏配置为一起结束主要 activity 和辅助 activity 时:
<SplitPairRule
window:finishPrimaryWithSecondary="true"
window:finishSecondaryWithPrimary="true">
<SplitPairFilter
window:primaryActivityName=".A"
window:secondaryActivityName=".B"/>
</SplitPairRule>
在运行时更改分屏属性
您不能更改当前活跃和可见的分屏的属性。更改分屏规则会影响其他 activity 启动和新容器,但不会影响现有和活跃分屏。
如需更改活跃分屏的属性,请结束分屏中侧面的一个或多个 activity,然后使用新配置再次启动到侧面。
将 activity 从分屏提取到全窗口
创建显示侧面 activity 全窗口的新配置,然后使用解析为同一实例的 intent 重新启动 activity。
在运行时检查分屏支持
activity 嵌入是 Android 12L(API 级别 32)的一项功能,但在一些搭载早期平台版本的设备上也可用。如需在运行时检查该功能是否可用,请使用 SplitController.isSplitSupported()
方法:
Kotlin
val splitController = SplitController.Companion.getInstance() if (splitController.isSplitSupported()) { // Device supports split activity features. }
Java
SplitController splitController = SplitController.Companion.getInstance(); if (splitController.isSplitSupported()) { // Device supports split activity features. }
如果不支持分屏,系统会在顶部启动 activity(遵循非 activity 嵌入模型)。
阻止系统替换
Android 设备的制造商(原始设备制造商 (OEM))可将 activity 嵌入作为设备系统的函数来实现。系统会为多 activity 应用指定分屏规则,从而替换应用的窗口行为。系统替换会强制多 activity 应用进入系统定义的 activity 嵌入模式。
系统 activity 嵌入可通过多窗格布局(例如 list-detail)增强应用呈现效果,而无需对应用进行任何更改。不过,系统的 activity 嵌入也可能会导致应用布局不正确、出现 bug 或与应用所实现的 activity 嵌入冲突。
您的应用可通过在应用清单文件中设置属性来阻止或允许系统 activity 嵌入,例如:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<property
android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE"
android:value="true|false" />
</application>
</manifest>
属性名称在 Jetpack WindowManager WindowProperties 对象中定义。如果您的应用实现了 activity 嵌入,或者您想阻止系统将其 activity 嵌入规则应用于您的应用,请将该值设为 false
;若想允许系统将系统定义的 activity 嵌入应用于您的应用,请将值设为 true
。
限制条件和注意事项
- 只有任务的托管应用(标识为任务中根 activity 的所有者)才能在任务中组织和嵌入其他 activity。如果支持嵌入和分屏的 activity 在属于其他应用的任务中运行,则嵌入和分屏将不适用于这些 activity。
- 只能在单个任务中组织 activity。在新任务中启动 activity 时,始终都会将其放置在所有现有分屏之外的新展开窗口中。
- 只能将同一进程中的 activity 组织和放置在分屏中。
SplitInfo
回调仅报告属于同一进程的 activity,因为无法知道其他进程中的 activity。 - 每对或单个 activity 规则仅适用于在注册该规则后发生的 activity 启动。目前无法更新现有分屏或其视觉属性。
- 分屏对过滤器配置必须与启动 activity 时使用的 intent 完全匹配。从应用进程中启动新的 activity 时会发生匹配,因此使用隐式 intent 时,可能不知道稍后在系统进程中解析的组件名称。如果在启动时不知道组件名称,可以改用通配符(“*/*”),系统会根据 intent 操作执行过滤。
- 目前无法在容器之间移动 activity,也无法在创建分屏后将 activity 移入和移出分屏。只有在启动具有匹配规则的新 activity 时,WindowManager 库才会创建分屏;当分屏容器中的最后一个 activity 结束时,分屏会被销毁。
- 当配置发生更改时可以重新启动 activity,因此在创建或移除了分屏以及 activity 边界发生更改时,activity 可以完全销毁之前的实例,并创建一个新的实例。因此,对于诸如从生命周期回调启动新 activity 之类的操作,应用开发者应格外小心。