activity 嵌入

使用集合让一切井井有条 根据您的偏好保存内容并对其进行分类。

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

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

更新旧版代码库以支持大屏幕需要耗费大量人力和时间。使用 fragment 将基于 activity 的应用转换为多窗格布局需要进行重大重构。

activity 嵌入只需要对应用进行很少的重构或根本不需要对应用进行重构。至于应用如何显示其 activity(是并排,还是堆叠),可以通过创建 XML 配置文件或进行 Jetpack WindowManager API 调用来确定。

系统会自动维护对小屏幕的支持。当应用在配备小屏幕的设备上时,activity 会相互堆叠。在大屏幕上,activity 会并排显示。系统会根据您已创建的配置(不需要分支逻辑)来确定呈现方式。

activity 嵌入支持设备屏幕方向的变化,并且可以在可折叠设备上无缝工作,该功能会随着设备折叠和展开而堆叠和取消堆叠 activity。

Modern Android Development 使用单 activity 架构,其中包含 fragment、导航组件和多功能布局管理器,如 SlidingPaneLayout

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

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

拆分任务窗口

activity 嵌入会将应用任务窗口拆分成两个容器:主要容器和辅助容器。这些容器存放从主 activity 或从已在容器中的其他 activity 启动的 activity。

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

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

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

多窗格布局

Jetpack WindowManager 1.1.0 Alpha04 可让您在搭载 12L(API 级别 32)的大屏幕设备上和某些搭载早期平台版本的设备上构建包含 activity 的多窗格布局。基于多个 activity 而非 fragment 或基于视图的布局(如 SlidingPaneLayout)的现有应用可以提供改进的大屏幕用户体验,而无需进行重大重构。

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

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

分屏比

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

图 9. 分屏比不同的两个 activity 分屏。

占位符

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

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

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

不过,SplitPlaceholderRulestickyPlaceholder 属性可以替换默认行为。将此属性设置为 true 时,如果将显示从双窗格显示缩小为单窗格显示,系统会将占位符显示为任务窗口中的顶部 activity(请参阅分屏配置)。

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

窗口大小变化

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

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

之所以能够堆叠 activity,是因为 WindowManager 会将辅助窗格中的 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-alpha04'

    WindowManager 的 SplitController 组件用于管理分屏。

  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

    例如:

    <!-- split_configuration.xml -->
    
    <resources
        xmlns:window="http://schemas.android.com/apk/res-auto">
    
        <!-- Define a split for the named activities. -->
        <SplitPairRule
            window:splitRatio="0.33"
            window:splitMinWidth="840dp"
            window:finishPrimaryWithSecondary="never"
            window:finishSecondaryWithPrimary="always">
            <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:splitMinWidth="840dp"
            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. -->
        <ActivityRule
            window:alwaysExpand="true">
            <ActivityFilter
                window:activityName=".ExpandedActivity"/>
        </ActivityRule>
    
    </resources>
    
  2. 创建初始化程序。

    WindowManager 的 SplitController 组件会根据 XML 配置文件中的规则来管理 activity 分屏。Jetpack StartupInitializer 会在应用启动时让 SplitController 能够使用分屏规则,以便 SplitController 可以根据需要在 activity 启动时应用这些规则。

    如需创建初始化程序,请执行以下操作:

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

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

    2. 创建一个实现 Initializer 接口的类。

      初始化程序会向组件的 initialize() 方法提供包含定义 (split_configuration.xml) 的 XML 资源文件 ID,以便 SplitController 能够使用分屏规则。

      Kotlin

      class SplitInitializer : Initializer<SplitController> {
      
       override fun create(context: Context): SplitController {
           SplitController.initialize(context, R.xml.split_configuration)
           return SplitController.getInstance()
       }
      
       override fun dependencies(): List<Class<out Initializer<*>>> {
           return emptyList()
       }
      }
      

      Java

      class SplitInitializer extends Initializer<SplitController> {
      
       @Override
       SplitController create(Context context) {
           SplitController.initialize(context, R.xml.split_configuration);
           return SplitController.getInstance();
       }
      
       @Override
       List<Class<? extends Initializer<?>>> dependencies() {
           return Collections.emptyList();
       }
      }
      
  3. 为规则定义创建 content provider。

    androidx.startup.InitializationProvider 作为 <provider> 添加到应用清单文件中。添加对 SplitController 初始化程序实现 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 嵌入。在分屏的主要 activity 的 onCreate() 方法中进行调用,也就是启动分屏的 activity。

如需程序化地创建 activity 分屏,请执行以下操作:

  1. 创建分屏规则:

    1. 创建一个 SplitPairFilter,用于标识共享分屏的 activity:

      Kotlin

      val splitPairFilter = SplitPairFilter(
       ComponentName(applicationContext, ListActivity::class.java),
       ComponentName(applicationContext, 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 minWidth = (800f * resources.displayMetrics.density).toInt()
      val minSmallestWidth = (600f * resources.displayMetrics.density).toInt()
      

      Java

      int minWidth = (int)(800f * getResources().getDisplayMetrics().density);
      int minSmallestWidth = (int)(600f * getResources().getDisplayMetrics().density);
      
      • minWidth:指定允许分屏的最小显示宽度(以像素为单位)
      • minSmallestWidth:指定两个显示尺寸中的较小尺寸必须达到的最低值(以像素为单位),才能允许分屏(无论设备方向为何)。
    4. 构建 SplitPairRule

      Kotlin

      val splitPairRule = SplitPairRule.Builder(
           filterSet,
           minWidth,
           minSmallestWidth
       ).setSplitRatio(0.33f)
        .setFinishPrimaryWithSecondary(0)
        .setFinishSecondaryWithPrimary(1)
        .setClearTop(false)
        .build()
      

      Java

      SplitPairRule splitPairRule = new SplitPairRule.Builder(
       filterSet,
       minWidth,
       minSmallestWidth
      ).setSplitRatio(0.33f)
      .setFinishPrimaryWithSecondary(0)
      .setFinishSecondaryWithPrimary(1)
      .setClearTop(false)
      .build();
      

      使用 SplitPairRule.Builder 创建规则并设置以下规则属性:

      • splitRatio:主要容器的可用显示区域所占的比例;辅助容器填满可用显示区域的其余部分
      • finishPrimaryWithSecondary:完成辅助容器中的所有 activity 对主要容器中的 activity 有何影响;0 = 永不完成 activity(请参阅完成 activity
      • finishSecondaryWithPrimary:完成主要容器中的所有 activity 对辅助容器中的 activity 有何影响;1 = 始终完成 activity(请参阅完成 activity
      • clearTop:在辅助容器中启动新 activity 时,该容器中的所有 activity 是否都完成
    5. 获取 WindowManager SplitController 的实例,并注册规则:

      Kotlin

      val splitController = SplitController.getInstance()
      splitController.registerRule(splitPairRule)
      

      Java

      SplitController splitController = SplitController.getInstance();
      splitController.registerRule(splitPairRule);
      
  2. 当内容不可用时,为辅助容器创建占位符

    1. 创建一个 ActivityFilter,用于标识哪个 activity 会与占位符共享任务窗口分屏:

      Kotlin

      val activityFilter = ActivityFilter(
        ComponentName(applicationContext, ListActivity::class.java),
        null
      )
      

      Java

      ActivityFilter activityFilter = new ActivityFilter(
        new ComponentName(this, ListActivity.class),
        null
      );
      
    2. 将过滤条件添加到过滤条件集:

      Kotlin

      val activityFilterSet = setOf(activityFilter)
      

      Java

      Set<ActivityFilter> activityFilterSet = new HashSet<>();
      activityFilterSet.add(activityFilter);
      
    3. 创建 SplitPlaceholderRule

      Kotlin

      val splitPlaceholderRule = SplitPlaceholderRule.Builder(
            activityFilterSet,
            Intent(applicationContext, PlaceholderActivity::class.java),
            minWidth,
            minSmallestWidth
        ).setSplitRatio(0.33f)
         .setFinishPrimaryWithPlaceholder(1)
         .build()
      

      Java

      SplitPlaceholderRule splitPlaceholderRule = new SplitPlaceholderRule.Builder(
        activityFilterSet,
        new Intent(this, PlaceholderActivity.class),
        minWidth,
        minSmallestWidth
      ).setSplitRatio(0.33f)
      .setFinishPrimaryWithPlaceholder(1)
      .build();
      

      使用 SplitPlaceholderRule.Builder 创建规则并设置以下规则属性:

      • splitRatio:主要容器的可用显示区域所占的比例;占位符 activity 占用可用显示区域的其余部分
      • finishPrimaryWithPlaceholder:完成占位符 activity 对主要容器中的 activity 有何影响;1 = 始终完成 activity(请参阅完成 activity
    4. 向 WindowManager SplitController 注册规则:

      Kotlin

      splitController.registerRule(splitPlaceholderRule)
      

      Java

      splitController.registerRule(splitPlaceholderRule);
      
  3. 指定绝不应属于分屏的 activity:

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

      Kotlin

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

      Java

      ActivityFilter expandedActivityFilter = new ActivityFilter(
         new ComponentName(this, ExpandedActivity.class),
         null
      );
      
    2. 将过滤条件添加到过滤条件集:

      Kotlin

      val expandedfilterSet = setOf(expandedActivityFilter)
      

      Java

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

      Kotlin

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

      Java

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

      使用 ActivityRule.Builder 创建规则并设置以下规则属性:

    4. 向 WindowManager SplitController 注册规则:

      Kotlin

      splitController.registerRule(activityRule)
      

      Java

      splitController.registerRule(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-1 证书。

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-1 指纹,不含分号分隔符。如需了解详情,请参阅生成签名报告对客户端进行身份验证

不受信任的主机

如需允许任何应用都能嵌入您应用的 activity 并控制其呈现方式,请在应用清单的 <activity><application> 元素中指定 android:allowUntrustedActivityEmbedding 属性,例如:

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

此属性的默认值为 false,这样可以阻止跨应用 activity 嵌入。

自定义身份验证

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

使用 Jetpack WindowManager 库中的 SplitController#isActivityEmbedded() 方法检查主机是否嵌入了您的 activity,例如:

Kotlin

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

Java

public static boolean isActivityEmbedded(@NonNull Activity activity) {
    return SplitController.getInstance().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:placeholderIntentName=".Placeholder">
    <ActivityFilter
        window:activityName=".Main"/>
</SplitPlaceholderRule>

当应用收到 intent 时,目标 activity 可以显示为 activity 分屏的辅助部分;例如,请求显示详情屏幕,该屏幕包含有关列表中某一项的信息。在小显示屏上,详情显示在完整的任务窗口中;在较大的设备上,详情显示在列表旁边。

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

启动请求应传送到主 activity,并且目标详情 activity 应在分屏中启动。SplitController 会根据可用的显示宽度自动选择正确的呈现方式(堆叠或并排)。

Kotlin

override fun onCreate(savedInstanceState Bundle?) {
    …
    splitController.registerRule(SplitPairRule(newFilters))
    startActivity(Intent(this, DetailActivity::class.java))
}

Java

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    …
    splitController.registerRule(new SplitPairRule(newFilters));
    startActivity(new Intent(this, DetailActivity.class));
}

深层链接目的地可能是返回导航堆栈中应可供用户使用的唯一一个 activity,并且您可能希望避免关闭详情 activity 而只留下主 activity:

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

仅显示详情 activity 的小显示屏。返回导航无法关闭详情 activity 而显示列表 activity。

您可以使用 finishPrimaryWithSecondary 属性来同时结束这两个 activity:

<SplitPairRule
    window:finishPrimaryWithSecondary="always">
    <SplitPairFilter
        window:primaryActivityName=".List"
        window:secondaryActivityName=".Detail"/>
</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 注册一个监听器来监听分屏状态的变化。然后,相应地调整界面:

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    splitController
        .addSplitListener(this, mainThreadExecutor, SplitInfoChangeCallback())
}

inner class SplitInfoChangeCallback : Consumer<List<SplitInfo>> {
    override fun accept(splitInfoList: List<SplitInfo>) {
        findViewById<View>(R.id.infoButton).visibility =
            if (!splitInfoList.isEmpty()) View.GONE else View.VISIBLE
    }
}

Java

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    splitController
        .addSplitListener(this, mainThreadExecutor, SplitInfoChangeCallback());
}

class SplitInfoChangeCallback extends Consumer<List<SplitInfo>> {
    public void accept(List<SplitInfo> splitInfoList) {
        findViewById<View>(R.id.infoButton).visibility =
            !splitInfoList.isEmpty()) ? View.GONE : View.VISIBLE;
    }
}

可以在任何生命周期状态下进行回调,包括当 activity 停止时。通常应在 onStart() 中注册监听器,在 onStop() 中取消注册监听器。

全窗口模态

某些 activity 会阻止用户与应用互动,直到执行了指定的操作;例如,登录屏幕 activity、政策确认屏幕或错误消息。应防止模态 activity 出现在分屏中。

您可以使用展开配置来强制 activity 始终填满任务窗口:

<ActivityRule
    window:alwaysExpand="true">
    <ActivityFilter
        window:activityName=".FullWidthActivity"/>
</ActivityRule>

结束 activity

用户可以通过从显示屏的边缘滑动,在分屏的任意一侧结束 activity:

图 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 the primary activity when the secondary activity finishes. -->
    window:finishPrimaryWithSecondary="never"
    <!-- Finish the secondary activity whenever the primary activity finishes. -->
    window:finishSecondaryWithPrimary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

默认配置

当分屏中的一个 activity 完成时,剩下的 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 完成时,一起完成 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.isSplitSupported() 方法:

Kotlin

val splitController = SplitController.Companion.getInstance()
if (splitController.isSplitSupported()) {
    // Device supports split activity features.
}

Java

SplitController splitController = SplitController.Companion.getInstance();
if (splitController.isSplitSupported()) {
  // Device supports split activity features.
}

如果不支持分屏,系统会在顶部启动 activity(遵循非 activity 嵌入模型)。

阻止系统替换

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

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

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

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

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

限制条件和注意事项

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

其他资源