Activity embedding 通过在两个 Activity 或同一 Activity 的两个实例之间拆分应用程序的任务窗口,来优化大屏设备上的应用显示效果。
如果您的应用包含多个 Activity,Activity embedding 可让您在平板电脑、可折叠设备和 ChromeOS 设备上提供更佳的用户体验。
Activity embedding 无需重构代码。您可以通过创建 XML 配置文件或调用 Jetpack WindowManager API 来确定应用如何显示其 Activity(并排或堆叠)。
系统会自动维护对小屏幕的支持。当您的应用在小屏幕设备上运行时,Activity 会垂直堆叠;在大屏幕上,Activity 则会并排显示。系统会根据您创建的配置确定显示方式,无需编写分支逻辑。
Activity embedding 可以适应设备方向的变化,并在可折叠设备上无缝运行,随着设备的折叠和展开自动堆叠或取消堆叠 Activity。
大多数运行 Android 12L(API 级别 32)及更高版本的大屏设备都支持 Activity embedding。
拆分任务窗口
Activity embedding 将应用的任务窗口拆分为两个容器:主容器和次容器。这些容器用于容纳从主 Activity 或已在容器中的其他 Activity 启动的 Activity。
当 Activity 被启动时,它们会在次容器中堆叠。在小屏幕上,次容器会堆叠在主容器上方,因此 Activity 的堆叠和返回导航顺序与您应用中预设的逻辑保持一致。
Activity embedding 支持多种显示方式。您的应用可以通过同时并排或上下启动两个 Activity 来拆分任务窗口。
占据整个任务窗口的 Activity 可以通过在旁边启动新 Activity 来创建拆分。
已经处于拆分状态并共享任务窗口的 Activity 可以通过以下方式启动其他 Activity:
在另一个 Activity 之上并排显示
图 4. Activity A 启动 Activity C,并排显示在 Activity B 之上。 在旁边显示,并侧向移动拆分区域,隐藏之前的 Activity。
图 23. Activity B 启动 Activity C,并排显示并侧向移动了拆分区域。 在当前位置上方启动 Activity;即在同一个 Activity 堆栈中。
图 6. Activity B 在不带额外 Intent 标志的情况下启动 Activity C。 在同一任务中以全窗口形式启动 Activity。
图 7. Activity A 或 B 启动 Activity C,使其充满整个任务窗口。
返回导航
根据 Activity 之间的依赖关系或用户触发返回事件的方式,不同类型的应用在拆分任务窗口状态下可能有不同的返回导航规则,例如:
- 共同进退:如果 Activity 彼此相关且缺一不可,则可以将返回导航配置为同时关闭两者。
- 各行其是:如果 Activity 完全独立,则一个 Activity 上的返回导航不会影响任务窗口中另一个 Activity 的状态。
使用按钮导航时,返回事件会发送到最后获得焦点的 Activity。
对于基于手势的导航
Android 14 (API 级别 34) 及更低版本 — 返回事件会发送到触发手势的 Activity。当用户从屏幕左侧滑动时,返回事件发送到拆分窗口左侧面板的 Activity。当用户从右侧滑动时,事件发送到右侧面板的 Activity。
Android 15 (API 级别 35) 及更高版本
处理来自同一应用的多个 Activity 时,无论滑动方向如何,手势都会结束最顶层的 Activity,从而提供更统一的体验。
在涉及来自不同应用的两个 Activity(覆盖)的情况下,返回事件会定向到最后获得焦点的 Activity,这与按钮导航的行为一致。
多面板布局
Jetpack WindowManager 允许您在运行 Android 12L (API 级别 32) 或更高版本的大屏设备以及某些具有较旧平台版本的设备上构建 Activity embedding 多面板布局。无需重构源代码,现有的基于多个 Activity(而非 Fragment 或 SlidingPaneLayout 等视图布局)的应用即可提供改进的大屏用户体验。
常见的例子是列表-详情拆分。为确保高质量的呈现,系统启动列表 Activity,随后应用立即启动详情 Activity。过渡系统会等待两个 Activity 都绘制完成,然后将它们一起显示。对用户而言,这两个 Activity 是同时启动的。
拆分属性
您可以指定任务窗口在拆分容器之间的比例,以及容器之间如何布局。
对于在 XML 配置文件中定义的规则,请设置以下属性:
splitRatio:设置容器比例。该值是一个开区间 (0.0, 1.0) 内的浮点数。splitLayoutDirection:指定拆分容器的相对布局方向。值包括:ltr:从左到右rtl:从右到左locale:根据语言区域设置决定是ltr还是rtl
示例请参阅 XML 配置部分。
对于使用 WindowManager API 创建的规则,请使用 SplitAttributes.Builder 创建 SplitAttributes 对象,并调用以下构建器方法:
setSplitType():设置拆分容器的比例。有关有效参数(包括SplitAttributes.SplitType.ratio()方法),请参阅SplitAttributes.SplitType。setLayoutDirection():设置容器的布局。有关可能的值,请参阅SplitAttributes.LayoutDirection。
示例请参阅 WindowManager API 部分。
拆分方向
显示屏的尺寸和宽高比决定了 Activity 在 Activity embedding 拆分中的位置。在大尺寸横屏显示器上,Activity 并排显示;在高尺寸竖屏显示器或处于桌面模式的可折叠设备上,它们则上下排列。
您可以使用 SplitController SplitAttributes 计算器来指定拆分方向。该计算器会为活动的 SplitRule 计算 SplitAttributes。
利用该计算器可以在不同的设备状态下以不同的方向拆分父容器,例如:
Kotlin
if (WindowSdkExtensions.getInstance().extensionVersion >= 2) { SplitController.getInstance(this).setSplitAttributesCalculator { params -> val parentConfiguration = params.parentConfiguration val builder = SplitAttributes.Builder() return@setSplitAttributesCalculator if (parentConfiguration.screenWidthDp >= 840) { // Side-by-side dual-pane layout for wide displays. builder .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE) .build() } else if (parentConfiguration.screenHeightDp >= 600) { // Horizontal split for tall displays. builder .setLayoutDirection(SplitAttributes.LayoutDirection.BOTTOM_TO_TOP) .build() } else { // Fallback to expand the secondary container. builder .setSplitType(SPLIT_TYPE_EXPAND) .build() } } }
Java
if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 2) { SplitController.getInstance(this).setSplitAttributesCalculator(params -> { Configuration parentConfiguration = params.getParentConfiguration(); SplitAttributes.Builder builder = new SplitAttributes.Builder(); if (parentConfiguration.screenWidthDp >= 840) { // Side-by-side dual-pane layout for wide displays. return builder .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE) .build(); } else if (parentConfiguration.screenHeightDp >= 600) { // Horizontal split for tall displays. return builder .setLayoutDirection(SplitAttributes.LayoutDirection.BOTTOM_TO_TOP) .build(); } else { // Fallback to expand the secondary container. return builder .setSplitType(SplitType.SPLIT_TYPE_EXPAND) .build(); } }); }
在可折叠设备上,如果设备处于横屏模式,您可以垂直拆分屏幕;如果是竖屏模式,则显示单个 Activity;如果处于桌面模式,则水平拆分屏幕。
Kotlin
if (WindowSdkExtensions.getInstance().extensionVersion >= 2) { SplitController.getInstance(this).setSplitAttributesCalculator { params -> val tag = params.splitRuleTag val parentWindowMetrics = params.parentWindowMetrics val parentConfiguration = params.parentConfiguration val foldingFeatures = params.parentWindowLayoutInfo.displayFeatures.filterIsInstance<FoldingFeature>() val feature = if (foldingFeatures.size == 1) foldingFeatures[0] else null val builder = SplitAttributes.Builder() builder.setSplitType(SPLIT_TYPE_HINGE) return@setSplitAttributesCalculator if (feature?.isSeparating == true) { // Horizontal split for tabletop posture. builder .setSplitType(SPLIT_TYPE_HINGE) .setLayoutDirection( if (feature.orientation == FoldingFeature.Orientation.HORIZONTAL) { SplitAttributes.LayoutDirection.BOTTOM_TO_TOP } else { SplitAttributes.LayoutDirection.LOCALE } ) .build() } else if (parentConfiguration.screenWidthDp >= 840) { // Side-by-side dual-pane layout for wide displays. builder .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE) .build() } else { // No split for tall displays. builder .setSplitType(SPLIT_TYPE_EXPAND) .build() } } }
Java
if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 2) { SplitController.getInstance(this).setSplitAttributesCalculator(params -> { String tag = params.getSplitRuleTag(); WindowMetrics parentWindowMetrics = params.getParentWindowMetrics(); Configuration parentConfiguration = params.getParentConfiguration(); List<FoldingFeature> foldingFeatures = params.getParentWindowLayoutInfo().getDisplayFeatures().stream().filter( item -> item instanceof FoldingFeature) .map(item -> (FoldingFeature) item) .collect(Collectors.toList()); FoldingFeature feature = foldingFeatures.size() == 1 ? foldingFeatures.get(0) : null; SplitAttributes.Builder builder = new SplitAttributes.Builder(); builder.setSplitType(SplitType.SPLIT_TYPE_HINGE); if (feature != null && feature.isSeparating()) { // Horizontal slit for tabletop posture. return builder .setSplitType(SplitType.SPLIT_TYPE_HINGE) .setLayoutDirection( feature.getOrientation() == FoldingFeature.Orientation.HORIZONTAL ? SplitAttributes.LayoutDirection.BOTTOM_TO_TOP : SplitAttributes.LayoutDirection.LOCALE) .build(); } else if (parentConfiguration.screenWidthDp >= 840) { // Side-by-side dual-pane layout for wide displays. return builder .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE) .build(); } else { // No split for tall displays. return builder .setSplitType(SplitType.SPLIT_TYPE_EXPAND) .build(); } }); }
占位符
占位符 Activity 是空的次级 Activity,占据 Activity 拆分的一个区域。它们最终旨在被包含内容的另一个 Activity 取代。例如,在列表-详情布局中,占位符 Activity 可以占据拆分的次级区域,直到选中列表中的项目,此时包含所选项详细信息的 Activity 会取代占位符。
默认情况下,系统仅在有足够空间进行 Activity 拆分时才显示占位符。当显示屏尺寸变化到过窄或过短而无法进行拆分时,占位符会自动结束。当空间恢复时,系统会以重置后的状态重新启动占位符。
但是,SplitPlaceholderRule 的 stickyPlaceholder 属性或 SplitPlaceholder.Builder 的 setSticky() 方法可以覆盖默认行为。当属性或方法指定值为 true 时,如果显示屏从双面板调整为单面板,系统会将占位符显示为任务窗口中最顶层的 Activity(示例请参见拆分配置)。
窗口尺寸变化
当设备配置变化导致任务窗口宽度不足以进行多面板布局时(例如,大屏可折叠设备从平板模式折叠为手机模式,或在多窗口模式下调整应用窗口大小),次级面板中非占位符的 Activity 会堆叠在主面板的 Activity 之上。
占位符 Activity 仅在有足够宽度时显示。在更小的屏幕上,占位符会自动被丢弃。当显示区域足够大时,占位符会再次创建。(参见占位符部分。)
Activity 堆叠之所以可能,是因为 WindowManager 会将次级面板中的 Activity 在 Z 轴上置于主面板 Activity 之上。
次级面板中的多个 Activity
Activity B 在不带额外 Intent 标志的情况下原地启动 Activity C

从而导致同一任务中的 Activity 具有以下 Z 轴顺序:

因此,在较小的任务窗口中,应用会缩减为单个 Activity,C 位于堆栈顶部。

在较小窗口中向后导航会穿过堆叠在一起的 Activity。
如果任务窗口配置恢复到足以容纳多个面板的大小时,Activity 会再次并排显示。
堆叠拆分
Activity B 在旁边启动 Activity C 并侧向移动拆分区域

结果是同一任务中的 Activity 具有以下 Z 轴顺序:

在较小的任务窗口中,应用缩减为单个 Activity,且 C 位于顶部。

固定竖屏方向
清单设置 android:screenOrientation 允许应用将 Activity 限制为竖屏或横屏方向。为了提升平板电脑和可折叠设备等大屏设备上的用户体验,设备制造商 (OEM) 可以忽略屏幕方向请求,并在横屏显示器上将竖屏应用添加黑边(或反之)。
同样,启用 Activity embedding 时,OEM 可以自定义设备,以便在大屏幕(宽度 ≥ 600dp)的横向模式下为固定竖屏的 Activity 添加黑边。当固定竖屏的 Activity 启动第二个 Activity 时,设备可以在双面板中并排显示这两个 Activity。
务必在应用清单文件中添加 android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED 属性,以通知设备您的应用支持 Activity embedding(参见拆分配置部分)。这样 OEM 定制设备就可以确定是否对固定竖屏的 Activity 添加黑边。
拆分配置
拆分规则用于配置 Activity 拆分。您可以在 XML 配置文件中或通过调用 Jetpack WindowManager API 来定义这些规则。
在这两种情况下,您的应用都必须访问 WindowManager 库,并通知系统该应用已实现 Activity embedding。
执行以下操作:
将最新的 WindowManager 库依赖项添加到应用的模块级
build.gradle文件中,例如:implementation 'androidx.window:window:1.1.0-beta02'WindowManager 库提供了 Activity embedding 所需的所有组件。
通知系统您的应用已实现 Activity embedding。
将
android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED属性添加到应用清单文件的 <application> 元素中,并将其值设为 true,例如:<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <application> <property android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED" android:value="true" /> </application> </manifest>在 WindowManager 1.1.0-alpha06 及更高版本中,除非在清单中添加该属性并将其设为 true,否则 Activity embedding 拆分将被禁用。
此外,设备制造商会利用此设置,为支持 Activity embedding 的应用启用自定义功能。例如,设备可以在横屏显示器上对仅支持竖屏的 Activity 添加黑边,以便在启动第二个 Activity 时调整 Activity 方向,以进行双面板布局(参见固定竖屏方向)。
XML 配置
要创建基于 XML 的 Activity embedding 实现,请完成以下步骤:
创建一个 XML 资源文件,用于:
- 定义共享拆分的 Activity
- 配置拆分选项
- 当内容不可用时,为拆分的次容器创建占位符
- 指定绝对不应参与拆分的 Activity
例如
<!-- main_split_config.xml --> <resources xmlns:window="http://schemas.android.com/apk/res-auto"> <!-- Define a split for the named activities. --> <SplitPairRule window:splitRatio="0.33" window:splitLayoutDirection="locale" window:splitMinWidthDp="840" window:splitMaxAspectRatioInPortrait="alwaysAllow" window:finishPrimaryWithSecondary="never" window:finishSecondaryWithPrimary="always" window:clearTop="false"> <SplitPairFilter window:primaryActivityName=".ListActivity" window:secondaryActivityName=".DetailActivity"/> </SplitPairRule> <!-- Specify a placeholder for the secondary container when content is not available. --> <SplitPlaceholderRule window:placeholderActivityName=".PlaceholderActivity" window:splitRatio="0.33" window:splitLayoutDirection="locale" window:splitMinWidthDp="840" window:splitMaxAspectRatioInPortrait="alwaysAllow" window:stickyPlaceholder="false"> <ActivityFilter window:activityName=".ListActivity"/> </SplitPlaceholderRule> <!-- Define activities that should never be part of a split. Note: Takes precedence over other split rules for the activity named in the rule. --> <ActivityRule window:alwaysExpand="true"> <ActivityFilter window:activityName=".ExpandedActivity"/> </ActivityRule> </resources>创建一个初始化程序 (Initializer)。
WindowManager 的
RuleController组件会解析 XML 配置文件并向系统提供规则。Jetpack Startup 库的Initializer会在应用启动时使 XML 文件对RuleController可用,从而确保规则在任何 Activity 启动时生效。要创建初始化程序,请执行以下操作:
将最新的 Jetpack Startup 库依赖项添加到您的模块级
build.gradle文件中,例如:implementation 'androidx.startup:startup-runtime:1.1.1'创建一个实现
Initializer接口的类。通过将 XML 配置文件(
main_split_config.xml)的 ID 传递给RuleController.parseRules()方法,该初始化程序可将拆分规则提供给RuleController。Kotlin
class SplitInitializer : Initializer<RuleController> { override fun create(context: Context): RuleController { return RuleController.getInstance(context).apply { setRules(RuleController.parseRules(context, R.xml.main_split_config)) } } override fun dependencies(): List<Class<out Initializer<*>>> { return emptyList() } }
Java
public class SplitInitializer implements Initializer<RuleController> { @NonNull @Override public RuleController create(@NonNull Context context) { RuleController ruleController = RuleController.getInstance(context); ruleController.setRules( RuleController.parseRules(context, R.xml.main_split_config) ); return ruleController; } @NonNull @Override public List<Class<? extends Initializer<?>>> dependencies() { return Collections.emptyList(); } }
为规则定义创建一个内容提供程序 (Content Provider)。
将
androidx.startup.InitializationProvider作为<provider>添加到您的应用清单文件中。包含对您的RuleController初始化程序实现(即SplitInitializer)的引用:<!-- AndroidManifest.xml --> <provider android:name="androidx.startup.InitializationProvider" android:authorities="${applicationId}.androidx-startup" android:exported="false" tools:node="merge"> <!-- Make SplitInitializer discoverable by InitializationProvider. --> <meta-data android:name="${applicationId}.SplitInitializer" android:value="androidx.startup" /> </provider>InitializationProvider会在应用onCreate()方法调用之前发现并初始化SplitInitializer。因此,当应用的主 Activity 启动时,拆分规则即已生效。
WindowManager API
您可以通过少数 API 调用以编程方式实现 Activity embedding。请在 Application 子类的 onCreate() 方法中进行这些调用,以确保规则在任何 Activity 启动前生效。
要以编程方式创建 Activity 拆分,请执行以下操作:
创建一个拆分规则。
创建一个用于标识共享拆分之 Activity 的
SplitPairFilter。Kotlin
val splitPairFilter = SplitPairFilter( ComponentName(this, ListActivity::class.java), ComponentName(this, DetailActivity::class.java), null )
Java
SplitPairFilter splitPairFilter = new SplitPairFilter( new ComponentName(this, ListActivity.class), new ComponentName(this, DetailActivity.class), null );
将过滤器添加到过滤器集合。
```Kotlin
val filterSet = setOf(splitPairFilter)
Java
Set<SplitPairFilter> filterSet = new HashSet<>(); filterSet.add(splitPairFilter);
为拆分创建布局属性。
Kotlin
val splitAttributes: SplitAttributes = SplitAttributes.Builder() .setSplitType(SplitAttributes.SplitType.ratio(0.33f)) .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT) .build()
Java
SplitAttributes splitAttributes = new SplitAttributes.Builder() .setSplitType(SplitAttributes.SplitType.ratio(0.33f)) .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT) .build();
SplitAttributes.Builder创建一个包含布局属性的对象。setSplitType():定义可用显示区域如何分配给每个 Activity 容器。比率拆分类型指定了分配给主容器的显示区域比例;次容器占据剩余的可用显示区域。setLayoutDirection():指定 Activity 容器如何相对于彼此布局(主容器优先)。
构建一个
SplitPairRule。Kotlin
val splitPairRule = SplitPairRule.Builder(filterSet) .setDefaultSplitAttributes(splitAttributes) .setMinWidthDp(840) .setMinSmallestWidthDp(600) .setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ratio(1.5f)) .setFinishPrimaryWithSecondary(SplitRule.FinishBehavior.NEVER) .setFinishSecondaryWithPrimary(SplitRule.FinishBehavior.ALWAYS) .setClearTop(false) .build()
Java
SplitPairRule splitPairRule = new SplitPairRule.Builder(filterSet) .setDefaultSplitAttributes(splitAttributes) .setMinWidthDp(840) .setMinSmallestWidthDp(600) .setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ratio(1.5f)) .setFinishPrimaryWithSecondary(SplitRule.FinishBehavior.NEVER) .setFinishSecondaryWithPrimary(SplitRule.FinishBehavior.ALWAYS) .setClearTop(false) .build();
SplitPairRule.Builder创建并配置规则:filterSet:包含拆分对过滤器,这些过滤器通过识别共享拆分的 Activity 来确定何时应用规则。setDefaultSplitAttributes():将布局属性应用于规则。setMinWidthDp():设置启用拆分的最小显示宽度(以与密度无关的像素 dp 为单位)。setMinSmallestWidthDp():设置无论设备方向如何,启用拆分所需的两个显示尺寸中较小者的最小值(以 dp 为单位)。setMaxAspectRatioInPortrait():设置显示 Activity 拆分的竖屏模式下的最大显示宽高比(高:宽)。如果竖屏宽高比超过此最大比例,无论屏幕宽度如何,拆分都将禁用。注意: 默认值为 1.4,这会导致大多数平板电脑的竖屏模式下 Activity 占据整个任务窗口。另请参阅SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT和setMaxAspectRatioInLandscape()。横屏的默认值为ALWAYS_ALLOW。setFinishPrimaryWithSecondary():设置关闭次容器中所有 Activity 对主容器中 Activity 的影响。NEVER指示当次容器中的所有 Activity 结束时,系统不应结束主容器中的 Activity(参见结束 Activity)。setFinishSecondaryWithPrimary():设置关闭主容器中所有 Activity 对次容器中 Activity 的影响。ALWAYS指示当主容器中的所有 Activity 结束时,系统应始终结束次容器中的 Activity(参见结束 Activity)。setClearTop():指定当在容器中启动新 Activity 时,是否结束次容器中的所有 Activity。false值指定新 Activity 堆叠在次容器中已有的 Activity 之上。
获取 WindowManager
RuleController的单例实例,并添加规则。Kotlin
val ruleController = RuleController.getInstance(this) ruleController.addRule(splitPairRule)
Java
RuleController ruleController = RuleController.getInstance(this); ruleController.addRule(splitPairRule);
当内容不可用时,为次容器创建占位符。
创建一个
ActivityFilter,标识占位符与其共享任务窗口拆分的 Activity。Kotlin
val placeholderActivityFilter = ActivityFilter( ComponentName(this, ListActivity::class.java), null )
Java
ActivityFilter placeholderActivityFilter = new ActivityFilter( new ComponentName(this, ListActivity.class), null );
将过滤器添加到过滤器集合。
Kotlin
val placeholderActivityFilterSet = setOf(placeholderActivityFilter)
Java
Set<ActivityFilter> placeholderActivityFilterSet = new HashSet<>(); placeholderActivityFilterSet.add(placeholderActivityFilter);
创建一个
SplitPlaceholderRule。Kotlin
val splitPlaceholderRule = SplitPlaceholderRule.Builder( placeholderActivityFilterSet, Intent(context, PlaceholderActivity::class.java) ).setDefaultSplitAttributes(splitAttributes) .setMinWidthDp(840) .setMinSmallestWidthDp(600) .setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ratio(1.5f)) .setFinishPrimaryWithPlaceholder(SplitRule.FinishBehavior.ALWAYS) .setSticky(false) .build()
Java
SplitPlaceholderRule splitPlaceholderRule = new SplitPlaceholderRule.Builder( placeholderActivityFilterSet, new Intent(this, PlaceholderActivity.class) ).setDefaultSplitAttributes(splitAttributes) .setMinWidthDp(840) .setMinSmallestWidthDp(600) .setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ratio(1.5f)) .setFinishPrimaryWithPlaceholder(SplitRule.FinishBehavior.ALWAYS) .setSticky(false) .build();
SplitPlaceholderRule.Builder创建并配置规则:placeholderActivityFilterSet:包含 Activity 过滤器,这些过滤器通过识别与占位符 Activity 关联的 Activity 来确定何时应用规则。Intent:指定占位符 Activity 的启动。setDefaultSplitAttributes():将布局属性应用于规则。setMinWidthDp():设置启用拆分的最小显示宽度(以 dp 为单位)。setMinSmallestWidthDp():设置无论设备方向如何,启用拆分所需的两个显示尺寸中较小者的最小值(以 dp 为单位)。setMaxAspectRatioInPortrait():设置显示 Activity 拆分的竖屏模式下的最大显示宽高比(高:宽)。注意: 默认值为 1.4,这会导致大多数平板电脑的竖屏模式下 Activity 占据任务窗口。另请参阅SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT和setMaxAspectRatioInLandscape()。横屏的默认值为ALWAYS_ALLOW。setFinishPrimaryWithPlaceholder():设置关闭占位符 Activity 对主容器中 Activity 的影响。ALWAYS 指示当占位符结束时,系统应始终结束主容器中的 Activity(参见结束 Activity)。setSticky():确定在小屏幕上,占位符 Activity 在首次以足够最小宽度的拆分形式出现后,是否应出现在 Activity 堆栈顶部。
将规则添加到 WindowManager
RuleController。Kotlin
ruleController.addRule(splitPlaceholderRule)
Java
ruleController.addRule(splitPlaceholderRule);
指定绝对不应参与拆分的 Activity。
创建一个
ActivityFilter,标识应始终占据整个任务显示区域的 Activity。Kotlin
val expandedActivityFilter = ActivityFilter( ComponentName(this, ExpandedActivity::class.java), null )
Java
ActivityFilter expandedActivityFilter = new ActivityFilter( new ComponentName(this, ExpandedActivity.class), null );
将过滤器添加到过滤器集合。
Kotlin
val expandedActivityFilterSet = setOf(expandedActivityFilter)
Java
Set<ActivityFilter> expandedActivityFilterSet = new HashSet<>(); expandedActivityFilterSet.add(expandedActivityFilter);
创建一个
ActivityRule。Kotlin
val activityRule = ActivityRule.Builder(expandedActivityFilterSet) .setAlwaysExpand(true) .build()
Java
ActivityRule activityRule = new ActivityRule.Builder( expandedActivityFilterSet ).setAlwaysExpand(true) .build();
ActivityRule.Builder创建并配置规则:expandedActivityFilterSet:包含 Activity 过滤器,这些过滤器通过识别您想要从拆分中排除的 Activity 来确定何时应用规则。setAlwaysExpand():指定 Activity 是否应充满整个任务窗口。
将规则添加到 WindowManager
RuleController。Kotlin
ruleController.addRule(activityRule)
Java
ruleController.addRule(activityRule);
跨应用嵌入
在 Android 13 (API 级别 33) 及更高版本中,应用可以嵌入来自其他应用的 Activity。跨应用(或跨 UID)的 Activity embedding 实现了来自多个 Android 应用的 Activity 的视觉集成。系统会将宿主应用的 Activity 和来自另一个应用的嵌入 Activity 并排或上下显示,与单应用 Activity embedding 一样。
例如,设置应用可以嵌入 WallpaperPicker 应用的壁纸选择器 Activity。
信任模型
嵌入来自其他应用的 Activity 的宿主进程能够重新定义嵌入 Activity 的呈现方式,包括尺寸、位置、裁剪和透明度。恶意宿主可能会利用此功能误导用户并进行点击劫持或其他 UI 伪装攻击。
为防止滥用跨应用 Activity embedding,Android 要求应用选择加入以允许嵌入其 Activity。应用可以将宿主指定为受信任或不受信任。
受信任的宿主
要允许其他应用嵌入并完全控制来自您应用的 Activity 的呈现,请在您的应用清单文件的 <activity> 或 <application> 元素的 android:knownActivityEmbeddingCerts 属性中指定宿主应用的 SHA-256 证书。
将 android:knownActivityEmbeddingCerts 的值设为字符串,
<activity
android:name=".MyEmbeddableActivity"
android:knownActivityEmbeddingCerts="@string/known_host_certificate_digest"
... />
或者若要指定多个证书,则设为字符串数组,
<activity
android:name=".MyEmbeddableActivity"
android:knownActivityEmbeddingCerts="@array/known_host_certificate_digests"
... />
引用如下资源:
<resources>
<string-array name="known_host_certificate_digests">
<item>cert1</item>
<item>cert2</item>
...
</string-array>
</resources>
应用所有者可以通过运行 Gradle signingReport 任务获取 SHA 证书摘要。证书摘要是不含分隔冒号的 SHA-256 指纹。有关更多信息,请参阅运行签名报告和验证您的客户端。
不受信任的宿主
要允许任何应用嵌入您应用的 Activity 并控制其呈现,请在应用清单的 <activity> 或 <application> 元素中指定 android:allowUntrustedActivityEmbedding 属性,例如:
<activity
android:name=".MyEmbeddableActivity"
android:allowUntrustedActivityEmbedding="true"
... />
该属性的默认值为 false,这会防止跨应用 Activity embedding。
自定义身份验证
为缓解不受信任的 Activity embedding 的风险,请创建一个自定义身份验证机制来验证宿主身份。如果您知道宿主证书,请使用 androidx.security.app.authenticator 库进行验证。如果宿主在嵌入您的 Activity 后通过了身份验证,您可以显示实际内容。否则,您可以通知用户该操作不被允许并阻止内容。
使用 Jetpack WindowManager 库中的 ActivityEmbeddingController#isActivityEmbedded() 方法检查宿主是否正在嵌入您的 Activity,例如:
Kotlin
fun isActivityEmbedded(activity: Activity): Boolean { return ActivityEmbeddingController.getInstance(this).isActivityEmbedded(activity) }
Java
boolean isActivityEmbedded(Activity activity) { return ActivityEmbeddingController.getInstance(context).isActivityEmbedded(activity); }
最小尺寸限制
Android 系统会对嵌入的 Activity 应用应用清单 <layout> 元素中指定的最小高度和宽度。如果应用未指定最小高度和宽度,则适用系统默认值 (sw220dp)。
如果宿主尝试将嵌入容器调整为小于最小尺寸的尺寸,嵌入容器会展开以占据整个任务边界。
<activity-alias>
为了使受信任或不受信任的 Activity embedding 与 <activity-alias> 元素一起工作,必须将 android:knownActivityEmbeddingCerts 或 android:allowUntrustedActivityEmbedding 应用于目标 Activity,而不是别名。系统服务器上验证安全性的策略基于目标上设置的标志,而非别名。
宿主应用
宿主应用实现跨应用 Activity embedding 的方式与实现单应用 Activity embedding 的方式相同。SplitPairRule 和 SplitPairFilter 或 ActivityRule 和 ActivityFilter 对象指定了嵌入的 Activity 和任务窗口拆分。拆分规则在 XML 中静态定义,或在运行时使用 Jetpack WindowManager API 调用。
如果宿主应用尝试嵌入一个未选择加入跨应用嵌入的 Activity,该 Activity 会占据整个任务边界。因此,宿主应用需要知道目标 Activity 是否允许跨应用嵌入。
如果嵌入的 Activity 在同一任务中启动了一个新 Activity,且该新 Activity 未选择加入跨应用嵌入,则该 Activity 会占据整个任务边界,而不是覆盖嵌入容器中的 Activity。
只要 Activity 在同一任务中启动,宿主应用就可以不受限制地嵌入自己的 Activity。
拆分示例
从全窗口拆分
无需重构。您可以静态或在运行时定义拆分配置,然后调用 Context#startActivity(),无需任何额外参数。
<SplitPairRule>
<SplitPairFilter
window:primaryActivityName=".A"
window:secondaryActivityName=".B"/>
</SplitPairRule>
默认拆分
当应用着陆页设计为在大屏幕上拆分为两个容器时,如果两个 Activity 同时创建并呈现,用户体验会最佳。但是,在用户与主容器中的 Activity 进行交互(例如从导航菜单中选择一个项目)之前,次容器可能没有内容。占位符 Activity 可以在内容显示在次容器中之前填补空白(参见占位符部分)。
要创建带有占位符的拆分,请创建一个占位符并将其与主 Activity 关联。
<SplitPlaceholderRule
window:placeholderActivityName=".PlaceholderActivity">
<ActivityFilter
window:activityName=".MainActivity"/>
</SplitPlaceholderRule>
深层链接拆分
当应用收到 Intent 时,目标 Activity 可以显示为 Activity 拆分的次要部分;例如,请求显示包含列表项详细信息的详情屏幕。在小屏幕上,详情显示在整个任务窗口中;在大设备上,显示在列表旁边。
启动请求应路由到主 Activity,目标详情 Activity 应在拆分中启动。系统根据可用的显示宽度自动选择正确的呈现方式——堆叠或并排。
Kotlin
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) RuleController.getInstance(this) .addRule(SplitPairRule.Builder(filterSet).build()) startActivity(Intent(this, DetailActivity::class.java)) }
Java
@Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); RuleController.getInstance(this) .addRule(new SplitPairRule.Builder(filterSet).build()); startActivity(new Intent(this, DetailActivity.class)); }
深层链接目标可能是用户在返回导航堆栈中唯一可用的 Activity,您可能希望避免关闭详情 Activity 而只留下主 Activity:


相反,您可以通过使用 finishPrimaryWithSecondary 属性同时关闭两个 Activity:
<SplitPairRule
window:finishPrimaryWithSecondary="always">
<SplitPairFilter
window:primaryActivityName=".ListActivity"
window:secondaryActivityName=".DetailActivity"/>
</SplitPairRule>
参见配置属性部分。
拆分容器中的多个 Activity
在拆分容器中堆叠多个 Activity 允许用户访问深度内容。例如,通过列表-详情拆分,用户可能需要进入子详情部分,但保留主 Activity 的位置:
Kotlin
class DetailActivity : AppCompatActivity() { fun onOpenSubdetail() { startActivity(Intent(this, SubdetailActivity::class.java)) } }
Java
public class DetailActivity extends AppCompatActivity { 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,当拆分折叠(设备折叠)时,返回导航可能会造成困惑。例如,如果您在主面板中有一个菜单,而在次面板中堆叠了屏幕 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
inner class MenuActivity : AppCompatActivity() { fun onMenuItemSelected(selectedMenuItem: Int) { startActivity(Intent(this, classForItem(selectedMenuItem))) } }
Java
public class MenuActivity extends AppCompatActivity{ void onMenuItemSelected(int selectedMenuItem) { startActivity(new Intent(this, classForItem(selectedMenuItem))); } }
或者,使用相同的次 Activity,并从主(菜单)Activity 发送解析为相同实例的新 Intent,但在次容器中触发状态或 UI 更新。
多个拆分
应用可以通过在旁边启动额外的 Activity 来提供多级深度导航。
当次容器中的 Activity 在旁边启动新 Activity 时,会在现有拆分之上创建一个新拆分。
返回堆栈包含之前打开的所有 Activity,因此用户可以在结束 C 后导航到 A/B 拆分。

要创建新拆分,请从现有的次容器在旁边启动新 Activity。声明 A/B 和 B/C 拆分的配置,并正常从 B 启动 Activity C:
<SplitPairRule>
<SplitPairFilter
window:primaryActivityName=".A"
window:secondaryActivityName=".B"/>
<SplitPairFilter
window:primaryActivityName=".B"
window:secondaryActivityName=".C"/>
</SplitPairRule>
Kotlin
class B : AppCompatActivity() { fun onOpenC() { startActivity(Intent(this, C::class.java)) } }
Java
public class B extends AppCompatActivity{ void onOpenC() { startActivity(new Intent(this, C.class)); } }
响应拆分状态更改
应用中不同的 Activity 可能具有执行相同功能的 UI 元素;例如,打开包含帐户设置窗口的控件。
如果两个具有公共 UI 元素的 Activity 处于拆分状态,在两个 Activity 中都显示该元素是冗余的,甚至可能造成困惑。
要了解 Activity 何时处于拆分状态,请检查 SplitController.splitInfoList 流,或通过 SplitControllerCallbackAdapter 注册侦听器以获取拆分状态的更改。然后,相应地调整 UI:
Kotlin
val layout = layoutInflater.inflate(R.layout.activity_main, null) val view = layout.findViewById<View>(R.id.infoButton) lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { splitController.splitInfoList(this@SplitDeviceActivity) // The activity instance. .collect { list -> view.visibility = if (list.isEmpty()) View.VISIBLE else View.GONE } } }
Java
@Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); new SplitControllerCallbackAdapter(SplitController.getInstance(this)) .addSplitListener( this, Runnable::run, splitInfoList -> { View layout = getLayoutInflater().inflate(R.layout.activity_main, null); layout.findViewById(R.id.infoButton).setVisibility( splitInfoList.isEmpty() ? View.VISIBLE : View.GONE); }); }
协程可以在任何生命周期状态下启动,但通常在 STARTED 状态下启动以节省资源(参见将 Kotlin 协程与生命周期感知组件结合使用了解更多信息)。
回调可以在任何生命周期状态下进行,包括 Activity 停止时。侦听器通常应在 onStart() 中注册,并在 onStop() 中注销。
全窗口模态
某些 Activity 会阻止用户与应用交互,直到执行特定操作;例如,登录屏幕 Activity、政策确认屏幕或错误消息。应防止模态 Activity 出现在拆分中。
可以使用展开配置强制 Activity 始终充满任务窗口。
<ActivityRule
window:alwaysExpand="true">
<ActivityFilter
window:activityName=".FullWidthActivity"/>
</ActivityRule>
结束 Activity
用户可以通过从显示屏边缘滑动来结束拆分任一侧的 Activity。
如果设备设置为使用返回按钮而不是手势导航,输入将发送到获得焦点的 Activity——即最后被触摸或启动的 Activity。
在一个容器中结束所有 Activity 对相对容器的影响取决于拆分配置。
配置属性
您可以指定拆分对规则属性,以配置关闭拆分一侧的所有 Activity 如何影响另一侧的 Activity。属性如下:
window:finishPrimaryWithSecondary— 关闭次容器中所有 Activity 如何影响主容器中的 Activitywindow:finishSecondaryWithPrimary— 关闭主容器中所有 Activity 如何影响次容器中的 Activity
这些属性的可能值包括:
always— 始终结束关联容器中的 Activitynever— 从不结束关联容器中的 Activityadjacent— 当两个容器相邻显示时结束关联容器中的 Activity,但不堆叠时不会结束
例如
<SplitPairRule
<!-- Do not finish primary container activities when all secondary container activities finish. -->
window:finishPrimaryWithSecondary="never"
<!-- Finish secondary container activities when all primary container activities finish. -->
window:finishSecondaryWithPrimary="always">
<SplitPairFilter
window:primaryActivityName=".A"
window:secondaryActivityName=".B"/>
</SplitPairRule>
默认配置
当拆分中一个容器的所有 Activity 结束时,剩余的容器占据整个窗口。
<SplitPairRule>
<SplitPairFilter
window:primaryActivityName=".A"
window:secondaryActivityName=".B"/>
</SplitPairRule>


同时结束 Activity
当次容器中的所有 Activity 结束时,自动结束主容器中的 Activity。
<SplitPairRule
window:finishPrimaryWithSecondary="always">
<SplitPairFilter
window:primaryActivityName=".A"
window:secondaryActivityName=".B"/>
</SplitPairRule>


当主容器中的所有 Activity 结束时,自动结束次容器中的 Activity。
<SplitPairRule
window:finishSecondaryWithPrimary="always">
<SplitPairFilter
window:primaryActivityName=".A"
window:secondaryActivityName=".B"/>
</SplitPairRule>


当主容器或次容器中的所有 Activity 结束时,同时结束 Activity。
<SplitPairRule
window:finishPrimaryWithSecondary="always"
window:finishSecondaryWithPrimary="always">
<SplitPairFilter
window:primaryActivityName=".A"
window:secondaryActivityName=".B"/>
</SplitPairRule>


结束容器中的多个 Activity
如果多个 Activity 堆叠在拆分容器中,结束堆栈底部的 Activity 不会自动结束顶部的 Activity。
例如,如果两个 Activity 在次容器中,C 在 B 之上,

且拆分的配置由 Activity A 和 B 的配置定义,
<SplitPairRule>
<SplitPairFilter
window:primaryActivityName=".A"
window:secondaryActivityName=".B"/>
</SplitPairRule>
结束顶部 Activity 会保留拆分。

结束次容器的底部(根)Activity 不会移除其上的 Activity;因此,也保留了拆分。

任何关于同时结束 Activity 的其他规则(例如将次 Activity 与主 Activity 一起结束)也会执行。
<SplitPairRule
window:finishSecondaryWithPrimary="always">
<SplitPairFilter
window:primaryActivityName=".A"
window:secondaryActivityName=".B"/>
</SplitPairRule>

当拆分配置为同时结束主 Activity 和次 Activity 时,
<SplitPairRule
window:finishPrimaryWithSecondary="always"
window:finishSecondaryWithPrimary="always">
<SplitPairFilter
window:primaryActivityName=".A"
window:secondaryActivityName=".B"/>
</SplitPairRule>



在运行时更改拆分属性
处于活动和可见状态的拆分的属性无法更改。更改拆分规则会影响后续的 Activity 启动和新容器,但不会影响现有的、活动的拆分。
要更改活动拆分的属性,请结束拆分中的侧向 Activity,并使用新配置再次在旁边启动。
动态拆分属性
Jetpack WindowManager 1.4 及更高版本支持 Android 15 (API 级别 35) 及更高版本,提供动态功能,使 Activity embedding 拆分具有可配置性,包括:
- 面板扩展:一个可交互的、可拖动的分隔符,允许用户调整拆分显示中面板的大小。
- Activity 堆栈锁定:用户可以锁定其中一个容器中的内容,并将该容器中的导航与另一个容器中的导航隔离。
- 对话框全屏变暗:显示对话框时,应用可以指定是让整个任务窗口变暗,还是仅让打开对话框的容器变暗。
面板扩展
面板扩展允许用户调整双面板布局中分配给两个 Activity 的屏幕空间量。
要自定义窗口分隔符的外观并设置分隔符的可拖动范围,请执行以下操作:
创建
DividerAttributes的实例。自定义分隔符属性:
color:可拖动面板分隔符的颜色。widthDp:可拖动面板分隔符的宽度。设为WIDTH_SYSTEM_DEFAULT,让系统确定分隔符宽度。拖动范围:任一面板可以占据的屏幕最小百分比。范围为 0.33 到 0.66。设为
DRAG_RANGE_SYSTEM_DEFAULT,让系统确定拖动范围。
Kotlin
val splitAttributesBuilder: SplitAttributes.Builder = SplitAttributes.Builder() .setSplitType(SplitAttributes.SplitType.ratio(0.33f)) .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT) if (WindowSdkExtensions.getInstance().extensionVersion >= 6) { splitAttributesBuilder.setDividerAttributes( DividerAttributes.DraggableDividerAttributes.Builder() .setColor(getColor(R.color.divider_color)) .setWidthDp(4) .setDragRange(DividerAttributes.DragRange.DRAG_RANGE_SYSTEM_DEFAULT) .build() ) } val splitAttributes: SplitAttributes = splitAttributesBuilder.build()
Java
SplitAttributes.Builder splitAttributesBuilder = new SplitAttributes.Builder() .setSplitType(SplitAttributes.SplitType.ratio(0.33f)) .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT); if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 6) { splitAttributesBuilder.setDividerAttributes( new DividerAttributes.DraggableDividerAttributes.Builder() .setColor(ContextCompat.getColor(this, R.color.divider_color)) .setWidthDp(4) .setDragRange(DividerAttributes.DragRange.DRAG_RANGE_SYSTEM_DEFAULT) .build() ); } SplitAttributes _splitAttributes = splitAttributesBuilder.build();
Activity 堆栈锁定
Activity 堆栈锁定允许用户锁定其中一个拆分窗口,以便在用户在另一个窗口中导航时 Activity 保持原样。Activity 堆栈锁定提供了增强的多任务体验。
要在您的应用中启用 Activity 堆栈锁定,请执行以下操作:
在要锁定的 Activity 的布局文件中添加一个按钮,例如列表-详情布局中的详情 Activity。
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/detailActivity" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/white" tools:context=".DetailActivity"> <TextView android:id="@+id/textViewItemDetail" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="36sp" android:textColor="@color/obsidian" app:layout_constraintBottom_toTopOf="@id/pinButton" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <androidx.appcompat.widget.AppCompatButton android:id="@+id/pinButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/pin_this_activity" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/textViewItemDetail"/> </androidx.constraintlayout.widget.ConstraintLayout>在该 Activity 的
onCreate()方法中,为按钮设置点击侦听器。Kotlin
val pinButton: Button = findViewById(R.id.pinButton) pinButton.setOnClickListener { val splitAttributes: SplitAttributes = SplitAttributes.Builder() .setSplitType(SplitAttributes.SplitType.ratio(0.66f)) .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT) .build() val pinSplitRule = SplitPinRule.Builder() .setSticky(true) .setDefaultSplitAttributes(splitAttributes) .build() SplitController.getInstance(applicationContext) .pinTopActivityStack(taskId, pinSplitRule) }
Java
Button pinButton = findViewById(R.id.pinButton); pinButton.setOnClickListener( (view) -> { SplitAttributes splitAttributes = new SplitAttributes.Builder() .setSplitType(SplitAttributes.SplitType.ratio(0.66f)) .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT) .build(); SplitPinRule pinSplitRule = new SplitPinRule.Builder() .setSticky(true) .setDefaultSplitAttributes(splitAttributes) .build(); SplitController.getInstance(getApplicationContext()) .pinTopActivityStack(getTaskId(), pinSplitRule); });
对话框全屏变暗
Activity 通常会调暗显示内容以引起对对话框的注意。在 Activity embedding 中,双面板显示器的两个面板都应变暗,而不仅仅是包含打开对话框的 Activity 的面板,以获得统一的 UI 体验。
使用 WindowManager 1.4 及更高版本,对话框打开时,整个应用窗口默认会变暗(参见 EmbeddingConfiguration.DimAreaBehavior.ON_TASK)。
要仅使打开对话框的 Activity 容器变暗,请使用 EmbeddingConfiguration.DimAreaBehavior.ON_ACTIVITY_STACK。
将 Activity 从拆分提取为全窗口
创建新的配置,使侧向 Activity 显示为全窗口,然后使用解析为相同实例的 Intent 重新启动该 Activity。
在运行时检查拆分支持
Activity embedding 在 Android 12L (API 级别 32) 及更高版本上受支持,但也适用于某些运行较早平台版本的设备。要检查该功能在运行时是否可用,请使用 SplitController.splitSupportStatus 属性或 SplitController.getSplitSupportStatus() 方法。
Kotlin
if (SplitController.getInstance(this).splitSupportStatus == SplitController.SplitSupportStatus.SPLIT_AVAILABLE ) { // Device supports split activity features. }
Java
if (SplitController.getInstance(this).getSplitSupportStatus() == SplitController.SplitSupportStatus.SPLIT_AVAILABLE) { // Device supports split activity features. }
如果不支持拆分,Activity 会在 Activity 堆栈顶部启动(遵循非 Activity embedding 模型)。
防止系统覆盖
Android 设备制造商(原始设备制造商或 OEM)可以将 Activity embedding 实现为设备系统的一项功能。系统会为多 Activity 应用指定拆分规则,覆盖应用的窗口行为。系统覆盖会将多 Activity 应用强制进入系统定义的 Activity embedding 模式。
系统 Activity embedding 可以通过多面板布局(例如 列表-详情)增强应用呈现效果,而无需更改应用。但是,系统的 Activity embedding 也可能导致错误的布局、错误或与应用实现的 Activity embedding 冲突。
您的应用可以通过在应用清单文件中设置 PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE 来防止或允许系统 Activity embedding,例如:
<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 embedding,或者如果您希望防止系统对其应用 Activity embedding 规则,请将值设为 false。若要允许系统应用其定义的 Activity embedding,请将值设为 true。
限制、约束和注意事项
- 只有任务的宿主应用(被标识为任务中根 Activity 的所有者)才能在任务中组织和嵌入其他 Activity。如果支持嵌入和拆分的 Activity 在属于不同应用的任务中运行,则这些 Activity 的嵌入和拆分将无法工作。
- Activity 只能在单个任务内组织。在新任务中启动 Activity 总是会将其放入现有拆分之外的新展开窗口中。
- 只有同一进程中的 Activity 才能被组织并放入拆分中。
SplitInfo回调仅报告属于同一进程的 Activity,因为无法获知不同进程中的 Activity。 - 每个对或单个 Activity 规则仅适用于规则注册后发生的 Activity 启动。目前无法更新现有的拆分或其视觉属性。
- 拆分对过滤器配置必须与启动 Activity 时使用的 Intent 完全匹配。匹配发生在应用进程启动新 Activity 时,因此使用隐式 Intent 时可能不知道在系统进程中稍后解析的组件名称。如果启动时不知道组件名称,可以使用通配符 ("*/*"),并根据 Intent 操作进行过滤。
- 目前无法在 Activity 创建后将其在容器之间移动,或移入/移出拆分。拆分仅在启动具有匹配规则的新 Activity 时由 WindowManager 库创建,并在拆分容器中的最后一个 Activity 结束时销毁。
- Activity 在配置更改时可能会重新启动,因此当创建或移除拆分且 Activity 边界改变时,Activity 可能会经历前一个实例的完全销毁和新实例的创建。因此,应用开发者应谨慎处理从生命周期回调启动新 Activity 等操作。
- 设备必须包含窗口扩展接口才能支持 Activity embedding。几乎所有运行 Android 12L (API 级别 32) 或更高版本的大屏设备都包含该接口。但是,一些无法运行多个 Activity 的大屏设备不包含窗口扩展接口。如果大屏设备不支持多窗口模式,它可能不支持 Activity embedding。
其他资源
- Codelabs
- 学习路径 — Activity embedding
- 示例应用 — activity-embedding