活动嵌入

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

图 1. 侧边显示 Activity 的设置应用。

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

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

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

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

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

拆分任务窗口

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

启动 Activity 时,它们会堆叠在辅助容器中,辅助容器会在小屏幕上堆叠在主容器之上,因此 Activity 堆叠和返回导航与应用中已构建的 Activity 顺序一致。

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

图 2. 两个并排的 Activity。

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

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

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

  • 在另一个 Activity 之上侧边显示

    图 4. 活动 A 在活动 B 旁边启动活动 C。
  • 在旁边,并将分割线向侧面移动,隐藏先前的主要活动

    图 5. 活动 B 在旁边启动活动 C 并将分割线向侧面移动。
  • 在同一活动栈中,启动一个位于顶部的活动;也就是说,在同一个活动栈中。

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

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

返回导航

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

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

当使用按钮导航时,返回事件将发送到最后一个聚焦的活动。使用手势导航时,返回事件将发送到发生手势的活动。

多窗格布局

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 时,当显示尺寸从双窗格显示调整为单窗格显示时,系统会将占位符显示为任务窗口中最顶层的活动(有关示例,请参阅 分割配置)。

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

窗口尺寸更改

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

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

WindowManager 会将次要窗格中的活动置于主要窗格中的活动之上,因此可以实现活动堆叠。

次要窗格中的多个活动

活动 B 在不使用任何额外的 Intent 标志的情况下,在同一位置启动活动 C

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

导致在同一任务中以下活动的 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.

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

Small window showing only activity C.

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

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

堆叠分割

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

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

结果是在同一任务中以下活动的 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.

在较小的任务窗口中,应用程序会缩小为一个活动,其中 C 位于顶部。

Small window showing only activity C.

固定纵向方向

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

图 12. 信箱活动:在横向设备上固定纵向(左),在纵向设备上固定横向(右)。

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

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

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

分割配置

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

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

执行以下操作

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

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

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

  2. 通知系统您的应用程序已实现活动嵌入。

    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,否则活动嵌入分割将被禁用。

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

XML 配置

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

  1. 创建一个 XML 资源文件,该文件执行以下操作

    • 定义共享分割的活动
    • 配置分割选项
    • 在内容不可用时为分割的次要容器创建一个 占位符
    • 指定永远不应该成为分割一部分的活动

    例如

    <!-- 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,以便在任何活动启动时规则都生效。

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

    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:设置主容器中的所有 Activity 结束如何影响辅助容器中的 Activity。ALWAYS 表示当主容器中的所有 Activity 结束时,系统应始终结束辅助容器中的 Activity(请参阅 结束 Activity)。
      • setClearTop:指定在辅助容器中启动新 Activity 时,是否结束辅助容器中的所有 Activity。False 指定新 Activity 堆叠在辅助容器中已有的 Activity 之上。
    5. 获取 WindowManager RuleController 的单例实例,并添加规则

      Kotlin

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

      Java

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

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

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

      Kotlin

      ruleController.addRule(splitPlaceholderRule)
      

      Java

      ruleController.addRule(splitPlaceholderRule);
      
  3. 指定不应成为分割一部分的 Activity

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

      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:包含 Activity 过滤器,这些过滤器通过识别您要从分割中排除的 Activity 来确定何时应用规则。
      • setAlwaysExpand:指定 Activity 是否应填充整个任务窗口。
    4. 将规则添加到 WindowManager RuleController

      Kotlin

      ruleController.addRule(activityRule)
      

      Java

      ruleController.addRule(activityRule);
      

跨应用嵌入

在 Android 13(API 级别 33)或更高版本上,应用可以嵌入其他应用的 Activity。跨应用或跨 UID Activity 嵌入能够实现来自多个 Android 应用的 Activity 的视觉集成。系统会并排或上下显示主机应用的 Activity 和来自另一个应用的嵌入式 Activity,就像在单应用 Activity 嵌入中一样。

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

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

信任模型

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

为了防止滥用跨应用 Activity 嵌入,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 嵌入。

自定义身份验证

为了降低不受信任的 Activity 嵌入的风险,请创建一个自定义身份验证机制来验证主机身份。如果您知道主机证书,请使用 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(this).isActivityEmbedded(activity);
}

最小尺寸限制

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

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

<activity-alias>

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

主机应用程序

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

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

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

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

拆分示例

从全窗口拆分

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

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

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

默认拆分

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

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

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

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

当应用收到 Intent 时,目标 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 或 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 更新的新 Intent。

多个拆分

应用可以通过将其他 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. 滑动手势完成活动 A。

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

在容器中完成所有活动对相反容器的影响取决于拆分配置。

配置属性

您可以指定拆分对规则属性来配置在一侧完成所有活动如何影响拆分另一侧的活动。这些属性包括

  • window:finishPrimaryWithSecondary — 完成辅助容器中的所有活动如何影响主容器中的活动
  • window:finishSecondaryWithPrimary — 完成主容器中的所有活动如何影响辅助容器中的活动

属性的可能值包括

  • always — 始终完成关联容器中的活动
  • never — 从不完成关联容器中的活动
  • adjacent — 当两个容器并排显示时,完成关联容器中的活动,但当两个容器堆叠时则不完成

例如

<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>

默认配置

当拆分的一个容器中的所有活动都完成时,剩余的容器将占据整个窗口。

<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.

一起完成活动

当辅助容器中的所有活动都完成时,自动完成主容器中的活动。

<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.

当主容器中的所有活动都完成时,自动完成辅助容器中的活动。

<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.

当主容器或辅助容器中的所有活动都完成时,一起完成活动。

<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.

在容器中完成多个活动

如果多个活动堆叠在拆分容器中,则完成堆栈底部的活动不会自动完成顶部的活动。

例如,如果辅助容器中有两个活动,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.

并且拆分的配置由活动 A 和 B 的配置定义

<SplitPairRule>
    <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.

还会执行任何其他一起完成活动的规则,例如使用主活动完成辅助活动。

<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.

当拆分配置为一起完成主活动和辅助活动时

<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.

在运行时更改拆分属性

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

要更改活动拆分的属性,请完成拆分中的侧边活动或活动,然后使用新配置再次启动到侧边。

从拆分中提取活动到完整窗口

创建一个新的配置,将侧边活动显示为完整窗口,然后使用解析到同一实例的 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)或更高版本的屏幕较大的设备都包含该接口。但是,一些无法运行多个活动的屏幕较大的设备不包含窗口扩展接口。如果屏幕较大的设备不支持多窗口模式,则它可能不支持活动嵌入。

其他资源