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

1. 简介

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

更新应用以充分利用大屏幕可能既耗时又成本高昂,特别是对于基于多个 Activity 的旧版应用。

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

前提条件

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

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

您将构建的内容

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

Activities A, B, and C stacked in the task window.

在大屏幕上,应用会根据您的设置同时在屏幕上显示两个 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 项目。

5169252cbf5e110e.png

仓库和 zip 文件中的 activity-embedding 文件夹包含两个 Android Studio 项目:一个用 Kotlin 编写,一个用 Java 编写。打开您选择的项目。Codelab 代码段以两种语言提供。

创建虚拟设备

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

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

3. 运行应用

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

该应用包含三个 Activity

  • ListActivity — 在 RecyclerView 中包含项目列表 ItemAdapter 类为 RecyclerView 创建项目列表。
  • DetailActivity — 当从列表中选择某个项目时,显示有关该列表项目的信息。
  • SummaryActivity — 当选择 Summary 列表项目时,显示信息摘要。

未嵌入 Activity 时的行为

运行示例应用,查看未嵌入 Activity 时的行为

  1. 在您的大平板电脑或 Pixel C 模拟器上运行示例应用。主(列表)Activity 会出现

Large tablet with sample app running in portrait orientation. List activity full screen.

  1. 选择一个列表项目以启动辅助(详情)Activity。详情 Activity 会覆盖列表 Activity

Large tablet with sample app running in portrait orientation. Detail activity full screen.

  1. 将平板电脑旋转到横向。辅助 Activity 仍会覆盖主 Activity 并占据整个显示屏

Large tablet with sample app running in landscape orientation. Detail activity full screen.

  1. 选择返回控件(应用栏中的向左箭头)以返回列表。
  2. 将设备旋转到纵向。
  3. 选择列表中的最后一项 Summary,以作为辅助 Activity 启动 Summary Activity。Summary Activity 会覆盖列表 Activity

Large tablet with sample app running in portrait orientation. Summary activity full screen.

  1. 将平板电脑旋转到横向。辅助 Activity 仍会覆盖主 Activity 并占据整个显示屏

Large tablet with sample app running in landscape orientation. Summary activity full screen.

嵌入 Activity 后的行为

完成此 Codelab 后,横向时将以列表-详情布局并排显示列表 Activity 和详情 Activity

Large tablet with sample app running in landscape orientation. List and detail activities in list-detail layout.

但是,您将配置摘要以全屏显示,即使该 Activity 是从分屏中启动的。摘要会覆盖分屏

Large tablet with sample app running in landscape orientation. Summary activity full screen.

4. 背景

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

主 Activity 可以在辅助容器中启动其他 Activity。然后,两个容器中的 Activity 都可以在各自的容器中启动 Activity。每个容器都可以包含一个 Activity 堆栈。有关更多信息,请参阅Activity 嵌入开发者指南。

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

5. XML 配置

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

添加 WindowManager 依赖项

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

build.gradle

 implementation 'androidx.window:window:1.3.0'

通知系统

让系统知道您的应用已实现 Activity 嵌入。

android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED 属性添加到应用清单文件的 <application> 元素中,并将其值设置为 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 embedding with portrait-only app on landscape display. Letterboxed, portrait-only activity A launches embedded 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。主 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 Views Activity
  3. 将 Activity 命名为 PlaceholderActivity
  4. 将源语言设置为 Kotlin 或 Java
  5. 选择 Finish

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

  1. 如果您的文件是 Kotlin 类,请移除对 enableEdgeToEdge() 的调用;如果您的文件是 Java 类,请移除对 EdgeToEdge.enable(this); 的调用。(应用中的其他 Activity,尤其是 DetailActivity,都不是全屏的。)

您完成的类文件应如下所示

PlaceholderActivity.kt

package com.example.activity_embedding

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat

class PlaceholderActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_placeholder)
    ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) {
      v, insets ->
        val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
        v.setPadding(systemBars.left, 
                     systemBars.top,
                     systemBars.right, 
                     systemBars.bottom)
        insets
    }
  }
}

PlaceholderActivity.java

package com.example.activity_embedding;

import android.os.Bundle;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

public class PlaceholderActivity extends AppCompatActivity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_placeholder);
    ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), 
      (v, insets) -> {
        Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
        v.setPadding(systemBars.left, 
                     systemBars.top, 
                     systemBars.right, 
                     systemBars.bottom);
      return insets;
    });
  }
}
  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:id="@+id/main"
    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 库的 Initializer 使 RuleController 能够访问配置文件。

Startup 库在应用启动时执行组件初始化。初始化必须在任何 Activity 启动之前发生,以便 RuleController 能够访问分屏规则并在必要时应用它们。

添加 Startup 库依赖项

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

build.gradle

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

实现 RuleController 的初始化程序

创建 Startup Initializer 接口的实现。

在 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">
    <!-- 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">

  <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">
    <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">
      <!-- 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 的子类

当为您的应用创建进程时,您的 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

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

要将规则添加到 RuleController 中,请执行以下操作

  1. 获取 RuleController 的单例实例
  2. 使用 RuleController 的 Java 静态方法或 Kotlin companion object 的 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 始终堆叠显示——即使在横向模式下也是如此

Detail (secondary) activity stacked on top of list (main) activity on phone in portrait orientation. Detail (secondary) activity stacked on top of list (main) activity on phone in landscape orientation.

在 Android 13(API 级别 33)及更低版本上,无论分屏最小宽度规范如何,非折叠屏手机上均未启用 Activity 嵌入。

在更高的 API 级别上,非折叠屏手机对 Activity 嵌入的支持取决于设备制造商是否已启用 Activity 嵌入。

在小平板电脑或 7 WSVGA (Tablet) 模拟器上,两个 Activity 在纵向模式下堆叠显示,但在横向模式下并排显示

List and detail activities stacked in portrait orientation on small tablet. List and detail activities side by side in landscape orientation on small tablet.

在大平板电脑或 Pixel C 模拟器上,Activity 在纵向模式下堆叠显示(参见下面的“纵横比”),但在横向模式下并排显示

List and detail activities stacked in portrait orientation on large tablet. List and detail activities side by side in landscape on large tablet.

摘要在横向模式下全屏显示,即使它是从分屏中启动的

Summary activity overlaying split in landscape on large tablet.

纵横比

除了分屏最小宽度外,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 在纵向模式下并排显示,但在横向模式下堆叠显示

List and detail activities side by side in portrait orientation on large tablet. List and detail activities stacked in landscape orientation on large tablet.

加分项

尝试按照上面的示例应用设置纵向和横向模式的纵横比。使用您的大平板电脑(如果纵向宽度为 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 嵌入。请参阅本 Codelab 的“XML 配置”部分,了解如何将您的实现告知系统。

创建管理分屏的类

在此 Codelab 部分中,您将在一个静态方法或 companion object 方法中完全实现 Activity 分屏,您将从示例应用的主 Activity ListActivity 中调用该方法。

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

SplitManager.kt

import android.content.Context

class SplitManager {

    companion object {

        fun createSplit(context: Context) {
        }
    }
}

SplitManager.java

import android.content.Context

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 启动的意图动作(第三个参数)。如果您包含意图动作,过滤器将同时检查意图动作和 Activity 名称。对于您自己的应用中的 Activity,您可能不会根据意图动作进行过滤,因此该参数可以为 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 来确定何时应用规则。在示例应用中,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 启动)的意图动作(第二个参数)。如果您包含意图动作,过滤器将同时检查意图动作和 Activity 名称。对于您自己的应用中的 Activity,您可能不会根据意图动作进行过滤,因此该参数可以为 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。)
  1. 将规则添加到 WindowManager RuleController

SplitManager.kt / createSplit()

ruleController.addRule(splitPlaceholderRule)

SplitManager.java / createSplit()

ruleController.addRule(splitPlaceholderRule);

创建 Activity 规则

所需 API

ActivityRule 可用于为占据整个任务窗口的 Activity 定义规则,例如模态对话框。(有关更多信息,请参阅Activity 嵌入开发者指南中的全窗口模态

ActivityRule.Builder 创建一个 ActivityRule。构建器接受一组 ActivityFilter 对象作为参数。这些对象指定与 Activity 规则关联的 Activity。如果过滤器匹配已启动的 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 启动)的意图动作(第二个参数)。如果您包含意图动作,过滤器将同时检查意图动作和 Activity 名称。对于您自己的应用中的 Activity,您可能不会根据意图动作进行过滤,因此该参数可以为 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);

源文件

这是完成的分屏管理器文件

SplitManager.kt

package com.example.activity_embedding

import android.content.ComponentName
import android.content.Context
import android.content.Intent
import androidx.window.embedding.ActivityFilter
import androidx.window.embedding.ActivityRule
import androidx.window.embedding.EmbeddingAspectRatio
import androidx.window.embedding.RuleController
import androidx.window.embedding.SplitAttributes
import androidx.window.embedding.SplitPairFilter
import androidx.window.embedding.SplitPairRule
import androidx.window.embedding.SplitPlaceholderRule
import androidx.window.embedding.SplitRule

/**
 * Creates activity embedding splits.
 */
class SplitManager {

    companion object {

        fun createSplit(context: Context) {
            val splitPairFilter = SplitPairFilter(
                ComponentName(context, ListActivity::class.java),
                ComponentName(context, DetailActivity::class.java),
                null
            )
            val filterSet = setOf(splitPairFilter)
            val splitAttributes: SplitAttributes = SplitAttributes.Builder()
                  .setSplitType(SplitAttributes.SplitType.ratio(0.33f))
                  .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
                  .build()
            val splitPairRule = SplitPairRule.Builder(filterSet)
                  .setDefaultSplitAttributes(splitAttributes)
                  .setMinWidthDp(840)
                  .setMinSmallestWidthDp(600)
                  .setFinishPrimaryWithSecondary(SplitRule.FinishBehavior.NEVER)
                  .setFinishSecondaryWithPrimary(SplitRule.FinishBehavior.ALWAYS)
                  .setClearTop(false)
                  .build()
            val ruleController = RuleController.getInstance(context)
            ruleController.addRule(splitPairRule)
            val placeholderActivityFilter = ActivityFilter(
                ComponentName(context, ListActivity::class.java),
                null
            )
            val placeholderActivityFilterSet = setOf(placeholderActivityFilter)
            val splitPlaceholderRule = SplitPlaceholderRule.Builder(
                  placeholderActivityFilterSet,
                  Intent(context, PlaceholderActivity::class.java)
                ).setDefaultSplitAttributes(splitAttributes)
                 .setMinWidthDp(840)
                 .setMinSmallestWidthDp(600)
                 .setFinishPrimaryWithPlaceholder(SplitRule.FinishBehavior.ALWAYS)
                 .build()
            ruleController.addRule(splitPlaceholderRule)
            val summaryActivityFilter = ActivityFilter(
                ComponentName(context, SummaryActivity::class.java),
                null
            )
            val summaryActivityFilterSet = setOf(summaryActivityFilter)
            val activityRule = ActivityRule.Builder(summaryActivityFilterSet)
                  .setAlwaysExpand(true)
                  .build()
            ruleController.addRule(activityRule)
        }
    }

}

SplitManager.java

package com.example.activity_embedding;

import android.content.ComponentName;
import android.content.Context;import android.content.Intent;
import androidx.window.embedding.ActivityFilter;
import androidx.window.embedding.ActivityRule;
import androidx.window.embedding.RuleController;
import androidx.window.embedding.SplitAttributes;
import androidx.window.embedding.SplitPairFilter;
import androidx.window.embedding.SplitPairRule;
import androidx.window.embedding.SplitPlaceholderRule;
import androidx.window.embedding.SplitRule;
import java.util.*;

/**
 * Creates activity embedding splits.
 */
class SplitManager   {
    static void createSplit(Context context) {
        SplitPairFilter splitPairFilter = new SplitPairFilter(
            new ComponentName(context, ListActivity.class),
            new ComponentName(context, DetailActivity.class),
            null
        );
        Set<SplitPairFilter> filterSet = new HashSet<>();
        filterSet.add(splitPairFilter);
        SplitAttributes splitAttributes = new SplitAttributes.Builder()
          .setSplitType(SplitAttributes.SplitType.ratio(0.33f))
          .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
          .build();
        SplitPairRule splitPairRule = new SplitPairRule.Builder(filterSet)
          .setDefaultSplitAttributes(splitAttributes)
          .setMinWidthDp(840)
          .setMinSmallestWidthDp(600)
          .setFinishPrimaryWithSecondary(SplitRule.FinishBehavior.NEVER)
          .setFinishSecondaryWithPrimary(SplitRule.FinishBehavior.ALWAYS)
          .setClearTop(false)
          .build();
        RuleController ruleController = RuleController.getInstance(context);
        ruleController.addRule(splitPairRule);
        ActivityFilter placeholderActivityFilter = new ActivityFilter(
            new ComponentName(context, ListActivity.class),
            null
        );
        Set<ActivityFilter> placeholderActivityFilterSet = new HashSet<>();
        placeholderActivityFilterSet.add(placeholderActivityFilter);
        SplitPlaceholderRule splitPlaceholderRule = new SplitPlaceholderRule.Builder(
          placeholderActivityFilterSet,
          new Intent(context, PlaceholderActivity.class)
        ).setDefaultSplitAttributes(splitAttributes)
         .setMinWidthDp(840)
         .setMinSmallestWidthDp(600)
         .setFinishPrimaryWithPlaceholder(SplitRule.FinishBehavior.ALWAYS)
         .build();
        ruleController.addRule(splitPlaceholderRule);
        ActivityFilter summaryActivityFilter = new ActivityFilter(
            new ComponentName(context, SummaryActivity.class),
            null
        );
        Set<ActivityFilter> summaryActivityFilterSet = new HashSet<>();
        summaryActivityFilterSet.add(summaryActivityFilter);
        ActivityRule activityRule = new ActivityRule.Builder(
            summaryActivityFilterSet
        ).setAlwaysExpand(true)
         .build();
        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 集成,以响应屏幕尺寸更新触发导航组件更改。

创建导航布局

首先,创建一个菜单,我们将使用它来填充导航轨道和导航栏。

在 Android Studio 中,执行以下操作

  1. 右键单击(辅助按钮单击)示例应用资源文件夹 res
  2. 选择 New > Android Resource File
  3. 将类命名为 nav_menu
  4. Resource type 设置为 Menu
  5. 确保 Directory namemenu
  6. 单击 OK — Android Studio 会在 menu 文件夹中创建菜单文件
  7. 将菜单文件的内容替换为以下内容
<?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: NavigationRailView  = findViewById(R.id.navigationRailView)
   val bottomNav: BottomNavigationView = 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 / onCreate()

if (WindowSdkExtensions.getInstance().extensionVersion >= 2) {
    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 / onCreate()

if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 2) {
    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 840 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. 了解更多