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 C 向一侧拆分。
  • 在原来的 activity 之上原位启动一个 activity;即,在同一 activity 堆栈中:

    <ph type="x-smartling-placeholder">
    </ph>
    图 6.activity B 启动 activity C,并且没有额外的 intent 标志。
  • 在同一任务中启动一个 activity 全窗口:

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

返回导航

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

  • 一起参加:如果活动相关,但不应显示活动 则返回导航可配置为同时完成这两项。
  • 单独执行:如果 activity 完全独立,则一个 activity 上的返回导航不影响任务窗口中另一个 activity 的状态。

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

对于基于手势的导航:

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

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

    • 处理同一应用中的多个 activity 时,无论滑动方向如何,此手势都会关闭顶部 activity,从而提供更统一的体验。

    • 在涉及来自不同应用(叠加层)的两个 activity 的情况下, 返回事件被定向到获得焦点的最后一个活动,并与 按钮导航行为。

多窗格布局

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

一个常见的示例是列表-详情分屏。为了确保高质量的呈现,系统先启动列表 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,是因为 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, 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 清单设置让应用可以限制 调整为纵向或横向。改善用户体验 在平板电脑和可折叠设备等大屏设备上,设备制造商 (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 嵌入 调用。在子类的 onCreate() 方法中进行调用: Application,以确保这些规则在任何 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():定义可用显示区域的外观 分配给每个活动容器比率拆分类型指定 分配给 主要容器;辅助容器会占用 显示区域。
      • 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():设置 两个展示广告尺寸中的较小者 无论设备的屏幕方向如何
      • setMaxAspectRatioInPortrait():设置在纵向模式下显示 activity 分屏的最大宽高比(高度:宽度)。如果纵向模式显示屏的宽高比超过最大宽高比,则无论显示屏的宽度如何,都会停用分屏。注意:默认值为 1.4, 会导致 activity 在纵向模式下占据整个任务窗口 大多数平板电脑上的屏幕方向。另请参阅 SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULTsetMaxAspectRatioInLandscape()。默认值 横向为 ALWAYS_ALLOW
      • setFinishPrimaryWithSecondary():设置完成所有操作的方式 辅助容器中的 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 相关联。
      • 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,用于标识应该 会占据整个任务显示区域:

      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 过滤条件, 通过确定您要进行的活动 排除哪些数据
      • 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。应用可以将主机指定为受信任 或不受信任。

受信任的托管

允许其他应用嵌入并完全控制 活动,请指定主机的 SHA-256 证书 指定应用的 android:knownActivityEmbeddingCerts 属性 应用清单文件中的 <activity><application> 元素。

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 都在同一任务中启动。

分屏示例

从全窗口分屏

图 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 时,新任务将与包含分屏的任务分开并显示在全窗口中。“最近使用的应用”屏幕显示两项任务:分屏中的任务和新任务。

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

activity 替换

可以在辅助容器堆栈中替换 activity;例如,当 主要 activity 用于顶级导航,辅助 activity 用于实现顶层导航 是一个选定的目的地。每当从顶级导航中选择一项时,都应在辅助容器中启动一个新的 activity,并移除之前在辅助容器中的一个或多个 activity。

图 21. 主要窗格中的顶级导航 activity 替换辅助窗格中的目的地 activity。

如果在导航选择发生变化时应用未完成辅助容器中的 activity,那么在分屏收起后(设备折叠后),返回导航可能会令人感到困惑。例如,如果您在 当用户 折叠手机,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,因此用户可以 学完 C 之后,进入 A/B 分组。

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

如需创建新的分屏,请从现有辅助容器中在侧面启动新的 activity。声明 A/B 分屏和 B/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
    &lt;!-- Do not finish primary container activities when all secondary container activities finish. --&gt;
    window:finishPrimaryWithSecondary="never"
    &lt;!-- Finish secondary container activities when all primary container activities finish. --&gt;
    window:finishSecondaryWithPrimary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

默认配置

当分屏中一个容器中的所有 activity 都结束时,剩余容器 占据整个窗口:

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

分屏包含 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,然后使用新配置再次启动到侧面。

动态分屏属性

Jetpack WindowManager 1.4 及更高版本支持的 Android 15(API 级别 35)及更高版本提供了动态功能,可让 activity 嵌入分屏可配置,包括:

  • 窗格展开:一种可拖动的交互式分隔线,可让用户 在拆分演示文稿中调整窗格大小。
  • activity 固定:用户可以在一个容器中固定内容,并 将容器中的导航与其他容器中的导航隔离开来。
  • 调暗全屏对话框颜色:在显示对话框时,应用可以指定 调暗整个任务窗口,还是只调暗打开 对话框。

窗格展开

借助窗格展开功能,用户可以调整为双窗格布局中的两个 activity 分配的屏幕空间量。

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

  1. 创建 DividerAttributes 实例

  2. 自定义分隔线属性:

    • color可拖动窗格分隔线的颜色。

    • widthDp:可拖动窗格分隔符的宽度。设置为 WIDTH_SYSTEM_DEFAULT:让系统确定分隔线 宽度。

    • 拖动范围:任意窗格可以占据的屏幕空间的最小百分比 空间介于 0.33 到 0.66 之间。设置为 DRAG_RANGE_SYSTEM_DEFAULT 可让系统确定拖动范围。

Kotlin

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

if (WindowSdkExtensions.getInstance().extensionVersion >= 6) {
    splitAttributesBuilder.setDividerAttributes(
      DividerAttributes.DraggableDividerAttributes.Builder()
        .setColor(getColor(context, R.color.divider_color))
        .setWidthDp(4)
        .setDragRange(DividerAttributes.DragRange.DRAG_RANGE_SYSTEM_DEFAULT)
        .build()
    )
}
val splitAttributes: SplitAttributes = splitAttributesBuilder.build()

Java

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

if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 6) {
    splitAttributesBuilder.setDividerAttributes(
      new DividerAttributes.DraggableDividerAttributes.Builder()
        .setColor(ContextCompat.getColor(context, R.color.divider_color))
        .setWidthDp(4)
        .setDragRange(DividerAttributes.DragRange.DRAG_RANGE_SYSTEM_DEFAULT)
        .build()
    );
}
SplitAttributes splitAttributes = splitAttributesBuilder.build();

activity 固定

通过活动固定功能,用户可以固定其中一个分屏窗口, 当用户在另一个窗口中导航时,activity 会保持不变。activity 固定功能可提供增强型多任务处理体验。

如需在应用中启用 activity 固定,请执行以下操作:

  1. 向要固定的 activity 的布局文件添加一个按钮,例如列表-详情布局的详情 activity:

    <androidx.constraintlayout.widget.ConstraintLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/detailActivity"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:background="@color/white"
     tools:context=".DetailActivity">
    
    <TextView
       android:id="@+id/textViewItemDetail"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:textSize="36sp"
       android:textColor="@color/obsidian"
       app:layout_constraintBottom_toTopOf="@id/pinButton"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />
    
    <androidx.appcompat.widget.AppCompatButton
       android:id="@+id/pinButton"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@string/pin_this_activity"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@id/textViewItemDetail"/>
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    
  2. 在 activity 的 onCreate() 方法中,为按钮设置 onclick 监听器:

    Kotlin

    pinButton = findViewById(R.id.pinButton)
    pinButton.setOnClickListener {
        val splitAttributes: SplitAttributes = SplitAttributes.Builder()
            .setSplitType(SplitAttributes.SplitType.ratio(0.66f))
            .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
            .build()
    
        val pinSplitRule = SplitPinRule.Builder()
            .setSticky(true)
            .setDefaultSplitAttributes(splitAttributes)
            .build()
    
        SplitController.getInstance(applicationContext).pinTopActivityStack(taskId, pinSplitRule)
    }

    Java

    Button pinButton = findViewById(R.id.pinButton);
    pinButton.setOnClickListener( (view) => {
        SplitAttributes splitAttributes = new SplitAttributes.Builder()
            .setSplitType(SplitAttributes.SplitType.ratio(0.66f))
            .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
            .build();
    
        SplitPinRule pinSplitRule = new SplitPinRule.Builder()
            .setSticky(true)
            .setDefaultSplitAttributes(splitAttributes)
            .build();
    
        SplitController.getInstance(getApplicationContext()).pinTopActivityStack(getTaskId(), pinSplitRule);
    });

全屏调暗

activity 通常会调暗显示屏,以吸引用户注意对话框。在 activity 嵌入,双窗格显示屏的两个窗格都应变暗,而不是 仅包含包含打开对话框的 activity 的窗格,以实现统一的界面 体验。

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

如需仅调暗打开对话框的 activity 的容器,请使用 EmbeddingConfiguration.DimAreaBehavior.ON_ACTIVITY_STACK

将 activity 从分屏提取到全窗口

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

在运行时检查分屏支持

Android 12L(API 级别 32)及更高版本支持 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 嵌入冲突。

您的应用可以通过设置 property,例如:

<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 时,可能不知道稍后在系统进程中解析的组件名称。如果在启动时不知道组件名称, 也可以使用通配符(“*/*”),并且可以根据 。
  • 目前无法在容器之间移动 activity,也无法在创建分屏后将 activity 移入和移出分屏。只有在启动具有匹配规则的新 activity 时,WindowManager 库才会创建分屏;当分屏容器中的最后一个 activity 结束时,分屏会被销毁。
  • 当配置发生更改时可以重新启动 activity,因此在创建或移除了分屏以及 activity 边界发生更改时,activity 可以完全销毁之前的实例,并创建一个新的实例。因此,对于诸如从生命周期回调启动新 activity 之类的操作,应用开发者应格外小心。
  • 设备必须包含窗口扩展接口,以支持 activity 嵌入。几乎所有搭载 Android 12L(API 级别)的大屏设备 32) 或更高版本的 SDK。不过,一些大屏设备 无法运行多个 activity,不包括窗口 扩展接口。如果大屏设备不支持多窗口模式 模式,那么它可能不支持 activity 嵌入。

其他资源