Activity 嵌入

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

图 1. 并排显示 activity 的“设置”应用。

如果应用由多个 activity 组成,activity 嵌入让您能够在平板电脑、可折叠设备和 ChromeOS 设备上提供增强的用户体验。

activity 嵌入无需重构代码。至于应用如何显示其 activity(是并排还是堆叠)时,可以通过创建 XML 配置文件或进行 Jetpack WindowManager API 调用来确定。

系统会自动维护对小屏幕的支持。当应用在配备小屏幕的设备上时,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. activity A 在侧面的 activity B 之上启动 activity C。
  • 在侧面启动一个 activity 并使分屏向一旁位移,从而隐藏之前的主要 activity:

    图 5. activity B 在侧面启动 activity C,并使分屏向一旁位移。
  • 在原来的 activity 之上原位启动一个 activity;即,在同一 activity 堆栈中:

    图 6. activity B 启动 activity C,并且没有额外的 intent 标志。
  • 在同一任务中启动一个 activity 全窗口:

    图 7. activity A 或 activity B 启动 activity C,activity C 将填满任务窗口。

返回导航

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

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

使用按钮导航时,系统会将返回事件发送到上次聚焦的 activity。对于基于手势的导航,系统会将返回事件发送到发生手势的 activity。

多窗格布局

无论是搭载 Android 12L(API 级别 32)或更高版本的大屏设备,还是某些搭载早期平台版本的设备,Jetpack WindowManager 都能让您在设备上构建 activity 嵌入多窗格布局。基于多个 activity 而非 fragment 或基于视图的布局(如 SlidingPaneLayout)的现有应用可以提供改进的大屏幕用户体验,无需重构源代码。

一个常见的示例是列表-详情分屏。为了确保高质量的呈现,系统先启动列表 activity,然后应用立即启动详情 activity。过渡系统等到这两个 activity 都绘制完成后再将它们一起显示出来。对用户来说,这两个 activity 作为一个启动。

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

分屏属性

您可以指定如何在分屏容器之间按比例划分任务窗口,以及容器彼此间的布局方式。

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

  • splitRatio:设置容器比例。该值是开放区间 (0.0, 1.0) 之间的一个浮点数。
  • splitLayoutDirection:指定分屏容器彼此间的布局方式。该参数的值包括:
    • ltr:从左到右
    • rtl:从右到左
    • localeltrrtl 取决于语言区域设置

有关示例,请参阅下面的 XML 配置

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

有关示例,请参阅下文的 WindowManager API

图 9. 两个 activity 分屏采用从左到右的布局,但是分屏比不同。

占位符

占位符 activity 是空的辅助 activity,占据了 activity 分屏的一个区域。其最终会被替换为另一个包含内容的 activity。例如,一个占位符 activity 可以在列表详情布局中占据 activity 分屏的辅助一侧,直到用户从列表中选择了一项,此时一个包含选定列表项的详情的 activity 会替换该占位符。

默认情况下,系统仅在有足够的 activity 分屏空间时才会显示占位符。当显示屏宽度或高度变得太小而无法显示分屏时,占位符会自动结束。在空间允许的情况下,系统会重新启动占位符(处于重新初始化状态)。

图 10. 可折叠设备折叠和展开。占位符 activity 随着显示屏尺寸的变化而结束并重新创建。

不过,SplitPlaceholder.BuilderSplitPlaceholderRulesetSticky() 方法的 stickyPlaceholder 属性可以替换默认值行为。将此属性或方法指定为 true 值时,如果将显示屏从双窗格显示缩小为单窗格显示,系统会将占位符显示为任务窗口中的顶部 activity(有关示例,请参阅分屏配置)。

图 11. 可折叠设备折叠和展开。占位符 activity 是固定的。

窗口大小变化

当设备配置发生了变化,使得任务窗口变窄而无法显示多窗格布局时(例如,当大屏幕可折叠设备从平板电脑大小折叠成手机大小时或者应用窗口在多窗口模式下调整大小时),任务窗口辅助窗格中的非占位符 activity 便会堆叠在主要窗格中的 activity 之上。

仅当有足够的显示宽度来显示分屏时,才会显示占位符 activity。在较小的屏幕上,系统会自动关闭占位符。当显示区域再次变得足够大时,系统会重新创建占位符(请参阅上文的占位符)。

之所以能够堆叠 activity,是因为 WindowManager 会按照 Z 序将辅助窗格中的 activity 叠置于主要窗格中的 activity 之上。

辅助窗格中的多个 activity

activity B 原位启动 activity C,并且没有额外的 intent 标志:

activity 分屏包含 activity A、activity B 和 activity C,其中 activity C 堆叠在 activity B 之上。

结果是同一任务中 activity 的叠置顺序如下:

辅助 activity 堆栈中的 activity C 堆叠在 activity B 之上。
          辅助堆栈堆叠在包含 activity A 的主要 activity 堆栈之上。

因此,在较小的任务窗口中,应用会缩小到单个 activity,其中 activity C 在堆栈的顶部:

仅显示 activity C 的小窗口。

在较小的窗口中进行返回导航时,会沿着相互堆叠的 activity 原路返回。

如果任务窗口配置恢复为可以容纳多个窗格的较大大小,系统会再次并排显示 activity。

堆叠的分屏

activity B 在侧面启动 activity C,并使分屏向一旁位移:

任务窗口先显示 activity A 和 activity B,再显示 activity B 和 activity C。

结果是同一任务中 activity 的叠置顺序如下:

单个堆栈中的 activity A、activity B 和 activity C。这些 activity 按以下顺序堆叠,从上到下依次为:activity C,activity B,activity A。

在较小的任务窗口中,应用会缩小到单个 activity,其中 activity C 在顶部:

仅显示 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 配置

如需创建基于 XML 的 activity 嵌入实现,请完成以下步骤:

  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 会在应用启动时为 RuleController 提供 XML 文件,以便在任何 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. 为规则定义创建 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 嵌入。在 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 的呈现方式,包括大小、位置、剪裁和透明度。恶意主机可能会利用此功能误导用户并发起点击劫持攻击或其他界面伪装攻击。

为防止跨应用 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> 元素一起使用,必须对目标 activity(而非别名)应用 android:knownActivityEmbeddingCertsandroid:allowUntrustedActivityEmbedding。用于验证系统服务器安全性的政策取决于在目标上设置的标志,而不是别名。

托管应用

托管应用实现跨应用 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:

并排显示列表 activity 和详情 activity 的大显示屏。返回导航无法关闭详情 activity,而将列表 activity 留在屏幕上。

仅显示详情 activity 的小显示屏。返回导航无法关闭详情 activity 而显示列表 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 之上,从而将详情 activity 隐藏起来:

然后,用户可以通过在堆栈中进行返回导航来回到之前的详情级别:

图 19. 从堆栈顶部移除了 activity。

当从同一辅助容器中的一个 activity 启动多个 activity 时,相互堆叠 activity 是默认行为。从活跃分屏的主要容器中启动的 activity 最终也会出现在 activity 堆栈顶部的辅助容器中。

新任务中的 activity

当分屏任务窗口中的 activity 启动新任务中的 activity 时,新任务将与包含分屏的任务分开并显示在全窗口中。“最近使用的应用”屏幕显示两项任务:分屏中的任务和新任务。

图 20. 从 activity B 启动新任务中的 activity C。

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 发送新的 intent,这些 intent 解析为相同的实例,但会在辅助容器中触发状态或界面更新。

多重分屏

应用可以通过在侧面启动额外的 activity 来提供多级深层导航。

当辅助容器中的 activity 在侧面启动一个新的 activity 时,系统会在现有分屏之上创建一个新的分屏。

图 22. activity B 在侧面启动 activity C。

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

堆栈中的 activity A、activity B 和 activity C。这些 activity 按以下顺序堆叠,从上到下依次为:activity C、activity B 和 activity A。

如需创建新的分屏,请从现有辅助容器中在侧面启动新的 activity。声明 activity A/activity B 分屏和 activity B/activity C 分屏的配置,并正常从 activity B 启动 activity C:

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

Kotlin

class B {
    . . .
    fun onOpenC() {
        startActivity(Intent(this, C::class.java))
    }
}

Java

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

响应分屏状态变化

应用中的不同 activity 可以具有执行相同功能的界面元素;例如,一个用于打开包含账号设置的窗口的控件。

图 23. 不同的 activity 具有功能上完全相同的界面元素。

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

图 24. activity 分屏中的重复界面元素。

如需了解 activity 何时出现在分屏中,请检查 SplitController.splitInfoList 流或通过 SplitControllerCallbackAdapter 注册一个监听器来监听分屏状态的变化。然后,相应地调整界面:

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;但当两个容器堆叠时,不结束这些 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 A 和 activity B。activity A 已完成,留下 activity B 占据整个窗口。

分屏包含 activity A 和 activity B。activity B 已完成,留下 activity A 占据整个窗口。

一起结束 activity

当辅助容器中的所有 activity 都结束时,自动结束主要容器中的 activity:

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

分屏包含 activity A 和 activity B。activity B 已完成,activity A 也随之完成,这使得任务窗口为空。

分屏包含 activity A 和 activity B。activity A 已完成,在任务窗口中只留下 activity B。

当主要容器中的所有 activity 都结束时,自动结束辅助容器中的 activity:

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

分屏包含 activity A 和 activity B。activity A 已完成,activity B 也随之完成,这使得任务窗口为空。

分屏包含 activity A 和 activity B。activity B 已完成,在任务窗口中只留下 activity A。

当主要容器或辅助容器中的所有 activity 都结束时,一起结束这些 activity:

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

分屏包含 activity A 和 activity B。activity A 已完成,activity B 也随之完成,这使得任务窗口为空。

分屏包含 activity A 和 activity B。activity B 已完成,activity A 也随之完成,这使得任务窗口为空。

结束容器中的多个 activity

如果分屏容器中堆叠了多个 activity,结束堆栈底层的 activity 并不会自动结束它上面的 activity。

例如,如果辅助容器中有两个 activity,其中 activity C 在 activity B 之上:

辅助 activity 堆栈(其中 activity C 堆叠在 activity B 之上)堆叠在主要 activity 堆栈(包含 activity A)之上。

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

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

那么,结束顶层 activity 时,会保留分屏。

一个分屏,其中 activity A 在主要容器中,activity B 和 activity C 在辅助容器中,且 activity C 堆叠在 activity B 之上。activity C 已结束,将 activity A 和 activity B 留在 activity 分屏中。

结束辅助容器的底层(根)activity 并不会移除它上面的 activity;因此,仍会保留分屏。

一个分屏,其中 activity A 在主要容器中,activity B 和 activity C 在辅助容器中,且 activity C 堆叠在 activity B 之上。activity B 已结束,将 activity A 和 activity C 留在 activity 分屏中。

也会执行关于一起结束 activity 的其他任何规则,如将辅助 activity 与主要 activity 一起结束:

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

一个分屏,其中 activity A 在主要容器中,activity B 和 activity C 在辅助容器中,且 activity C 堆叠在 activity B 之上。activity A 已完成,activity B 和 activity C 也随之完成。

将分屏配置为一起结束主要 activity 和辅助 activity 时:

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

一个分屏,其中 activity A 在主要容器中,activity B 和 activity C 在辅助容器中,且 activity C 堆叠在 activity B 之上。activity C 已结束,将 activity A 和 activity B 留在 activity 分屏中。

一个分屏,其中 activity A 在主要容器中,activity B 和 activity C 在辅助容器中,且 activity C 堆叠在 activity B 之上。activity B 已结束,将 activity A 和 activity C 留在 activity 分屏中。

一个分屏,其中 activity A 在主要容器中,activity B 和 activity C 在辅助容器中,且 activity C 堆叠在 activity B 之上。activity A 已完成,activity B 和 activity C 也随之完成。

在运行时更改分屏属性

您不能更改当前活跃和可见的分屏的属性。更改分屏规则会影响其他 activity 启动和新容器,但不会影响现有和活跃分屏。

如需更改活跃分屏的属性,请结束分屏中侧面的一个或多个 activity,然后使用新配置再次启动到侧面。

将 activity 从分屏提取到全窗口

创建显示侧面 activity 全窗口的新配置,然后使用解析为同一实例的 intent 重新启动 activity。

在运行时检查分屏支持

Android 12L(API 级别 32)及更高版本支持 activity 嵌入,但某些搭载更低平台版本的设备也支持 activity 嵌入。如需在运行时检查该功能是否可用,请使用 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 嵌入模型)。

阻止系统替换

Android 设备的制造商(原始设备制造商)可将 activity 嵌入作为设备系统的函数来实现。系统会为多 activity 应用指定分屏规则,从而替换应用的窗口行为。系统替换会强制多 activity 应用进入系统定义的 activity 嵌入模式。

系统 activity 嵌入可通过多窗格布局(例如列表-详情)增强应用呈现效果,而无需对应用进行任何更改。不过,系统的 activity 嵌入也可能会导致应用布局不正确、出现 bug 或与应用所实现的 activity 嵌入冲突。

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

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application>
        <property
            android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE"
            android:value="true|false" />
    </application>
</manifest>

属性名称在 Jetpack WindowManager WindowProperties 对象中定义。如果您的应用实现了 activity 嵌入,或者您想阻止系统将其 activity 嵌入规则应用于您的应用,请将该值设为 false;若想允许系统将系统定义的 activity 嵌入应用于您的应用,请将值设为 true

限制条件和注意事项

  • 只有任务的托管应用(标识为任务中根 activity 的所有者)才能在任务中组织和嵌入其他 activity。如果支持嵌入和分屏的 activity 在属于其他应用的任务中运行,则嵌入和分屏将不适用于这些 activity。
  • 只能在单个任务中组织 activity。在新任务中启动 activity 时,始终都会将其放置在所有现有分屏之外的新展开窗口中。
  • 只能将同一进程中的 activity 整理放置在分屏中。SplitInfo 回调仅报告属于同一进程的 activity,因为无法知道其他进程中的 activity。
  • 每对或单个 activity 规则仅适用于在注册该规则后发生的 activity 启动。目前无法更新现有分屏或其视觉属性。
  • 分屏对过滤器配置必须与启动 activity 时使用的 intent 完全匹配。从应用进程中启动新的 activity 时会发生匹配,因此使用隐式 intent 时,可能不知道稍后在系统进程中解析的组件名称。如果在启动时不知道组件名称,可以改用通配符(“*/*”),系统会根据 intent 操作执行过滤。
  • 目前无法在容器之间移动 activity,也无法在创建分屏后将 activity 移入和移出分屏。只有在启动具有匹配规则的新 activity 时,WindowManager 库才会创建分屏;当分屏容器中的最后一个 activity 结束时,分屏会被销毁。
  • 当配置发生更改时可以重新启动 activity,因此在创建或移除了分屏以及 activity 边界发生更改时,activity 可以完全销毁之前的实例,并创建一个新的实例。因此,对于诸如从生命周期回调启动新 activity 之类的操作,应用开发者应格外小心。
  • 设备必须包含窗口扩展接口,以支持 activity 嵌入。几乎所有搭载 Android 12L(API 级别 32)或更高版本的大屏幕设备均包含接口。不过,一些无法运行多个 activity 的大屏幕设备未包含窗口扩展接口。如果大屏设备不支持多窗口模式,则可能不支持 activity 嵌入。

其他资源