活动嵌入

活动嵌入通过在两个活动或同一活动的两个实例之间拆分应用程序的任务窗口来优化大型屏幕设备上的应用。

图 1. 设置应用,活动并排显示。

如果您的应用包含多个活动,则活动嵌入使您能够在平板电脑、折叠屏手机和 Chrome OS 设备上提供增强的用户体验。

活动嵌入不需要代码重构。您可以通过创建 XML 配置文件或进行Jetpack WindowManager API 调用来确定应用如何显示其活动——并排或堆叠。

对小屏幕的支持会自动维护。当您的应用位于小屏幕设备上时,活动会一个叠一个地堆叠。在大屏幕上,活动并排显示。系统根据您创建的配置确定表示方式——无需分支逻辑。

活动嵌入可适应设备方向更改,并在折叠屏设备上无缝工作,在设备折叠和展开时堆叠和取消堆叠活动。

大多数运行 Android 12L(API 级别 32)及更高版本的较大屏幕设备都支持活动嵌入。

拆分任务窗口

活动嵌入将应用任务窗口拆分为两个容器:主容器和辅助容器。容器保存从主活动或已在容器中的其他活动启动的活动。

活动在启动时堆叠在辅助容器中,辅助容器在小屏幕上堆叠在主容器之上,因此活动堆叠和返回导航与已内置到应用中的活动顺序一致。

活动嵌入使您能够以各种方式显示活动。您的应用可以通过同时并排启动两个活动来拆分任务窗口

图 2. 两个活动并排显示。

或者,占据整个任务窗口的活动可以通过在旁边启动一个新活动来创建拆分

图 3. 活动 A 在旁边启动活动 B。

已在拆分中并共享任务窗口的活动可以通过以下方式启动其他活动

  • 在另一个活动之上的一侧

    图 4. 活动 A 在活动 B 上方的一侧启动活动 C。
  • 在侧面,并向侧面移动拆分,隐藏之前的活动

    图 5. 活动 B 在一侧启动活动 C 并向侧面移动拆分。
  • 在顶部就地启动活动;也就是说,在同一个活动堆栈中

    图 6. 活动 B 启动活动 C,没有额外的意图标志。
  • 在同一任务中以全窗口方式启动活动

    图 7. 活动 A 或活动 B 启动填充任务窗口的活动 C。

返回导航

不同类型的应用程序在拆分任务窗口状态下可以具有不同的返回导航规则,具体取决于活动之间的依赖关系或用户触发返回事件的方式,例如

  • 一起返回:如果活动相关,并且一个活动不应在没有另一个活动的情况下显示,则可以将返回导航配置为同时完成两者。
  • 单独返回:如果活动完全独立,则活动上的返回导航不会影响任务窗口中其他活动的状态。

使用按钮导航时,返回事件将发送到最后一个获得焦点的活动。

对于基于手势的导航

  • Android 14(API 级别 34)及更低版本 - 返回事件将发送到发生手势的活动。当用户从屏幕左侧滑动时,返回事件将发送到拆分窗口左侧窗格中的活动。当用户从屏幕右侧滑动时,返回事件将发送到右侧窗格中的活动。

  • Android 15(API 级别 35)及更高版本

    • 在处理来自同一应用的多个活动时,无论滑动方向如何,手势都会完成顶部活动,从而提供更统一的体验。

    • 在涉及来自不同应用的两个活动(叠加)的场景中,返回事件将定向到最后一个获得焦点的活动,这与按钮导航的行为一致。

多窗格布局

Jetpack WindowManager 使您能够在运行 Android 12L(API 级别 32)或更高版本的较大屏幕设备上以及某些运行早期平台版本的设备上构建活动嵌入多窗格布局。基于多个活动而不是片段或基于视图的布局(例如SlidingPaneLayout)的现有应用可以在不重构源代码的情况下提供改进的大屏幕用户体验。

一个常见的示例是列表-详细信息拆分。为了确保高质量的呈现,系统启动列表活动,然后应用程序立即启动详细信息活动。转换系统等待两个活动都绘制完毕,然后将它们一起显示。对于用户而言,这两个活动作为一个活动启动。

图 8. 在多窗格布局中同时启动的两个活动。

拆分属性

您可以指定任务窗口如何在拆分容器之间分配,以及容器如何彼此布局。

对于在 XML 配置文件中定义的规则,请设置以下属性

  • splitRatio:设置容器比例。该值是在开区间 (0.0, 1.0) 中的浮点数。
  • splitLayoutDirection:指定拆分容器如何彼此布局。值包括
    • ltr:从左到右
    • rtl:从右到左
    • localeltrrtl 由区域设置确定

有关示例,请参阅XML 配置 部分。

对于使用 WindowManager API 创建的规则,请使用SplitAttributes 创建一个对象SplitAttributes.Builder 并调用以下构建器方法

有关示例,请参阅WindowManager API 部分。

图 9. 两个活动拆分从左到右布局,但拆分比例不同。

占位符

占位符活动是占用活动拆分区域的空辅助活动。它们最终旨在替换为包含内容的另一个活动。例如,占位符活动可以在列表-详细信息布局中占据活动拆分的辅助侧,直到选择列表中的项目,此时包含所选列表项目详细信息的活动将替换占位符。

默认情况下,系统仅在有足够空间进行活动拆分时显示占位符。当显示尺寸更改为过小而无法显示拆分时,占位符会自动完成。当空间允许时,系统会使用重新初始化的状态重新启动占位符。

图 10. 可折叠设备折叠和展开。占位符活动在显示尺寸更改时完成并重新创建。

但是,SplitPlaceholderRulestickyPlaceholder 属性或 setSticky() 方法(SplitPlaceholder.Builder)可以覆盖默认行为。当属性或方法指定值为 true 时,系统会在显示屏从双窗格显示调整为单窗格显示时(例如,参见 分割配置),将占位符显示为任务窗口中最顶层的 Activity。

图 11. 可折叠设备折叠和展开。占位符 Activity 是粘性的。

窗口大小变化

当设备配置更改导致任务窗口宽度减小,以至于不足以容纳多窗格布局时(例如,当大型屏幕可折叠设备从平板电脑尺寸折叠成手机尺寸,或应用程序窗口在多窗口模式下调整大小时),任务窗口辅助窗格中的非占位符 Activity 会堆叠在主窗格中的 Activity 之上。

仅当有足够的显示宽度用于分割时,才会显示占位符 Activity。在较小的屏幕上,占位符会自动关闭。当显示区域再次足够大时,占位符会重新创建。(请参阅 占位符 部分。)

Activity 堆叠之所以成为可能,是因为 WindowManager 将辅助窗格中的 Activity 的 z 顺序排在主窗格中的 Activity 之上。

辅助窗格中的多个 Activity

Activity B 在不使用额外 Intent 标志的情况下启动 Activity C

Activity split containing activities A, B, and C with C stacked on
          top of B.

导致在同一任务中以下 Activity 的 z 顺序

Secondary activity stack containing activity C stacked on top of B.
          Secondary stack is stacked on top of prmary activity stack
          containing activity A.

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

Small window showing only activity C.

在较小的窗口中向后导航会遍历彼此堆叠的 Activity。

如果任务窗口配置恢复为可以容纳多个窗格的较大尺寸,则 Activity 将再次并排显示。

堆叠分割

Activity B 在旁边启动 Activity C 并将分割向侧面移动

Task window showing activities A and B, then activities B and C.

结果是在同一任务中以下 Activity 的 z 顺序

Activities A, B, and C in a single stack. The activities are stacked
          in the following order from top to bottom: C, B, A.

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

Small window showing only activity C.

固定纵向方向

通过 android:screenOrientation 清单设置,应用程序可以将 Activity 限制为纵向或横向方向。为了改善大型屏幕设备(如平板电脑和可折叠设备)上的用户体验,设备制造商 (OEM) 可以忽略屏幕方向请求,并在横向显示屏上将应用程序以纵向方向显示,或在纵向显示屏上以横向方向显示。

图 12. 带黑边的 Activity:横向设备上的固定纵向(左),纵向设备上的固定横向(右)。

类似地,启用 Activity 嵌入后,OEM 可以自定义设备,以便在大型屏幕(宽度 ≥ 600dp)上以横向方向显示固定纵向 Activity 的黑边。当固定纵向 Activity 启动第二个 Activity 时,设备可以在双窗格显示中并排显示这两个 Activity。

图 13. 固定纵向 Activity A 在旁边启动 Activity B。

始终将 android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED 属性添加到应用程序清单文件中,以告知设备您的应用程序支持 Activity 嵌入(请参阅 分割配置 部分)。然后,OEM 自定义的设备可以确定是否显示固定纵向 Activity 的黑边。

分割配置

分割规则配置 Activity 分割。您可以在 XML 配置文件中定义分割规则,或通过调用 Jetpack WindowManager API 来定义。

在这两种情况下,您的应用程序都必须访问 WindowManager 库,并必须告知系统应用程序已实现 Activity 嵌入。

执行以下操作

  1. 将最新的 WindowManager 库依赖项添加到应用程序的模块级 build.gradle 文件中,例如

    implementation 'androidx.window:window:1.1.0-beta02'

    WindowManager 库提供了 Activity 嵌入所需的所有组件。

  2. 告知系统您的应用程序已实现 Activity 嵌入。

    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 嵌入分割将被禁用。

    此外,设备制造商使用此设置为支持 Activity 嵌入的应用程序启用自定义功能。例如,设备可以在横向显示屏上显示仅纵向 Activity 的黑边,以便在启动第二个 Activity 时使 Activity 能够过渡到双窗格布局(请参阅 固定纵向方向)。

XML 配置

要创建 Activity 嵌入的基于 XML 的实现,请完成以下步骤

  1. 创建一个 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>
    
  2. 创建初始化程序。

    WindowManager RuleController 组件会解析 XML 配置文件,并将规则提供给系统。Jetpack StartupInitializer 会在应用程序启动时将 XML 文件提供给 RuleController,以便在任何 Activity 启动时规则都生效。

    要创建初始化程序,请执行以下操作

    1. 将最新的 Jetpack Startup 库依赖项添加到模块级 build.gradle 文件中,例如

      implementation 'androidx.startup:startup-runtime:1.1.1'

    2. 创建一个实现 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();
           }
      }
  3. 为规则定义创建内容提供程序。

    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 嵌入。在 Application 的子类的 onCreate() 方法中进行调用,以确保在任何 Activity 启动之前规则都生效。

要以编程方式创建 Activity 分割,请执行以下操作

  1. 创建分割规则

    1. 创建一个 SplitPairFilter,用于识别共享分割的 Activity

      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
      );
    2. 将筛选器添加到筛选器集中

      Kotlin

      val filterSet = setOf(splitPairFilter)

      Java

      Set<SplitPairFilter> filterSet = new HashSet<>();
      filterSet.add(splitPairFilter);
    3. 为分割创建布局属性

      Kotlin

      val splitAttributes: SplitAttributes = SplitAttributes.Builder()
          .setSplitType(SplitAttributes.SplitType.ratio(0.33f))
          .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
          .build()

      Java

      final SplitAttributes splitAttributes = new SplitAttributes.Builder()
            .setSplitType(SplitAttributes.SplitType.ratio(0.33f))
            .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
            .build();

      SplitAttributes.Builder 创建一个包含布局属性的对象

      • setSplitType():定义如何将可用显示区域分配给每个 Activity 容器。比例分割类型指定分配给主容器的可用显示区域的比例;辅助容器占用可用显示区域的其余部分。
      • setLayoutDirection():指定 Activity 容器如何相对于彼此布局,主容器位于首位。
    4. 构建一个 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_DEFAULTsetMaxAspectRatioInLandscape()。横向的默认值为 ALWAYS_ALLOW
      • setFinishPrimaryWithSecondary():设置辅助容器中的所有 Activity 完成后如何影响主容器中的 Activity。NEVER 表示当辅助容器中的所有 Activity 完成时,系统不应完成主 Activity(请参阅 完成 Activity)。

      • setFinishSecondaryWithPrimary():设置主容器中所有活动完成时如何影响辅助容器中的活动。ALWAYS 表示当主容器中的所有活动都完成时,系统应始终完成辅助容器中的活动(请参阅完成活动)。
      • setClearTop():指定在辅助容器中启动新活动时,是否会完成辅助容器中的所有活动。 false 值指定新活动将堆叠在辅助容器中已有的活动之上。
    5. 获取 WindowManager RuleController 的单例实例,并添加规则

      Kotlin

        val ruleController = RuleController.getInstance(this)
        ruleController.addRule(splitPairRule)
        

      Java

        RuleController ruleController = RuleController.getInstance(this);
        ruleController.addRule(splitPairRule);
        
  2. 在内容不可用时,为辅助容器创建占位符

    1. 创建一个ActivityFilter,用于识别与占位符共享任务窗口拆分的活动

      Kotlin

      val placeholderActivityFilter = ActivityFilter(
          ComponentName(this, ListActivity::class.java),
          null
      )

      Java

      ActivityFilter placeholderActivityFilter = new ActivityFilter(
          new ComponentName(this, ListActivity.class),
          null
      );
    2. 将筛选器添加到筛选器集中

      Kotlin

      val placeholderActivityFilterSet = setOf(placeholderActivityFilter)

      Java

      Set<ActivityFilter> placeholderActivityFilterSet = new HashSet<>();
      placeholderActivityFilterSet.add(placeholderActivityFilter);
    3. 创建一个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(context, PlaceholderActivity.class)
          ).setDefaultSplitAttributes(splitAttributes)
           .setMinWidthDp(840)
           .setMinSmallestWidthDp(600)
           .setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ratio(1.5f))
           .setFinishPrimaryWithPlaceholder(SplitRule.FinishBehavior.ALWAYS)
           .setSticky(false)
           .build();

      SplitPlaceholderRule.Builder 创建并配置规则

      • placeholderActivityFilterSet:包含活动过滤器,这些过滤器通过识别与占位符活动关联的活动来确定何时应用规则。
      • Intent:指定占位符活动的启动。
      • setDefaultSplitAttributes():将布局属性应用于规则。
      • setMinWidthDp():设置允许拆分的最小显示宽度(以密度无关像素 dp 为单位)。
      • setMinSmallestWidthDp():设置两个显示尺寸中较小的尺寸必须具有的最小值(以 dp 为单位),以允许拆分,而不管设备方向如何。
      • setMaxAspectRatioInPortrait():设置纵向方向的显示最大纵横比(高:宽),用于显示活动拆分。**注意:**默认值为 1.4,这会导致活动在大多数平板电脑的纵向方向上填充任务窗口。另请参阅SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULTsetMaxAspectRatioInLandscape()。横向的默认值为 ALWAYS_ALLOW
      • setFinishPrimaryWithPlaceholder():设置占位符活动完成时如何影响主容器中的活动。ALWAYS 表示当占位符完成时,系统应始终完成主容器中的活动(请参阅完成活动)。
      • setSticky():确定占位符活动在小屏幕上是否显示在活动堆栈的顶部,前提是占位符已首先以足够的最小宽度拆分显示。
    4. 将规则添加到 WindowManager RuleController

      Kotlin

      ruleController.addRule(splitPlaceholderRule)

      Java

      ruleController.addRule(splitPlaceholderRule);
  3. 指定不应成为拆分一部分的活动

    1. 创建一个 ActivityFilter,用于识别始终应占据整个任务显示区域的活动

      Kotlin

      val expandedActivityFilter = ActivityFilter(
        ComponentName(this, ExpandedActivity::class.java),
        null
      )

      Java

      ActivityFilter expandedActivityFilter = new ActivityFilter(
        new ComponentName(this, ExpandedActivity.class),
        null
      );
    2. 将筛选器添加到筛选器集中

      Kotlin

      val expandedActivityFilterSet = setOf(expandedActivityFilter)

      Java

      Set<ActivityFilter> expandedActivityFilterSet = new HashSet<>();
      expandedActivityFilterSet.add(expandedActivityFilter);
    3. 创建一个ActivityRule

      Kotlin

      val activityRule = ActivityRule.Builder(expandedActivityFilterSet)
          .setAlwaysExpand(true)
          .build()

      Java

      ActivityRule activityRule = new ActivityRule.Builder(
          expandedActivityFilterSet
      ).setAlwaysExpand(true)
       .build();

      ActivityRule.Builder 创建并配置规则

      • expandedActivityFilterSet:包含活动过滤器,这些过滤器通过识别您想要排除在拆分之外的活动来确定何时应用规则。
      • setAlwaysExpand():指定活动是否应填充整个任务窗口。
    4. 将规则添加到 WindowManager RuleController

      Kotlin

      ruleController.addRule(activityRule)

      Java

      ruleController.addRule(activityRule);

跨应用程序嵌入

在 Android 13(API 级别 33)或更高版本上,应用可以嵌入来自其他应用的活动。跨应用程序或跨UID 活动嵌入允许来自多个 Android 应用程序的活动的视觉集成。系统并排或上下在屏幕上显示主机应用的活动和来自另一个应用的嵌入式活动,就像在单应用活动嵌入中一样。

例如,设置应用可以嵌入来自 WallpaperPicker 应用的壁纸选择器活动

图 14. 设置应用(左侧菜单)以及壁纸选择器作为嵌入式活动(右侧)。

信任模型

嵌入来自其他应用的活动的主机进程能够重新定义嵌入式活动的呈现方式,包括大小、位置、裁剪和透明度。恶意主机可以使用此功能误导用户并创建点击劫持或其他 UI 重塑攻击。

为了防止滥用跨应用活动嵌入,Android 要求应用选择加入以允许嵌入其活动。应用可以将主机指定为受信任或不受信任。

受信任的主机

要允许其他应用程序嵌入和完全控制来自您应用的活动的呈现方式,请在应用清单的<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><application> 元素中指定android:allowUntrustedActivityEmbedding 属性,例如

<activity
    android:name=".MyEmbeddableActivity"
    android:allowUntrustedActivityEmbedding="true"
    ... />

属性的默认值为 false,这会阻止跨应用活动嵌入。

自定义身份验证

为了降低不受信任的活动嵌入的风险,请创建一个自定义身份验证机制来验证主机身份。如果您知道主机证书,请使用androidx.security.app.authenticator 库进行身份验证。如果主机在嵌入您的活动后进行身份验证,则可以显示实际内容。否则,您可以通知用户未允许该操作并阻止内容。

使用 Jetpack WindowManager 库中的ActivityEmbeddingController#isActivityEmbedded() 方法检查主机是否正在嵌入您的活动,例如

Kotlin

fun isActivityEmbedded(activity: Activity): Boolean {
    return ActivityEmbeddingController.getInstance(this).isActivityEmbedded(activity)
}

Java

boolean isActivityEmbedded(Activity activity) {
    return ActivityEmbeddingController.getInstance(this).isActivityEmbedded(activity);
}

最小尺寸限制

Android 系统会将应用清单<layout> 元素中指定的最小高度和宽度应用于嵌入式活动。如果应用程序未指定最小高度和宽度,则应用系统默认值(sw220dp)。

如果主机尝试将嵌入式容器调整为小于最小尺寸的大小,则嵌入式容器会扩展以占据整个任务边界。

<activity-alias>

为了使受信任或不受信任的活动嵌入能够与<activity-alias> 元素一起使用,必须将android:knownActivityEmbeddingCertsandroid:allowUntrustedActivityEmbedding 应用于目标活动,而不是别名。系统服务器上验证安全性的策略基于目标上设置的标志,而不是别名。

主机应用程序

主机应用程序以与实现单应用活动嵌入相同的方式实现跨应用活动嵌入。SplitPairRuleSplitPairFilterActivityRuleActivityFilter 对象指定嵌入式活动和任务窗口拆分。拆分规则是在XML 中静态定义或使用 Jetpack WindowManager API 调用在运行时定义的。

如果主机应用程序尝试嵌入尚未选择加入跨应用嵌入的活动,则该活动将占据整个任务边界。因此,主机应用程序需要知道目标活动是否允许跨应用嵌入。

如果嵌入式活动在同一任务中启动新活动,并且新活动尚未选择加入跨应用嵌入,则该活动将占据整个任务边界,而不是覆盖嵌入式容器中的活动。

只要活动在同一任务中启动,主机应用程序就可以不受限制地嵌入其自己的活动。

拆分示例

从全窗口拆分

图 15. 活动 A 将活动 B 启动到侧面。

无需重构。您可以静态或在运行时定义拆分的配置,然后调用Context#startActivity(),而无需任何其他参数。

<SplitPairRule>
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

默认拆分

当应用程序的登录页面设计为在大屏幕上拆分为两个容器时,当同时创建和呈现这两个活动时,用户体验最佳。但是,在用户与主容器中的活动交互之前,拆分的辅助容器可能无法获得内容(例如,用户从导航菜单中选择项目)。占位符活动可以填补空白,直到可以在拆分的辅助容器中显示内容(请参阅占位符 部分)。

图 16. 通过同时打开两个活动创建的拆分。一个活动是占位符。

要创建带占位符的拆分,请创建一个占位符并将其与主活动关联

<SplitPlaceholderRule
    window:placeholderActivityName=".PlaceholderActivity">
    <ActivityFilter
        window:activityName=".MainActivity"/>
</SplitPlaceholderRule>

当应用接收到一个意图时,目标 Activity 可以作为 Activity 分屏的次要部分显示;例如,请求显示一个详细屏幕,其中包含来自列表中项目的相关信息。在小屏幕上,详细信息将显示在整个任务窗口中;在大屏幕设备上,则显示在列表旁边。

图 17. 深层链接详细 Activity 在小屏幕上单独显示,但在较大屏幕上与列表 Activity 一起显示。

启动请求应路由到主 Activity,目标详细 Activity 应在分屏中启动。系统会根据可用的显示宽度自动选择正确的呈现方式——堆叠或并排。

Kotlin

override fun onCreate(savedInstanceState Bundle?) {
    . . .
    RuleController.getInstance(this)
        .addRule(SplitPairRule.Builder(filterSet).build())
    startActivity(Intent(this, DetailActivity::class.java))
}

Java

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    . . .
    RuleController.getInstance(this)
        .addRule(new SplitPairRule.Builder(filterSet).build());
    startActivity(new Intent(this, DetailActivity.class));
}

深层链接目标可能是唯一一个应该在用户返回导航堆栈中可用的 Activity,您可能希望避免关闭详细 Activity 并只保留主 Activity。

Large display with list activity and detail activity side by side.
          Back navigation unable to dismiss detail activity and leave list
          activity on screen.

Small display with detail activity only. Back navigation unable to
          dismiss detail activity and reveal list activity.

相反,您可以使用 finishPrimaryWithSecondary 属性同时结束这两个 Activity。

<SplitPairRule
    window:finishPrimaryWithSecondary="always">
    <SplitPairFilter
        window:primaryActivityName=".ListActivity"
        window:secondaryActivityName=".DetailActivity"/>
</SplitPairRule>

请参阅配置属性 部分。

分屏容器中的多个 Activity

在分屏容器中堆叠多个 Activity 使用户能够访问深度内容。例如,对于列表-详细信息分屏,用户可能需要进入子详细信息部分,但需要保持主 Activity 保持原位。

图 18. 在任务窗口的次要窗格中就地打开的 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 的顶部,将其隐藏。

然后,用户可以通过导航返回堆栈返回到上一级详细信息。

图 19. 从堆栈顶部移除的 Activity。

当从同一辅助容器中的 Activity 启动 Activity 时,在彼此顶部堆叠 Activity 是默认行为。从活动分屏中的主容器启动的 Activity 也最终位于辅助容器中,位于 Activity 堆栈的顶部。

新任务中的 Activity

当分屏任务窗口中的 Activity 在新任务中启动 Activity 时,新任务与包含分屏的任务分离,并以全窗口显示。最近使用的应用屏幕显示两个任务:分屏中的任务和新任务。

图 20. 从 Activity B 在新任务中启动 Activity C。

Activity 替换

可以在辅助容器堆栈中替换 Activity;例如,当主 Activity 用于顶级导航,而辅助 Activity 是选定的目标时。顶级导航中的每次选择都应在辅助容器中启动一个新的 Activity,并删除之前存在的 Activity 或 Activity。

图 21. 主窗格中的顶级导航 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 发送新的意图,这些意图解析为相同的实例,但会触发辅助容器中的状态或 UI 更新。

多个分屏

应用可以通过向侧面启动其他 Activity 来提供多级深度导航。

当辅助容器中的 Activity 向侧面启动新的 Activity 时,会在现有分屏的顶部创建一个新的分屏。

图 22. Activity B 向侧面启动 Activity C。

返回堆栈包含之前打开的所有 Activity,因此用户可以在完成 C 后导航到 A/B 分屏。

Activities A, B, and C in a stack. The activities are stacked in
          the following order from top to bottom: C, B, A.

要创建新的分屏,请从现有的辅助容器中向侧面启动新的 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 {
    . . .
    fun onOpenC() {
        startActivity(Intent(this, C::class.java))
    }
}

Java

public class B {
    . . .
    void onOpenC() {
        startActivity(new Intent(this, C.class));
    }
}

对分屏状态更改做出反应

应用中的不同 Activity 可以具有执行相同功能的 UI 元素;例如,一个打开包含帐户设置的窗口的控件。

图 23. 具有功能上相同的 UI 元素的不同 Activity。

如果两个具有共同 UI 元素的 Activity 位于分屏中,则在两个 Activity 中显示该元素是冗余的,并且可能令人困惑。

图 24. Activity 分屏中的重复 UI 元素。

要了解 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 {
    lifecycle.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) {
    . . .
    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。

图 25. 滑动手势结束 Activity B。
图 26. 滑动手势结束 Activity A。

如果设备设置为使用后退按钮而不是手势导航,则输入将发送到焦点 Activity——最后被触摸或启动的 Activity。

结束容器中所有 Activity 对相对容器的影响取决于分屏配置。

配置属性

您可以指定分屏对规则属性来配置结束分屏一侧的所有 Activity 如何影响分屏另一侧的 Activity。属性包括

  • window:finishPrimaryWithSecondary — 结束辅助容器中的所有 Activity 如何影响主容器中的 Activity
  • window:finishSecondaryWithPrimary — 结束主容器中的所有 Activity 如何影响辅助容器中的 Activity

属性的可能值包括

  • always — 始终结束关联容器中的 Activity
  • never — 从不结束关联容器中的 Activity
  • adjacent — 当两个容器并排显示时结束关联容器中的 Activity,但当两个容器堆叠时则不结束。

例如

<SplitPairRule
    &lt;!-- Do not finish primary container activities when all secondary container activities finish. --&gt;
    window:finishPrimaryWithSecondary="never"
    &lt;!-- Finish secondary container activities when all primary container activities finish. --&gt;
    window:finishSecondaryWithPrimary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

默认配置

当分屏的一个容器中的所有 Activity 结束时,剩余的容器将占据整个窗口。

<SplitPairRule>
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

Split containing activities A and B. A is finished, leaving B to
          occupy the entire window.

Split containing activities A and B. B is finished, leaving A to
          occupy the entire window.

一起结束 Activity

当辅助容器中的所有 Activity 结束时,自动结束主容器中的 Activity。

<SplitPairRule
    window:finishPrimaryWithSecondary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

Split containing activities A and B. B is finished, which also
          finishes A, leaving the task window empty.

Split containing activities A and B. A is finished, leaving B alone
          in the task window.

当主容器中的所有 Activity 结束时,自动结束辅助容器中的 Activity。

<SplitPairRule
    window:finishSecondaryWithPrimary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

Split containing activities A and B. A is finished, which also
          finishes B, leaving the task window empty.

Split containing activities A and B. B is finished, leaving A alone
          in the task window.

当主容器或辅助容器中的所有 Activity 结束时,一起结束 Activity。

<SplitPairRule
    window:finishPrimaryWithSecondary="always"
    window:finishSecondaryWithPrimary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

Split containing activities A and B. A is finished, which also
          finishes B, leaving the task window empty.

Split containing activities A and B. B is finished, which also
          finishes A, leaving the task window empty.

结束容器中的多个 Activity

如果多个 Activity 堆叠在分屏容器中,结束堆栈底部的 Activity 不会自动结束顶部的 Activity。

例如,如果两个 Activity 位于辅助容器中,C 在 B 的顶部

Secondary activity stack containing activity C stacked on top of B
          is stacked on top of the prmary activity stack containing activity
          A.

并且分屏的配置由 Activity A 和 B 的配置定义

<SplitPairRule>
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

结束顶部 Activity 会保留分屏。

Split with activity A in primary container and activities B and C in
          secondary, C stacked on top of B. C finishes, leaving A and B in the
          activity split.

结束辅助容器的底部(根)Activity 不会删除其顶部的 Activity;因此,也会保留分屏。

Split with activity A in primary container and activities B and C in
          secondary, C stacked on top of B. B finishes, leaving A and C in the
          activity split.

还会执行任何其他一起结束 Activity 的规则,例如使用主 Activity 结束辅助 Activity。

<SplitPairRule
    window:finishSecondaryWithPrimary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

Split with activity A in primary container and activities B and C in
          secondary container, C stacked on top of B. A finishes, also
          finishing B and C.

并且当分屏配置为一起结束主 Activity 和辅助 Activity 时

<SplitPairRule
    window:finishPrimaryWithSecondary="always"
    window:finishSecondaryWithPrimary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

Split with activity A in primary container and activities B and C in
          secondary, C stacked on top of B. C finishes, leaving A and B in the
          activity split.

Split with activity A in primary container and activities B and C in
          secondary, C stacked on top of B. B finishes, leaving A and C in the
          activity split.

Split with activity A in primary container and activities B and C in
          secondary, C stacked on top of B. A finishes, also finishing B and
          C.

在运行时更改分屏属性

无法更改活动且可见的分屏的属性。更改分屏规则会影响其他 Activity 的启动和新容器,但不会影响现有和活动的分屏。

要更改活动分屏的属性,请结束分屏中的侧边 Activity 或 Activity,然后使用新的配置再次向侧面启动。

动态分屏属性

Android 15(API 级别 35)及更高版本(由 Jetpack WindowManager 1.4 及更高版本支持)提供动态功能,这些功能支持 Activity 嵌入分屏的可配置性,包括

  • 窗格扩展:交互式、可拖动的分隔线使用户能够调整分屏呈现中窗格的大小。

  • 活动固定:用户可以固定一个容器中的内容,并隔离该容器内的导航与其他容器内的导航。
  • 全屏对话框变暗:显示对话框时,应用可以指定是使整个任务窗口变暗,还是仅使打开对话框的容器变暗。

窗格扩展

窗格扩展使用户能够调整分配给双窗格布局中两个活动屏幕空间的大小。

要自定义窗口分隔符的外观并设置分隔符的可拖动范围,请执行以下操作

  1. 创建 DividerAttributes 的实例

  2. 自定义分隔符属性

    • 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(context, 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(context, R.color.divider_color))
        .setWidthDp(4)
        .setDragRange(DividerAttributes.DragRange.DRAG_RANGE_SYSTEM_DEFAULT)
        .build()
    );
}
SplitAttributes splitAttributes = splitAttributesBuilder.build();

活动固定

活动固定使用户能够固定拆分窗口之一,以便在用户在另一个窗口中导航时活动保持不变。活动固定提供增强的多任务处理体验。

要在您的应用中启用活动固定,请执行以下操作

  1. 将按钮添加到要固定的活动的布局文件中,例如列表-详细信息布局的详细信息活动

    <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>
    
  2. 在活动的 onCreate() 方法中,在按钮上设置 onclick 监听器

    Kotlin

    pinButton = 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);
    });

全屏变暗

活动通常会使其显示变暗以引起对对话框的注意。在活动嵌入中,双窗格显示的两个窗格都应变暗,而不仅仅是包含打开对话框的活动的窗格,以实现统一的 UI 体验。

使用 WindowManager 1.4 及更高版本,当对话框打开时,整个应用窗口默认会变暗(请参阅 EmbeddingConfiguration.DimAreaBehavior.ON_TASK)。

要仅使打开对话框的活动的容器变暗,请使用 EmbeddingConfiguration.DimAreaBehavior.ON_ACTIVITY_STACK

从拆分窗口提取活动到全窗口

创建一个新的配置,将侧边活动显示为全窗口,然后使用解析到同一实例的 Intent 重新启动该活动。

在运行时检查拆分支持

活动嵌入在 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.
}

如果不受支持拆分,则活动将在活动堆栈顶部启动(遵循非活动嵌入模型)。

防止系统覆盖

Android 设备制造商(原始设备制造商或 OEM)可以将活动嵌入作为设备系统功能来实现。系统为多活动应用指定拆分规则,覆盖应用的窗口行为。系统覆盖会强制多活动应用进入系统定义的活动嵌入模式。

系统活动嵌入可以通过多窗格布局(例如 列表-详细信息)来增强应用呈现,而无需对应用进行任何更改。但是,系统的活动嵌入也可能导致应用布局错误、错误或与应用实现的活动嵌入冲突。

您的应用可以通过在应用清单文件中设置属性来防止或允许系统活动嵌入,例如

<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 对象中定义。如果您的应用实现活动嵌入,或者您希望以其他方式阻止系统将活动嵌入规则应用于您的应用,则将值设置为 false。将值设置为 true 以允许系统将系统定义的活动嵌入应用于您的应用。

限制、约束和注意事项

  • 只有任务的主机应用(被识别为任务中根活动的拥有者)才能组织和嵌入任务中的其他活动。如果支持嵌入和拆分的活动在属于不同应用的任务中运行,则这些活动的嵌入和拆分将无法正常工作。
  • 活动只能在一个任务内组织。在新的任务中启动活动始终会将其置于任何现有拆分之外的新扩展窗口中。
  • 只有同一进程中的活动才能组织并放入拆分中。SplitInfo 回调仅报告属于同一进程的活动,因为无法了解不同进程中的活动。
  • 每个活动对或单活动规则仅适用于在注册该规则后发生的活动启动。目前无法更新现有的拆分或其视觉属性。
  • 拆分对筛选器配置必须与启动活动时使用的 Intent 完全匹配。匹配发生在从应用进程启动新活动时,因此它可能不知道在使用隐式 Intent 时系统进程中稍后解析的组件名称。如果在启动时不知道组件名称,则可以使用通配符(“*/*”)代替,并可以根据 Intent 操作进行筛选。
  • 目前无法在创建活动后将活动在容器之间移动或进出拆分。当启动具有匹配规则的新活动时,WindowManager 库才会创建拆分,并且当拆分容器中的最后一个活动完成时,拆分才会被销毁。
  • 当配置更改时,可以重新启动活动,因此当创建或删除拆分以及活动边界更改时,活动可能会经历先前实例的完全销毁和新实例的创建。因此,应用开发者在从生命周期回调启动新活动时应格外小心。
  • 设备必须包含窗口扩展接口才能支持活动嵌入。几乎所有运行 Android 12L(API 级别 32)或更高版本的较大屏幕设备都包含该接口。但是,某些无法运行多个活动的较大屏幕设备不包含窗口扩展接口。如果大屏幕设备不支持多窗口模式,则它可能不支持活动嵌入。

其他资源