使用 activity 嵌入和 Material Design 构建列表-详情布局

1. 简介

大显示屏让您可以创建有助于提升用户体验并提高用户工作效率的应用布局和界面。但是,如果您的应用是专为非可折叠手机的小显示屏而设计的,那么它可能无法利用平板电脑、可折叠设备和 ChromeOS 设备提供的额外显示空间。

更新应用以充分利用大显示屏可能非常耗时且成本高昂,尤其是对于基于多个 activity 的旧版应用而言。

Android 12L(API 级别 32)中引入的 activity 嵌入可让基于 activity 的应用在大屏设备上同时显示多个 activity,从而创建双窗格布局,例如列表-详情布局。无需 Kotlin 或 Java 重新编码。您需要添加一些依赖项,创建一个 XML 配置文件,实现一个初始化程序,并向您的应用清单添加一些内容。或者,如果您更喜欢在代码中操作,只需向应用的主要 activity 的 onCreate() 方法添加一些 Jetpack WindowManager API 调用即可。

前提条件

如要完成此 Codelab,您需要具备以下经验:

  • 构建 Android 应用
  • 使用 activity
  • 编写 XML
  • 在 Android Studio 中工作,包括虚拟设备设置

您将构建的内容

在此 Codelab 中,您将更新一个基于 activity 的应用,以支持与 SlidingPaneLayout 类似的动态双窗格布局。在小屏幕上,应用会在任务窗口中叠加(堆叠)activity。

堆叠在任务窗口中的 activity A、activity B 和 activity C。

在大屏设备上,该应用会根据您的规格,在屏幕上以并排或一上一下的方式同时显示两个 activity。

4b27b07b7361d6d8.png

学习内容

如何通过两种方式实现 activity 嵌入:

  • 使用 XML 配置文件
  • 使用 Jetpack WindowManager API 调用

所需条件

  • 最新版本的 Android Studio
  • Android 手机或模拟器
  • Android 小屏平板电脑或模拟器
  • Android 大屏平板电脑或模拟器

2. 设置

获取示例应用

第 1 步:克隆代码库

克隆大屏幕 Codelab Git 代码库:

git clone https://github.com/android/large-screen-codelabs

或下载并解压大屏 Codelab zip 文件:

下载源代码

第 2 步:检查 Codelab 源文件

导航到 activity-embedding 文件夹。

第 3 步:打开 Codelab 项目

在 Android Studio 中,打开 Kotlin 或 Java 项目

代码库和 ZIP 文件中的 activity 文件夹的文件列表。

代码库和 ZIP 文件中的 activity-embedding 文件夹包含两个 Android Studio 项目:一个是 Kotlin 项目,一个是 Java 项目。打开您选择的项目。Codelab 代码段同时以这两种语言提供。

创建虚拟设备

如果您没有 API 级别为 32 或更高级别的 Android 手机、小屏平板电脑或大屏平板电脑,请在 Android Studio 中打开设备管理器,并创建您所需的以下任意一种虚拟设备:

  • 手机 - Pixel 6,API 级别 32 或更高级别
  • 小屏平板电脑 - 7 WSVGA(平板电脑),API 级别 32 或更高级别
  • 大屏平板电脑 - Pixel C,API 级别 32 或更高级别

3. 运行应用

示例应用会显示项目列表。当用户选择某个列表项时,应用会显示该列表项的相关信息。

该应用包含三个 activity:

  • ListActivity - 包含 RecyclerView 中的项目列表
  • DetailActivity - 从列表中选择某个项目时,应用会显示该列表项的相关信息
  • SummaryActivity - 选择“摘要”列表项时显示信息摘要

不嵌入 activity 时的行为

运行示例应用,查看应用在不嵌入 activity 时的行为:

  1. 在大屏平板电脑或 Pixel C 模拟器上运行示例应用。系统会显示主(列表)activity:

纵向显示示例应用的大屏平板电脑。全屏显示列表 activity。

  1. 选择一个列表项以启动辅助(详情)activity。详情 activity 会叠加在列表 activity 上:

纵向显示示例应用的大屏平板电脑。全屏显示详情 activity。

  1. 将平板电脑旋转为横向模式。辅助 activity 仍会叠加在主要 activity 上,并占据整个屏幕:

横向显示示例应用的大屏平板电脑。全屏显示详情 activity。

  1. 选择返回控件(应用栏中的向左箭头)可返回到列表。
  2. 选择列表中的最后一项(即“摘要”),将摘要 activity 作为辅助 activity 启动。摘要 activity 会叠加在列表 activity 上:

纵向显示示例应用的大屏平板电脑。全屏显示摘要 activity。

  1. 将平板电脑旋转为横向模式。辅助 activity 仍会叠加在主要 activity 上,并占据整个屏幕:

横向显示示例应用的大屏平板电脑。全屏显示摘要 activity。

嵌入 activity 时的行为

完成此 Codelab 后,应用在横向模式下将在列表-详情布局中并排显示列表 activity 和详情 activity:

横向显示示例应用的大屏平板电脑。列表-详情布局中的列表 activity 和详情 activity。

不过,即使 activity 是在分屏模式下启动,您还是可以将摘要配置为全屏显示。摘要将叠加在分屏上:

横向显示示例应用的大屏平板电脑。全屏显示摘要 activity。

4. 背景

activity 嵌入会将应用任务窗口拆分成两个容器:主要容器和辅助容器。任何 activity 都可以通过启动另一个 activity 来创建分屏。启动 activity 占据主要容器;而被启动的 activity 则占据辅助容器。

主要 activity 可以在辅助容器中启动其他 activity。这样一来,两个容器中的 activity 都可以在其各自的容器中启动 activity。每个容器可以包含 activity 的堆栈。如需了解详情,请参阅 activity 嵌入开发者指南。

您可以通过创建 XML 配置文件或进行 Jetpack WindowManager API 调用,将应用配置为支持 activity 嵌入。我们先从 XML 配置方法开始。

5. XML 配置

Jetpack WindowManager 库会根据您在 XML 配置文件中创建的分屏规则来创建和管理 activity 嵌入容器和分屏。

添加 WindowManager 依赖项

通过将库依赖项添加到应用的模块级 build.gradle 文件,使示例应用能够访问 WindowManager 库,例如:

build.gradle

 implementation 'androidx.window:window:1.2.0'

告知系统

告知系统您的应用已实现 activity 嵌入。

为应用清单文件的 <application> 元素添加 android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED 属性,并将值设置为 true:

AndroidManifest.xml

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

设备制造商 (OEM) 使用该设置来为支持 activity 嵌入的应用启用自定义功能。例如,设备可以在横向显示屏上将仅限纵向模式的 activity 设为信箱模式(请参阅 android:screenOrientation)来确定 activity 的方向,以实现平滑过渡到 activity 嵌入双窗格布局。

在横向显示屏上对仅限纵向模式的应用使用 activity 嵌入。仅支持纵向显示且采用信箱模式的 activity A 启动嵌入的 activity B。

创建配置文件

在应用的 res/xml 文件夹中创建一个名为 main_split_config.xml 的 XML 资源文件,并将 resources 作为根元素。

将 XML 命名空间更改为:

main_split_config.xml

xmlns:window="http://schemas.android.com/apk/res-auto"

分屏对规则

将以下分屏规则添加到配置文件中:

main_split_config.xml

<!-- Define a split for the named activity pair. -->
<SplitPairRule
    window:splitRatio="0.33"
    window:splitMinWidthDp="840"
    window:finishPrimaryWithSecondary="never"
    window:finishSecondaryWithPrimary="always">
  <SplitPairFilter
      window:primaryActivityName=".ListActivity"
      window:secondaryActivityName=".DetailActivity"/>
</SplitPairRule>

该规则将执行以下操作:

  • 为共享分屏的 activity 配置分屏选项
  • splitRatio - 指定主要 activity 占据了任务窗口的比例 (33%),剩余空间则留给辅助 activity。
  • splitMinWidthDp - 指定两个 activity 在屏幕上同时显示所需的最小显示宽度 (840)。单位是密度无关像素 (dp)。
  • finishPrimaryWithSecondary - 指定当辅助容器中的所有 activity 结束时,主要分屏容器中的 activity 是否结束(永不)。
  • finishSecondaryWithPrimary - 指定当主要容器中的所有 activity 结束时,辅助分屏容器中的 activity 是否结束(始终)。
  • 包含一个分屏过滤器,用于定义共享任务窗口分屏的 activity。主要 activity 为 ListActivity;辅助 activity 为 DetailActivity

占位符规则

如果没有任何内容可用于辅助容器(例如,当列表-详情分屏打开时尚未选择任何列表项),占位符 activity 就会占据 activity 分屏的辅助容器。(如需了解详情,请参阅 activity 嵌入开发者指南中的占位符部分)。

将以下占位符规则添加到配置文件中:

main_split_config.xml

<!-- Automatically launch a placeholder for the detail activity. -->
<SplitPlaceholderRule
    window:placeholderActivityName=".PlaceholderActivity"
    window:splitRatio="0.33"
    window:splitMinWidthDp="840"
    window:finishPrimaryWithPlaceholder="always"
    window:stickyPlaceholder="false">
  <ActivityFilter
      window:activityName=".ListActivity"/>
</SplitPlaceholderRule>

该规则将执行以下操作:

  • 标识占位符 activity PlaceholderActivity(我们将在下一步中创建此 activity)
  • 配置占位符的选项:
  • splitRatio - 指定主要 activity 占据了任务窗口的比例 (33%),剩余空间则留给占位符。通常,此值应与占位符关联的分屏对规则的分屏比率一致。
  • splitMinWidthDp - 指定占位符与主要 activity 在屏幕上同时显示所需的最小显示宽度 (840)。通常,此值应与占位符关联的分屏对规则的最小宽度一致。单位是密度无关像素 (dp)。
  • finishPrimaryWithPlaceholder - 指定当占位符结束时,主要分屏容器中的 activity 是否结束(始终)。
  • stickyPlaceholder - 指示在将显示从双窗格显示缩小为单窗格显示(例如,当可折叠设备被折叠时),占位符是否应作为顶层 activity 保留在屏幕上 (false)。
  • 它包括一个 activity 过滤器,用于指定与占位符共享任务窗口分屏的 activity (ListActivity)。

该占位符代表分屏对规则的辅助 activity,其主要 activity 与占位符 activity 过滤器中的 activity 相同(请参阅本 Codelab 的“XML 配置”部分中的“分屏对规则”)。

activity 规则

activity 规则是通用规则。您可以通过 activity 规则指定需要占据整个任务窗口(即绝不会成为分屏的一部分)的 activity。(如需了解详情,请参阅 activity 嵌入开发者指南中的全窗口模态部分)。

我们将使摘要 activity 填充整个任务窗口,并叠加在分屏上。返回导航操作将返回到分屏。

将以下 activity 规则添加到配置文件中:

main_split_config.xml

<!-- Activities that should never be in a split. -->
<ActivityRule
    window:alwaysExpand="true">
  <ActivityFilter
      window:activityName=".SummaryActivity"/>
</ActivityRule>

该规则将执行以下操作:

  • 用于标识应全屏显示的 activity (SummaryActivity).
  • 为 activity 配置选项:
  • alwaysExpand - 指定 activity 是否应展开以填充所有可用的显示空间。

源文件

完成的 XML 配置文件应如下所示:

main_split_config.xml

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:window="http://schemas.android.com/apk/res-auto">

    <!-- Define a split for the named activity pair. -->
    <SplitPairRule
        window:splitRatio="0.33"
        window:splitMinWidthDp="840"
        window:finishPrimaryWithSecondary="never"
        window:finishSecondaryWithPrimary="always">
      <SplitPairFilter
          window:primaryActivityName=".ListActivity"
          window:secondaryActivityName=".DetailActivity"/>
    </SplitPairRule>

    <!-- Automatically launch a placeholder for the detail activity. -->
    <SplitPlaceholderRule
        window:placeholderActivityName=".PlaceholderActivity"
        window:splitRatio="0.33"
        window:splitMinWidthDp="840"
        window:finishPrimaryWithPlaceholder="always"
        window:stickyPlaceholder="false">
      <ActivityFilter
          window:activityName=".ListActivity"/>
    </SplitPlaceholderRule>

    <!-- Activities that should never be in a split. -->
    <ActivityRule
        window:alwaysExpand="true">
      <ActivityFilter
          window:activityName=".SummaryActivity"/>
    </ActivityRule>

</resources>

创建占位符 activity

您需要创建一个新的 activity,作为在 XML 配置文件中指定的占位符。这个 activity 可以非常简单 - 只需显示一些内容,告诉用户内容最终会出现在这里。

在示例应用的主要源文件夹中创建 activity。

在 Android Studio 中,执行以下操作:

  1. 右键点击(辅助按钮-点击)示例应用源文件夹 com.example.activity_embedding
  2. 依次选择 New > Activity > Empty View Activity
  3. 将该 activity 命名为“placeholderActivity”
  4. 选择 Finish

Android Studio 在示例应用软件包中创建 activity,将其添加到应用清单文件中,并在 res/layout 文件夹中创建一个名为 activity_placeholder.xml 的布局资源文件。

  1. 在示例应用的 AndroidManifest.xml 文件中,将占位符 activity 的标签设置为空字符串:

AndroidManifest.xml

<activity
    android:name=".PlaceholderActivity"
    android:exported="false"
    android:label="" />
  1. res/layout 文件夹中 activity_placeholder.xml 布局文件的内容替换为以下代码:

activity_placeholder.xml

<?xml version="1.0" encoding="utf-8"?>
<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:background="@color/gray"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".PlaceholderActivity">

  <TextView
      android:id="@+id/textViewPlaceholder"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@string/placeholder_text"
      android:textSize="36sp"
      android:textColor="@color/obsidian"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
  1. 最后,将以下字符串资源添加到 res/values 文件夹的 strings.xml 资源文件中:

strings.xml

<string name="placeholder_text">Placeholder</string>

创建初始化程序

WindowManager RuleController 组件会解析 XML 配置文件中定义的规则,并将这些规则提供给系统。

Jetpack Startup 库的初始化程序可让 RuleController 访问配置文件。

Startup 库会在应用启动时执行组件初始化。初始化必须在任何 activity 启动之前进行,以便 RuleController 能够访问分屏规则,并且可以根据需要应用这些规则。

添加 Startup 库依赖项

如需启用启动功能,请将 Startup 库依赖项添加到示例应用的模块级 build.gradle 文件中,示例如下:

build.gradle

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

为 RuleController 实现初始化程序

创建 Startup 初始化程序接口的实现。

在 Android Studio 中,执行以下操作:

  1. 右键点击(辅助按钮-点击)示例应用源文件夹 com.example.activity_embedding
  2. 依次选择 New > Kotlin Class/FileNew > Java Class
  3. 将类命名为“SplitInitializer”
  4. Enter - Android Studio 会在示例应用软件包中创建该类。
  5. 将类文件的内容替换为以下代码:

SplitInitializer.kt

package com.example.activity_embedding

import android.content.Context
import androidx.startup.Initializer
import androidx.window.embedding.RuleController

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()
  }
}

SplitInitializer.java

package com.example.activity_embedding;

import android.content.Context;
import androidx.annotation.NonNull;
import androidx.startup.Initializer;
import androidx.window.embedding.RuleController;
import java.util.Collections;
import java.util.List;

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();
   }
}

初始化程序将包含定义 (main_split_config 的 XML 资源文件的 ID 传递给组件的 parseRules() 方法,从而将分屏规则提供给 RuleController 组件。setRules() 方法会将解析后的规则添加到 RuleController

创建初始化提供程序

提供程序会调用分屏规则初始化过程。

androidx.startup.InitializationProvider 作为提供程序添加到示例应用的清单文件的 <application> 元素中,然后引用 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 会初始化 SplitInitializer,而后者会调用 RuleController 方法,用于解析 XML 配置文件 (main_split_config.xml) 并将规则添加到 RuleController 中(请参阅上文的“为 RuleController 实现初始化程序”)。

InitializationProvider 会在应用的 onCreate() 方法执行之前发现并初始化 SplitInitializer;因此,分屏规则在应用主要 activity 启动时生效。

源文件

下面是完成后的应用清单:

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

  <application
      android:allowBackup="true"
      android:dataExtractionRules="@xml/data_extraction_rules"
      android:fullBackupContent="@xml/backup_rules"
      android:icon="@mipmap/ic_launcher"
      android:label="@string/app_name"
      android:roundIcon="@mipmap/ic_launcher_round"
      android:supportsRtl="true"
      android:theme="@style/Theme.Activity_Embedding"
      tools:targetApi="32">
    <activity
        android:name=".ListActivity"
        android:exported="true">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
    <activity
        android:name=".DetailActivity"
        android:exported="false"
        android:label="" />
    <activity
        android:name=".SummaryActivity"
        android:exported="false"
        android:label="" />
    <activity
        android:name=".PlaceholderActivity"
        android:exported="false"
        android:label="" />
    <property
        android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"
        android:value="true" />
    <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>
  </application>

</manifest>

初始化快捷方式

如果您习惯于将 XML 配置与 WindowManager API 一起使用,可以消除 Startup 库初始化程序和清单提供程序,从而更轻松地实现。

创建 XML 配置文件后,请执行以下操作:

第 1 步:创建 Application 的子类

在创建应用进程时,应用子类将是实例化的第一个类。您需要在子类的 onCreate() 方法中将分屏规则添加到 RuleController,以确保这些规则在任何 activity 启动之前生效。

在 Android Studio 中,执行以下操作:

  1. 右键点击(辅助按钮-点击)示例应用源文件夹 com.example.activity_embedding
  2. 依次选择 New > Kotlin Class/FileNew > Java Class
  3. 将此类命名为“SampleApplication”
  4. Enter - Android Studio 会在示例应用软件包中创建该类
  5. Application 超类型扩展类

SampleApplication.kt

package com.example.activity_embedding

import android.app.Application

/**
 * Initializer for activity embedding split rules.
 */
class SampleApplication : Application() {

}

SampleApplication.java

package com.example.activity_embedding;

import android.app.Application;

/**
 * Initializer for activity embedding split rules.
 */
public class SampleApplication extends Application {

}

第 2 步:初始化 RuleController

在应用子类的 onCreate() 方法中,将 XML 配置文件中的分屏规则添加到 RuleController

如需将规则添加到 RuleController,请执行以下操作:

  1. 获取 RuleController 的单例实例
  2. 使用 RuleController 的 Java 静态或 Kotlin 配套的 parseRules() 方法解析 XML 文件
  3. 使用 setRules() 方法将解析后的规则添加到 RuleController

SampleApplication.kt

override fun onCreate() {
  super.onCreate()
  RuleController.getInstance(this)
    .setRules(RuleController.parseRules(this, R.xml.main_split_config))
}

SampleApplication.java

@Override
public void onCreate() {
  super.onCreate();
  RuleController.getInstance(this)
    .setRules(RuleController.parseRules(this, R.xml.main_split_config));
}

第 3 步:将您的子类名称添加到清单中

将子类的名称添加到应用清单的 <application> 元素中:

AndroidManifest.xml

<application
    android:name=".SampleApplication"
    . . .

开始运行!

构建和运行示例应用。

在不可折叠的手机上,这些 activity 始终堆叠显示 - 即使处于横屏模式:

详情(辅助)activity 堆叠在手机上列表(主要)activity 上(纵向模式)。 详情(辅助)activity 堆叠在手机上列表(主要)activity 上(横向模式)。

在 Android 13(API 级别 33)及更低版本中,无论分屏最小宽度规范如何,非可折叠手机上都不支持 activity 嵌入。

对于 API 级别较高的非可折叠手机,是否支持 activity 嵌入取决于设备制造商是否支持 activity 嵌入。

在小屏平板电脑或 7 WSVGA(平板电脑)模拟器上,两个 activity 在纵向模式下会堆叠显示,而在横向模式下则会并排显示:

在小屏平板电脑上,纵向模式下列表和详情 activity 堆叠显示。 在小屏平板电脑上,横向模式下列表和详情 activity 并排显示。

在大屏平板电脑或 Pixel C 模拟器上,activity 在纵向模式下会堆叠显示(请参阅下方的“宽高比”),而在横向模式下则会并排显示:

在大屏平板电脑上,纵向模式下列表和详情 activity 堆叠显示。 在大屏平板电脑上,横向模式下列表和详情 activity 并排显示。

摘要在横向模式下会全屏显示,即使它是在分屏模式下启动也不例外:

大屏平板电脑上的摘要 activity 叠加横向模式。

宽高比

除了分屏最小宽度之外,activity 分屏还受显示屏宽高比限制。splitMaxAspectRatioInPortraitsplitMaxAspectRatioInLandscape 属性用于指定 activity 分屏显示的最大屏幕宽高比(高度:宽度)。这些属性代表 SplitRulemaxAspectRatioInPortraitmaxAspectRatioInLandscape 属性。

如果显示屏的宽高比大于任一方向的值,则无论屏幕的宽度如何,都会停用分屏。纵向模式的默认值为 1.4(请参阅 SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT),这会阻止高窄显示屏包含分屏。默认情况下,始终允许在横向模式下分屏(请参阅 SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT)。

Pixel C 模拟器的纵向显示宽度为 900dp,大于示例应用 XML 配置文件中的 splitMinWidthDp 设置,因此模拟器应显示一个 activity 拆分。但是,Pixel C 的竖屏宽高比大于 1.4,这使得 activity 分屏无法在纵向模式下显示。

您可以在 XML 配置文件的 SplitPairRuleSplitPlaceholderRule 元素中设置纵向和横向显示屏的最大宽高比,例如:

main_split_config.xml

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:window="http://schemas.android.com/apk/res/android">

  <!-- Define a split for the named activity pair. -->
  <SplitPairRule
      . . .
      window:splitMaxAspectRatioInPortrait="alwaysAllow"
      window:splitMaxAspectRatioInLandscape="alwaysDisallow"
      . . .
 </SplitPairRule>

  <SplitPlaceholderRule
      . . .
      window:splitMaxAspectRatioInPortrait="alwaysAllow"
      window:splitMaxAspectRatioInLandscape="alwaysDisallow"
      . . .
  </SplitPlaceholderRule>

</resources>

在纵向显示宽度大于或等于 840dp 的大屏平板电脑或 Pixel C 模拟器上,activity 在纵向模式下并排显示,而在横向模式下则堆叠显示:

在大屏平板电脑上,纵向模式下列表和详情 activity 并排显示。 在大屏平板电脑上,横向模式下列表和详情 activity 堆叠显示。

额外课程内容

尝试按照上述针对横向和纵向模式的说明设置示例应用程序的宽高比。使用您的大屏平板电脑(如果纵向宽度不低于 840dp)或 Pixel C 模拟器测试相应设置。您应该会在纵向模式下看到 activity 分屏,但在横向模式下不会看到分屏。

确定大屏平板电脑的宽高比(Pixel C 的宽高比略高于 1.4)。将 splitMaxAspectRatioInPortrait 设为高于和低于该宽高比的值。运行应用,看看能等到什么结果。

6. WindowManager API

您可以通过在启动分屏的 activity 的 onCreate() 方法内调用一个方法来完全在代码中启用 activity 嵌入。如果您更喜欢使用代码而非 XML,那么这种方法很适合您。

添加 WindowManager 依赖项

无论您是要创建基于 XML 的实现,还是使用 API 调用,您的应用都需要访问 WindowManager 库。请参阅此 Codelab 的“XML 配置”部分,了解如何向您的应用添加 WindowManager 依赖项。

告知系统

无论您使用 XML 配置文件还是 WindowManager API 调用,应用都必须通知系统应用已实现 activity 嵌入。如需了解如何告知系统您已实现 activity 嵌入,请参阅此 Codelab 的“XML 配置”部分。

创建用于管理分屏的类

在此 Codelab 的这一部分中,您将完全在单个静态或伴生对象方法(将从示例应用的主要 activity ListActivity 中调用)内实现 activity 分屏。

创建一个名为 SplitManager 的类,其中包含一个名为 createSplit 的方法,该方法包含一个 context 参数(某些 API 调用需要该参数):

SplitManager.kt

class SplitManager {

    companion object {

        fun createSplit(context: Context) {
        }
}

SplitManager.java

class SplitManager {

    static void createSplit(Context context) {
    }
}

Application 类的子类的 onCreate() 方法中调用该方法。

如需详细了解为何以及如何创建 Application 的子类,请参阅此 Codelab 的“XML 配置”部分中的“初始化快捷方式”。

SampleApplication.kt

package com.example.activity_embedding

import android.app.Application

/**
 * Initializer for activity embedding split rules.
 */
class SampleApplication : Application() {

  override fun onCreate() {
    super.onCreate()
    SplitManager.createSplit(this)
  }
}

SampleApplication.java

package com.example.activity_embedding;

import android.app.Application;

/**
 * Initializer for activity embedding split rules.
 */
public class SampleApplication extends Application {

  @Override
  public void onCreate() {
    super.onCreate();
    SplitManager.createSplit(this);
  }
}

创建分屏规则

必需的 API:

SplitPairRule 定义了一组 activity 的分屏规则。

SplitPairRule.Builder 会创建一个 SplitPairRule。该构建器将一组 SplitPairFilter 对象作为参数,过滤条件会指定何时应用规则。

您可以使用 RuleController 组件的单例实例注册该规则,这样系统便可使用分屏规则。

如需创建分屏规则,请执行以下操作:

  1. 创建一个分屏对过滤器,将 ListActivityDetailActivity 标识为共享分屏的 activity:

SplitManager.kt / createSplit()

val splitPairFilter = SplitPairFilter(
    ComponentName(context, ListActivity::class.java),
    ComponentName(context, DetailActivity::class.java),
    null
)

SplitManager.java / createSplit()

SplitPairFilter splitPairFilter = new SplitPairFilter(
    new ComponentName(context, ListActivity.class),
    new ComponentName(context, DetailActivity.class),
    null
);

该过滤器可以包含用于启动辅助 activity 的 intent 操作(第三个参数)。如果您添加了 intent 操作,过滤器会检查该操作以及 activity 名称。对于您自己应用中的 activity,您可能不需要过滤 intent 操作,因此该参数可以为 null。

  1. 将过滤条件添加到过滤条件集:

SplitManager.kt / createSplit()

val filterSet = setOf(splitPairFilter)

SplitManager.java / createSplit()

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

SplitManager.kt / createSplit()

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

SplitManager.java / createSplit()

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

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

  • setSplitType:定义将可用显示区域分配给每个 activity 容器的方式。宽高比分屏类型指定主要容器占据的显示区域比例;辅助容器则会占据剩余的显示区域。
  • setLayoutDirection:指定 activity 容器相对于另一种容器的布局方式,主要容器优先。
  1. 构建分屏对规则:

SplitManager.kt / createSplit()

val splitPairRule = SplitPairRule.Builder(filterSet)
      .setDefaultSplitAttributes(splitAttributes)
      .setMinWidthDp(840)
      .setMinSmallestWidthDp(600)
      .setFinishPrimaryWithSecondary(SplitRule.FinishBehavior.NEVER)
      .setFinishSecondaryWithPrimary(SplitRule.FinishBehavior.ALWAYS)
      .setClearTop(false)
      .build()

SplitManager.java / createSplit()

SplitPairRule splitPairRule = new SplitPairRule.Builder(filterSet)
  .setDefaultSplitAttributes(splitAttributes)
  .setMinWidthDp(840)
  .setMinSmallestWidthDp(600)
  .setFinishPrimaryWithSecondary(SplitRule.FinishBehavior.NEVER)
  .setFinishSecondaryWithPrimary(SplitRule.FinishBehavior.ALWAYS)
  .setClearTop(false)
  .build();

SplitPairRule.Builder 创建并配置规则:

  • filterSet:包含 activity 过滤条件,通过确定共享分屏的 activity 以确定何时应用规则。在示例应用中,ListActivityDetailActivity 在分屏对过滤器中指定(请参阅上述步骤)。
  • setDefaultSplitAttributes:将布局属性应用于规则。
  • setMinWidthDp:设置允许分屏的最小显示宽度(以密度无关像素 dp 为单位)。
  • setMinSmallestWidthDp:设置允许分屏的最小值(以 dp 为单位),无论设备显示方向如何,都必须确保两个显示屏尺寸中较小的尺寸不低于该值才允许分屏。
  • setFinishPrimaryWithSecondary:设置结束辅助容器中的所有 activity 会对主要容器中的 activity 有何影响。NEVER 表示在辅助容器中的所有 activity 均结束时,系统不应结束主要 activity。(请参阅结束 activity。)
  • setFinishSecondaryWithPrimary:设置结束主要容器中的所有 activity 会对辅助容器中的 activity 有何影响。ALWAYS 表示当主要容器中的所有 activity 均结束时,系统应始终结束辅助容器中的 activity。(请参阅结束 activity。)
  • setClearTop:指定在辅助容器中启动新 activity 时,该容器中的所有 activity 是否都已结束。false 表示新 activity 会堆叠在辅助容器中已有的 activity 之上。
  1. 获取 WindowManager RuleController 的单例实例并添加规则:

SplitManager.kt / createSplit()

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

SplitManager.java / createSplit()

RuleController ruleController = RuleController.getInstance(context);
ruleController.addRule(splitPairRule);

创建占位符规则

必需的 API:

SplitPlaceholderRule 定义了一个规则,用于在没有内容可用于辅助容器时占据该容器的 activity。如需创建占位符 activity,请参阅本 Codelab 的“XML 配置”部分中的“创建占位符 activity”。(如需了解详情,请参阅 activity 嵌入开发者指南中的占位符部分)。

SplitPlaceholderRule.Builder 会创建一个 SplitPlaceholderRule。该构建器将一组 ActivityFilter 对象作为参数,这些对象会指定与占位符规则相关联的 activity。如果过滤器与启动的 activity 匹配,系统会应用占位符规则。

您可以使用 RuleController 组件注册规则。

如需创建分屏占位符规则,请执行以下操作:

  1. 创建 ActivityFilter

SplitManager.kt / createSplit()

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

SplitManager.java / createSplit()

ActivityFilter placeholderActivityFilter = new ActivityFilter(
    new ComponentName(context, ListActivity.class),
    null
);

过滤器将规则与示例应用的主要 activity ListActivity 相关联。因此,如果列表-详情布局中没有详情内容,则占位符会填充详情区域。

过滤器可以包含与关联 activity 启动(ListActivity 启动)相关的 intent 操作(第二个参数)。如果您添加了 intent 操作,过滤器会检查该操作以及 activity 名称。对于您自己应用中的 activity,您可能不需要过滤 intent 操作,因此该参数可以为 null。

  1. 将过滤条件添加到过滤条件集:

SplitManager.kt / createSplit()

val placeholderActivityFilterSet = setOf(placeholderActivityFilter)

SplitManager.java / createSplit()

Set<ActivityFilter> placeholderActivityFilterSet = new HashSet<>();
placeholderActivityFilterSet.add(placeholderActivityFilter);
  1. 创建 SplitPlaceholderRule

SplitManager.kt / createSplit()

val splitPlaceholderRule = SplitPlaceholderRule.Builder(
      placeholderActivityFilterSet,
      Intent(context, PlaceholderActivity::class.java)
    ).setDefaultSplitAttributes(splitAttributes)
     .setMinWidthDp(840)
     .setMinSmallestWidthDp(600)
     .setFinishPrimaryWithPlaceholder(SplitRule.FinishBehavior.ALWAYS)
     .build()

SplitManager.java / createSplit()

SplitPlaceholderRule splitPlaceholderRule = new SplitPlaceholderRule.Builder(
  placeholderActivityFilterSet,
  new Intent(context, PlaceholderActivity.class)
).setDefaultSplitAttributes(splitAttributes)
 .setMinWidthDp(840)
 .setMinSmallestWidthDp(600)
 .setFinishPrimaryWithPlaceholder(SplitRule.FinishBehavior.ALWAYS)
 .build();

SplitPlaceholderRule.Builder 创建并配置规则:

  • placeholderActivityFilterSet:包含 activity 过滤条件,通过确定与占位符 activity 相关联的 activity 来确定何时应用规则。
  • Intent:指定占位符 activity 的启动状态。
  • setDefaultSplitAttributes:将布局属性应用于规则。
  • setMinWidthDp:设置允许分屏的最小显示宽度(以密度无关像素 dp 为单位)。
  • setMinSmallestWidthDp:设置允许分屏的最小值(以 dp 为单位),无论设备显示方向如何,都必须确保两个显示屏尺寸中较小的尺寸不低于该值才允许分屏。
  • setFinishPrimaryWithPlaceholder:设置结束占位符 activity 会对主要容器中的 activity 有何影响。ALWAYS 表示当占位符 activity 结束时,系统应始终结束主要容器中的 activity。(请参阅结束 activity。)
  1. 向 WindowManager RuleController 添加规则:

SplitManager.kt / createSplit()

ruleController.addRule(splitPlaceholderRule)

SplitManager.java / createSplit()

ruleController.addRule(splitPlaceholderRule);

创建 activity 规则

必需的 API:

ActivityRule 可用于为占据整个任务窗口(例如模态对话框)的 activity 定义规则。(如需了解详情,请参阅 activity 嵌入开发者指南中的全窗口模态部分)。

SplitPlaceholderRule.Builder 会创建一个 SplitPlaceholderRule。该构建器将一组 ActivityFilter 对象作为参数,这些对象会指定与占位符规则相关联的 activity。如果过滤器与启动的 activity 匹配,系统会应用占位符规则。

您可以使用 RuleController 组件注册规则。

如需创建 activity 规则,请执行以下操作:

  1. 创建 ActivityFilter

SplitManager.kt / createSplit()

val summaryActivityFilter = ActivityFilter(
    ComponentName(context, SummaryActivity::class.java),
    null
)

SplitManager.java / createSplit()

ActivityFilter summaryActivityFilter = new ActivityFilter(
    new ComponentName(context, SummaryActivity.class),
    null
);

过滤条件可指定规则适用的 activity,即 SummaryActivity

过滤器可以包含与关联 activity 启动(SummaryActivity 启动)相关的 intent 操作(第二个参数)。如果您添加了 intent 操作,过滤器会检查该操作以及 activity 名称。对于您自己应用中的 activity,您可能不需要过滤 intent 操作,因此该参数可以为 null。

  1. 将过滤条件添加到过滤条件集:

SplitManager.kt / createSplit()

val summaryActivityFilterSet = setOf(summaryActivityFilter)

SplitManager.java / createSplit()

Set<ActivityFilter> summaryActivityFilterSet = new HashSet<>();
summaryActivityFilterSet.add(summaryActivityFilter);
  1. 创建 ActivityRule

SplitManager.kt / createSplit()

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

SplitManager.java / createSplit()

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

ActivityRule.Builder 创建并配置规则:

  • summaryActivityFilterSet:包含 activity 过滤条件,通过确定您希望从分屏中排除的 activity,以确定何时应用规则。
  • setAlwaysExpand:指定 activity 是否应展开以填充所有可用的显示空间。
  1. 向 WindowManager RuleController 添加规则:

SplitManager.kt / createSplit()

ruleController.addRule(activityRule)

SplitManager.java / createSplit()

ruleController.addRule(activityRule);

开始运行!

构建和运行示例应用。

应用的行为应与使用 XML 配置文件进行自定义时相同。

请参阅本 Codelab 的“XML 配置”部分中的“开始运行!”。

额外课程内容

尝试使用 SplitPairRule.BuilderSplitPlaceholderRule.BuildersetMaxAspectRatioInPortraitsetMaxAspectRatioInLandscape 方法,在示例应用中设置宽高比。您可以使用 EmbeddingAspectRatio 类的属性和方法指定值,例如:

SplitPairRule.Builder(filterSet)
  . . .
  .setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ratio(1.5f))
  . . .
.build()

使用您的大屏平板电脑或 Pixel C 模拟器测试相应设置。

确定大屏平板电脑的宽高比(Pixel C 的宽高比略高于 1.4)。将纵向模式的最大宽高比设置为高于或低于平板电脑或 Pixel C 宽高比的值。尝试使用 ALWAYS_ALLOWALWAYS_DISALLOW 属性。

运行应用,看看能等到什么结果。

如需了解详情,请参阅本 Codelab 的“XML 配置”部分中的“宽高比”。

7. Material Design 导航

Material Design 准则会为不同屏幕尺寸指定不同的导航组件 - 为宽度不小于 840dp 的屏幕指定侧边导航栏,为小于 840dp 的屏幕指定底部导航栏。

fb47462060f4818d.gif

使用 activity 嵌入时,您无法使用 WindowManager 方法 getCurrentWindowMetrics()getMaximumWindowMetrics() 来确定屏幕宽度,因为这些方法返回的窗口指标描述了包含调用方法的嵌入 activity 的显示窗格。

如需获取 activity 嵌入应用的准确尺寸,请使用分屏属性计算器SplitAttributesCalculatorParams

如果您在上一部分中添加了以下几行内容,请删除它们。

main_split_config.xml

<SplitPairRule
    . . .
    window:splitMaxAspectRatioInPortrait="alwaysAllow" // Delete this line.
    window:splitMaxAspectRatioInLandscape="alwaysDisallow" // Delete this line.
    . . .>
</SplitPairRule>

<SplitPlaceholderRule
    . . .

    window:splitMaxAspectRatioInPortrait="alwaysAllow" // Delete this line.
    window:splitMaxAspectRatioInLandscape="alwaysDisallow" // Delete this line.
    . . .>
<SplitPlaceholderRule/>

灵活导航

如需根据屏幕尺寸动态切换导航组件,请使用 SplitAttributes 计算器。计算器会检测设备屏幕方向和窗口大小的变化,并相应地重新计算显示屏尺寸。我们会将计算器与 SplitController 集成,触发导航组件更改以响应屏幕尺寸更新。

创建导航布局

首先,创建一个用于填充侧边导航栏和导航栏的菜单。

res/menu 文件夹中,创建一个名为 nav_menu.xml 的新菜单资源文件。将菜单文件的内容替换为以下代码:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/navigation_home"
        android:title="Home" />
    <item
        android:id="@+id/navigation_dashboard"
        android:title="Dashboard" />
    <item
        android:id="@+id/navigation_settings"
        android:title="Settings" />
</menu>

接下来,为布局添加导航栏和侧边导航栏。将其可见性设为 gone,以便它们最初处于隐藏状态。稍后,我们将根据布局尺寸显示它们。

activity_list.xml

<com.google.android.material.navigationrail.NavigationRailView
     android:id="@+id/navigationRailView"
     android:layout_width="wrap_content"
     android:layout_height="match_parent"
     app:layout_constraintStart_toStartOf="parent"
     app:layout_constraintTop_toTopOf="parent"
     app:menu="@menu/nav_menu"
     android:visibility="gone" />

<com.google.android.material.bottomnavigation.BottomNavigationView
   android:id="@+id/bottomNavigationView"
   android:layout_width="0dp"
   android:layout_height="wrap_content"
   app:menu="@menu/nav_menu"
   app:layout_constraintBottom_toBottomOf="parent"
   app:layout_constraintStart_toStartOf="parent"
   app:layout_constraintEnd_toEndOf="parent"
   android:visibility="gone" />

编写一个函数来处理导航栏和侧边导航栏之间的切换。

ListActivity.kt / setWiderScreenNavigation()

private fun setWiderScreenNavigation(useNavRail: Boolean) {
   val navRail = findViewById(R.id.navigationRailView)
   val bottomNav = findViewById(R.id.bottomNavigationView)
   if (useNavRail) {
       navRail.visibility = View.VISIBLE
       bottomNav.visibility = View.GONE
   } else {
       navRail.visibility = View.GONE
       bottomNav.visibility = View.VISIBLE
   }
}

ListActivity.java / setWiderScreenNavigation()

private void setWiderScreenNavigation(boolean useNavRail) {
   NavigationRailView navRail = findViewById(R.id.navigationRailView);
   BottomNavigationView bottomNav = findViewById(R.id.bottomNavigationView);
   if (useNavRail) {
       navRail.setVisibility(View.VISIBLE);
       bottomNav.setVisibility(View.GONE);
   } else {
       navRail.setVisibility(View.GONE);
       bottomNav.setVisibility(View.VISIBLE);
   }
}

分屏属性计算器

SplitController 会获取有关当前活跃 activity 分屏的信息,并提供互动点来自定义分屏和创建新的分屏。

在前面的部分中,我们设置了分屏的默认属性,方法是在 XML 文件的 <SplitPairRule><SplitPlaceHolderRule> 标记中指定 splitRatio 和其他属性,或者使用 SplitPairRule.Builder#setDefaultSplitAttributes()SplitPlaceholderRule.Builder#setDefaultSplitAttributes() API。

如果父级容器的 WindowMetrics 满足 SplitRule 尺寸要求(即 minWidthDpminHeightDpminSmallestWidthDp),则系统会应用默认的分屏属性。

我们将设置一个分屏属性计算器,以替换默认的分屏属性。当窗口或设备状态发生变化(例如屏幕方向发生变化或折叠状态发生变化)后,计算器会更新现有的分屏对。

这样一来,开发者便可以了解设备或窗口状态,并在不同场景(包括竖屏和横屏以及桌面折叠状态)中设置不同的分屏属性。

创建分屏属性计算器时,平台会将 SplitAttributesCalculatorParams 对象传递给 setSplitAttributesCalculator() 函数。parentWindowMetrics 属性提供应用窗口指标。

在以下代码中,activity 会检查是否满足默认约束条件,即宽度 > 840dp 且最小宽度 > 600dp。当满足约束条件时,activity 会嵌入双窗格布局,并且应用会使用侧边导航栏而不是底部导航栏。否则,activity 会全屏显示,并带有底部导航栏。

ListActivity.kt / setSplitAttributesCalculator()

SplitController.getInstance(this).setSplitAttributesCalculator
       params ->

   if (params.areDefaultConstraintsSatisfied) {
       // When default constraints are satisfied, use the navigation rail.
       setWiderScreenNavigation(true)
       return@setSplitAttributesCalculator params.defaultSplitAttributes
   } else {
       // Use the bottom navigation bar in other cases.
       setWiderScreenNavigation(false)
       // Expand containers if the device is in portrait or the width is less than 840 dp.
       SplitAttributes.Builder()
           .setSplitType(SPLIT_TYPE_EXPAND)
           .build()
   }
}

ListActivity.java / setSplitAttributesCalculator()

SplitController.getInstance(this).setSplitAttributesCalculator(params -> {
   if (params.areDefaultConstraintsSatisfied()) {
       // When default constraints are satisfied, use the navigation rail.
       setWiderScreenNavigation(true);
       return params.getDefaultSplitAttributes();
   } else {
       // Use the bottom navigation bar in other cases.
       setWiderScreenNavigation(false);
       // Expand containers if the device is in portrait or the width is less than 600 dp.
       return new SplitAttributes.Builder()
               .setSplitType(SplitType.SPLIT_TYPE_EXPAND)
               .build();
   }
});

太棒了,您的 activity 嵌入应用现在已遵循 Material Design 导航准则!

8. 恭喜!

祝贺您!您已将基于 activity 的应用优化为大屏幕上的列表-详情布局,并添加了 Material Design 导航。

您学习了两种实现 activity 嵌入的方法:

  • 使用 XML 配置文件
  • 执行 Jetpack API 调用
  • 使用 activity 嵌入实现灵活导航

您不用重写应用的任何 Kotlin 或 Java 源代码。

万事俱备,您可以使用 activity 嵌入功能针对大屏设备优化您的正式版应用了!

9. 了解更多内容