1. 简介
大屏幕使您可以创建增强用户体验并提高用户生产率的应用程序布局和 UI。但是,如果您的应用程序是为不可折叠手机的小屏幕设计的,它可能无法利用平板电脑、可折叠设备和 ChromeOS 设备提供的额外显示空间。
更新应用程序以充分利用大屏幕可能非常耗时且成本高昂,特别是对于基于多个 Activity 的遗留应用程序。
Android 12L(API 级别 32)中引入的 Activity 嵌入功能使基于 Activity 的应用程序能够在大屏幕上同时显示多个 Activity,从而创建双窗格布局,例如列表-详情。无需重新编写 Kotlin 或 Java 代码。您只需添加一些依赖项,创建 XML 配置文件,实现初始化程序,并对应用程序清单进行一些补充。或者,如果您更喜欢在代码中工作,只需向应用程序主 Activity 的onCreate()
方法添加一些 Jetpack WindowManager API 调用即可。
先决条件
要完成此代码实验室,您需要具备以下方面的经验:
- 构建 Android 应用
- 使用Activity
- 编写 XML
- 在 Android Studio 中工作,包括虚拟设备设置
您将构建的内容
在此代码实验室中,您将更新基于 Activity 的应用程序以支持类似于SlidingPaneLayout
的动态双窗格布局。在小屏幕上,应用程序将 Activity 在任务窗口中叠加(堆叠)。
在大屏幕上,应用程序会同时在屏幕上显示两个 Activity,根据您的规格并排显示或上下显示。
您将学习的内容
如何通过两种方式实现 Activity 嵌入
- 使用 XML 配置文件
- 使用 Jetpack WindowManager API 调用
您需要的内容
- 最新版本的 Android Studio
- Android 手机或模拟器
- Android 小型平板电脑或模拟器
- Android 大型平板电脑或模拟器
2. 设置
获取示例应用程序
步骤 1:克隆代码库
克隆大屏幕代码实验室 Git 代码库
git clone https://github.com/android/large-screen-codelabs
或下载并解压缩大屏幕代码实验室 zip 文件
步骤 2:检查代码实验室源文件
导航到activity-embedding
文件夹。
步骤 3:打开代码实验室项目
在 Android Studio 中,打开 Kotlin 或 Java 项目
代码库和 zip 文件中的activity-embedding
文件夹包含两个 Android Studio 项目:一个是用 Kotlin 编写的,另一个是用 Java 编写的。打开您选择的项目。代码实验室代码片段以两种语言提供。
创建虚拟设备
如果您没有 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 嵌入时的行为
- 在您的大型平板电脑或 Pixel C 模拟器上运行示例应用程序。主(列表)Activity 将出现
- 选择列表项目以启动辅助(详情)Activity。详情 Activity 将叠加在列表 Activity 上
- 将平板电脑旋转到横向模式。辅助 Activity 仍然叠加在主 Activity 上并占据整个显示屏
- 选择后退控件(应用程序栏中的左向箭头)以返回列表。
- 选择列表中的最后一个项目“摘要”以启动摘要 Activity 作为辅助 Activity。摘要 Activity 将叠加在列表 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 配置
Activity 嵌入容器和拆分由基于您在 XML 配置文件中创建的拆分规则的 Jetpack WindowManager 库创建和管理。
添加 WindowManager 依赖项
通过将库依赖项添加到应用程序的模块级build.gradle
文件(例如)来启用示例应用程序访问 WindowManager 库
build.gradle
implementation 'androidx.window:window:1.2.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 嵌入双窗格布局
创建配置文件
在应用程序的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
— 指定任务窗口中主要活动占据的空间比例 (33%),剩余空间留给占位符。通常,此值应与占位符关联的分割对规则的分割比例匹配。splitMinWidthDp
— 指定占位符与主要活动一起显示在屏幕上所需的最小显示宽度 (840)。通常,此值应与占位符关联的分割对规则的最小宽度匹配。单位为与显示无关的像素 (dp)。finishPrimaryWithPlaceholder
— 指定当占位符结束时,主要分割容器中的活动是否也结束 (始终结束)。stickyPlaceholder
— 指示当显示大小调整为单窗格显示(例如,可折叠设备折叠时)时,占位符是否应保留在屏幕上 (false) 作为顶部活动。- 包含一个活动过滤器,该过滤器指定与占位符共享任务窗口分割的活动 (
ListActivity
)。
占位符代表分割对规则的次要活动,其主要活动与占位符活动过滤器中的活动相同(请参阅本代码实验室“XML 配置”部分中的“分割对规则”)。
活动规则
活动规则是通用规则。可以使用活动规则指定希望占据整个任务窗口的活动,即永远不会成为分割的一部分的活动。(更多信息,请参阅活动嵌入 开发者指南中的全窗口模态。)
我们将使摘要活动填充整个任务窗口,覆盖分割。返回导航将返回到分割。
将以下活动规则添加到配置文件
main_split_config.xml
<!-- Activities that should never be in a split. -->
<ActivityRule
window:alwaysExpand="true">
<ActivityFilter
window:activityName=".SummaryActivity"/>
</ActivityRule>
此规则执行以下操作:
- 标识应全窗口显示的活动 (
SummaryActivity)。
- 配置活动的选项
alwaysExpand
— 指定活动是否应扩展以填充所有可用的显示空间。
源文件
完成的 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>
创建占位符活动
您需要创建一个新的活动来充当 XML 配置文件中指定的占位符。此活动可以非常简单——只需指示用户内容最终将显示在此处即可。
在示例应用程序的主源文件夹中创建活动。
在 Android Studio 中,执行以下操作
- 右键单击 (辅助按钮单击) 示例应用程序源文件夹
com.example.activity_embedding
- 选择新建 > 活动 > 空视图活动
- 将活动命名为 *PlaceholderActivity*
- 选择完成
Android Studio 会在示例应用程序包中创建活动,将活动添加到应用程序清单文件,并在 res/layout
文件夹中创建一个名为 activity_placeholder.xml
的布局资源文件。
- 在示例应用程序的
AndroidManifest.xml
文件中,将占位符活动的标签设置为空字符串
AndroidManifest.xml
<activity
android:name=".PlaceholderActivity"
android:exported="false"
android:label="" />
- 将
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>
- 最后,将以下字符串资源添加到
res/values
文件夹中strings.xml
资源文件
strings.xml
<string name="placeholder_text">Placeholder</string>
创建初始化程序
WindowManager RuleController
组件解析 XML 配置文件中定义的规则,并将这些规则提供给系统。
Jetpack 启动 库 Initializer 使 RuleController
能够访问配置文件。
启动库在应用程序启动时执行组件初始化。初始化必须在任何活动启动之前发生,以便 RuleController
可以访问分割规则并在必要时应用它们。
添加启动库依赖项
要启用启动功能,请将启动库依赖项添加到示例应用程序的模块级 build.gradle
文件,例如
build.gradle
implementation 'androidx.startup:startup-runtime:1.1.1'
为 RuleController 实现初始化程序
创建启动 Initializer 接口的实现。
在 Android Studio 中,执行以下操作
- 右键单击 (辅助按钮单击) 示例应用程序源文件夹
com.example.activity_embedding
- 选择新建 > Kotlin 类/文件或新建 > Java 类
- 将类命名为 *SplitInitializer*
- 按Enter — Android Studio 会在示例应用程序包中创建该类。
- 将类文件的内容替换为以下内容
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();
}
}
初始化程序通过将包含定义的 XML 资源文件的 ID (main_split_config
) 传递给组件的 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
,后者又调用解析 XML 配置文件 (main_split_config.xml
) 并将规则添加到 RuleController
的 RuleController
方法(请参见上文的“为 RuleController 实现初始化程序”)。
InitializationProvider
在应用程序的 onCreate()
方法执行之前发现并初始化 SplitInitializer
;因此,当主应用程序活动启动时,分割规则生效。
源文件
这是完整的应用程序清单
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 混合使用,则可以消除启动库初始化程序和清单提供程序,从而实现更简单的实现。
创建 XML 配置文件后,请执行以下操作
步骤 1:创建 Application
的子类
您的应用程序子类将是在创建应用程序的进程时实例化的第一个类。您将在子类的 onCreate()
方法中将分割规则添加到 RuleController
,以确保在任何活动启动之前规则生效。
在 Android Studio 中,执行以下操作
- 右键单击 (辅助按钮单击) 示例应用程序源文件夹
com.example.activity_embedding
- 选择新建 > Kotlin 类/文件或新建 > Java 类
- 将类命名为 *SampleApplication*
- 按Enter — Android Studio 会在示例应用程序包中创建该类
- 从
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
将 XML 配置文件中的分割规则添加到应用程序子类的 onCreate()
方法中的 RuleController
。
要将规则添加到 RuleController
,请执行以下操作
- 获取
RuleController
的单例实例 - 使用
RuleController
的 Java 静态方法或 Kotlin 伴生对象parseRules()
方法来解析 XML 文件 - 使用
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"
. . .
运行它!
构建并运行示例应用程序。
在不可折叠的手机上,活动始终堆叠在一起,即使在横向模式下也是如此
在 Android 13(API 级别 33)及更低版本上,无论分割最小宽度规格如何,不可折叠手机上都不启用活动嵌入。
在更高 API 级别上对不可折叠手机的活动嵌入的支持取决于设备制造商是否启用了活动嵌入。
在小型平板电脑或 7 WSVGA(平板电脑)模拟器上,两个活动在纵向模式下堆叠在一起,但在横向模式下并排显示
在大平板电脑或 Pixel C 模拟器上,活动在纵向模式下堆叠在一起(请参见下面的“纵横比”),但在横向模式下并排显示
即使摘要是从分割中启动的,它也会在横向模式下全屏显示
纵横比
除了分割最小宽度外,活动分割还受显示纵横比的控制。splitMaxAspectRatioInPortrait
和 splitMaxAspectRatioInLandscape
属性指定显示活动分割的最高显示纵横比(高度:宽度)。这些属性代表 maxAspectRatioInPortrait
和 maxAspectRatioInLandscape
属性 SplitRule
。
如果显示器的纵横比超过任一方向的值,则无论显示器的宽度如何,都会禁用分割。纵向模式的默认值为 1.4(请参见 SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT
),这可以防止高而窄的显示器包含分割。默认情况下,横向模式始终允许分割(请参见SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT
)。
Pixel C 模拟器的纵向显示宽度为 900dp,这比示例应用程序 XML 配置文件中的 splitMinWidthDp
设置宽,因此模拟器应显示活动分割。但 Pixel C 在纵向模式下的纵横比大于 1.4,这会阻止活动分割在纵向模式下显示。
您可以在 XML 配置文件中的 SplitPairRule
和 SplitPlaceholderRule
元素中设置纵向和横向显示的最大纵横比,例如
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 模拟器上,活动在纵向方向上并排显示,但在横向方向上堆叠显示
额外奖励
尝试按照上面所示设置示例应用中的纵横比,以适应纵向和横向方向。使用您的大型平板电脑(如果纵向宽度为 840dp 或更大)或 Pixel C 模拟器测试设置。您应该会看到活动在纵向方向上被分割,但在横向方向上没有被分割。
确定您大型平板电脑的纵向纵横比(Pixel C 的纵横比略大于 1.4)。将 splitMaxAspectRatioInPortrait
设置为高于和低于该纵横比的值。运行应用程序,看看您得到什么结果。
6. WindowManager API
您可以使用从启动分割的活动的 onCreate()
方法中调用的单个方法完全在代码中启用活动嵌入。如果您更喜欢在代码中而不是在 XML 中工作,那么这就是方法。
添加 WindowManager 依赖项
无论您是创建基于 XML 的实现还是使用 API 调用,您的应用程序都需要访问 WindowManager 库。请参阅本 codelab 的“XML 配置”部分,了解如何将 WindowManager 依赖项添加到您的应用程序。
通知系统
无论您使用 XML 配置文件还是 WindowManager API 调用,您的应用程序都必须通知系统应用程序已实现活动嵌入。请参阅本 codelab 的“XML 配置”部分,了解如何告知系统您的实现。
创建一个类来管理分割
在本节 codelab 中,您将完全在一个静态或伴生对象方法内实现活动分割,您将从示例应用程序的主活动 ListActivity
调用该方法。
创建一个名为 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
定义了一对活动的分割规则。
SplitPairRule.Builder
创建一个 SplitPairRule
。构建器将一组 SplitPairFilter
对象作为参数。过滤器指定何时应用规则。
您将规则注册到 RuleController
组件的单例实例,这使得系统可以访问分割规则。
要创建分割规则,请执行以下操作
- 创建一个分割对过滤器,将
ListActivity
和DetailActivity
识别为共享分割的活动
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
);
过滤器可以包含辅助活动启动的意图操作(第三个参数)。如果包含意图操作,则过滤器将检查操作以及活动名称。对于您自己应用程序中的活动,您可能不会根据意图操作进行筛选,因此参数可以为 null。
- 将过滤器添加到过滤器集
SplitManager.kt / createSplit()
val filterSet = setOf(splitPairFilter)
SplitManager.java / createSplit()
Set<SplitPairFilter> filterSet = new HashSet<>();
filterSet.add(splitPairFilter);
- 为分割创建布局属性
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
:定义如何将可用显示区域分配给每个活动容器。比例分割类型指定主容器占据的显示比例;辅助容器占据剩余的显示区域。setLayoutDirection
:指定活动容器相对于彼此的布局方式,首先是主容器。
- 构建分割对规则
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
:包含确定何时通过识别共享分割的活动来应用规则的分割对过滤器。在示例应用程序中,ListActivity
和DetailActivity
在分割对过滤器中指定(请参见前面的步骤)。setDefaultSplitAttributes
:将布局属性应用于规则。setMinWidthDp
:设置允许分割的最小显示宽度(以密度无关像素 dp 为单位)。setMinSmallestWidthDp
:设置两个显示尺寸中较小的尺寸必须具有的最小值(以 dp 为单位),以允许分割,而不管设备方向如何。setFinishPrimaryWithSecondary
:设置完成辅助容器中的所有活动如何影响主容器中的活动。NEVER
表示当辅助容器中的所有活动完成时,系统不应完成主活动。(请参阅完成活动。)setFinishSecondaryWithPrimary
:设置完成主容器中的所有活动如何影响辅助容器中的活动。ALWAYS
表示当主容器中的所有活动完成时,系统应始终完成辅助容器中的活动。(请参阅完成活动。)setClearTop
:指定在辅助容器中启动新活动时是否完成辅助容器中的所有活动。False 指定新活动堆叠在辅助容器中已有的活动之上。
- 获取 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
定义了当辅助容器中没有可用内容时占据辅助容器的活动的规则。要创建占位符活动,请参阅本 codelab“XML 配置”部分中的“创建占位符活动”。(有关更多信息,请参阅占位符 在活动嵌入 开发人员指南中。)
SplitPlaceholderRule.Builder
创建一个 SplitPlaceholderRule
。构建器将一组 ActivityFilter
对象作为参数。这些对象指定与占位符规则关联的活动。如果过滤器与已启动的活动匹配,则系统将应用占位符规则。
您将规则注册到 RuleController
组件。
要创建分割占位符规则,请执行以下操作
- 创建一个
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
);
该过滤器将规则与示例应用程序的主活动 ListActivity
关联。因此,当列表详细信息布局中没有详细信息内容时,占位符将填充详细信息区域。
过滤器可以包含关联的活动启动(ListActivity
启动)的意图操作(第二个参数)。如果包含意图操作,则过滤器将检查操作以及活动名称。对于您自己应用程序中的活动,您可能不会根据意图操作进行筛选,因此参数可以为 null。
- 将过滤器添加到过滤器集
SplitManager.kt / createSplit()
val placeholderActivityFilterSet = setOf(placeholderActivityFilter)
SplitManager.java / createSplit()
Set<ActivityFilter> placeholderActivityFilterSet = new HashSet<>();
placeholderActivityFilterSet.add(placeholderActivityFilter);
- 创建一个
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
:包含确定何时通过识别与占位符活动关联的活动来应用规则的活动过滤器。Intent
:指定占位符活动的启动。setDefaultSplitAttributes
:将布局属性应用于规则。setMinWidthDp
:设置允许分割的最小显示宽度(以密度无关像素 dp 为单位)。setMinSmallestWidthDp
:设置两个显示尺寸中较小的尺寸必须具有的最小值(以 dp 为单位),以允许分割,而不管设备方向如何。setFinishPrimaryWithPlaceholder
:设置完成占位符活动如何影响主容器中的活动。ALWAYS
表示当占位符完成时,系统应始终完成主容器中的活动。(请参阅完成活动。)
- 将规则添加到 WindowManager
RuleController
SplitManager.kt / createSplit()
ruleController.addRule(splitPlaceholderRule)
SplitManager.java / createSplit()
ruleController.addRule(splitPlaceholderRule);
创建一个活动规则
必需的 API
可以使用ActivityRule
定义占据整个任务窗口的活动规则,例如模式对话框。(更多信息,请参见Android开发者指南中关于全屏模式对话框 的内容,该内容位于活动嵌入开发者指南中)。
SplitPlaceholderRule.Builder
创建一个 SplitPlaceholderRule
。构建器将一组 ActivityFilter
对象作为参数。这些对象指定与占位符规则关联的活动。如果过滤器与已启动的活动匹配,则系统将应用占位符规则。
您将规则注册到 RuleController
组件。
要创建活动规则,请执行以下操作:
- 创建一个
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
);
过滤器指定规则适用的活动:SummaryActivity
。
过滤器可以包含相关活动启动的意图操作(第二个参数)(SummaryActivity
启动)。如果您包含意图操作,则过滤器会同时检查操作和活动名称。对于您自己应用中的活动,您可能不会根据意图操作进行过滤,因此参数可以为null。
- 将过滤器添加到过滤器集
SplitManager.kt / createSplit()
val summaryActivityFilterSet = setOf(summaryActivityFilter)
SplitManager.java / createSplit()
Set<ActivityFilter> summaryActivityFilterSet = new HashSet<>();
summaryActivityFilterSet.add(summaryActivityFilter);
- 创建
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
:包含活动过滤器,这些过滤器通过识别要从拆分中排除的活动来确定何时应用规则。setAlwaysExpand
:指定活动是否应扩展以填充所有可用的显示空间。
- 将规则添加到 WindowManager
RuleController
SplitManager.kt / createSplit()
ruleController.addRule(activityRule)
SplitManager.java / createSplit()
ruleController.addRule(activityRule);
运行它!
构建并运行示例应用程序。
应用的行为应与其使用XML配置文件自定义时的行为相同。
请参阅本Codelab“XML配置”部分中的“运行!”。
额外奖励
尝试使用SplitPairRule.Builder
和SplitPlaceholderRule.Builder
的setMaxAspectRatioInPortrait
和setMaxAspectRatioInLandscape
方法在示例应用中设置纵横比。使用EmbeddingAspectRatio
类的属性和方法指定值,例如:
SplitPairRule.Builder(filterSet)
. . .
.setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ratio(1.5f))
. . .
.build()
使用您的平板电脑或Pixel C模拟器测试设置。
确定您大型平板电脑的纵向纵横比(Pixel C的纵横比略大于1.4)。将纵向最大纵横比设置为高于和低于平板电脑或Pixel C纵横比的值。尝试使用ALWAYS_ALLOW
和ALWAYS_DISALLOW
属性。
运行应用,查看结果。
更多信息,请参阅本Codelab“XML配置”部分中的“纵横比”。
7. Material Design导航
Material Design指南为不同屏幕尺寸指定了不同的导航组件——对于宽度大于或等于840dp的屏幕,使用导航栏;对于宽度小于840dp的屏幕,使用底部导航栏。
使用活动嵌入时,您不能使用WindowManager
方法getCurrentWindowMetrics()
和getMaximumWindowMetrics()
来确定屏幕宽度,因为这些方法返回的窗口度量描述的是包含调用这些方法的嵌入式活动的显示窗格。
要获取活动嵌入式应用的精确尺寸,请使用拆分属性计算器和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
的新菜单资源文件。将菜单文件的内容替换为以下内容:
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: 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
获取有关当前活动拆分的信息,并提供交互点以自定义拆分并形成新的拆分。
在前面的章节中,我们通过在XML文件中指定splitRatio
和其他属性(在<SplitPairRule>
和<SplitPlaceHolderRule>
标签中),或者使用SplitPairRule.Builder#setDefaultSplitAttributes()
和SplitPlaceholderRule.Builder#setDefaultSplitAttributes()
API来设置拆分的默认属性。
如果父容器的WindowMetrics满足SplitRule的尺寸要求(minWidthDp、minHeightDp和minSmallestWidthDp),则应用默认拆分属性。
我们将设置一个拆分属性计算器来替换默认的拆分属性。计算器在窗口或设备状态发生变化后(例如方向变化或折叠状态变化)更新现有的拆分对。
这允许开发者了解设备或窗口状态,并在不同的场景中设置不同的拆分属性,包括纵向和横向方向以及桌面姿势。
创建拆分属性计算器时,平台会将SplitAttributesCalculatorParams
对象传递给setSplitAttributesCalculator()
函数。parentWindowMetrics
属性提供应用程序窗口度量。
在下面的代码中,活动检查默认约束是否满足,即宽度 > 840dp 且最小宽度 > 600dp。当条件满足时,活动将嵌入到双窗格布局中,并且应用使用导航栏而不是底部导航栏。否则,活动将以全屏模式显示,并带有底部导航栏。
ListActivity.kt / onCreate()
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()
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();
}
});
很好,您的活动嵌入式应用现在遵循Material Design导航指南!
8. 恭喜!
干得好!您已将基于活动的应用优化为在大屏幕上显示列表-详情布局,并添加了Material Design导航。
您学习了两种实现活动嵌入的方法:
- 使用XML配置文件
- 进行Jetpack API调用
- 使用活动嵌入实现灵活导航
并且您没有重写任何应用的Kotlin或Java源代码。
您已准备好使用活动嵌入优化您的生产应用以适应大屏幕!
9. 了解更多
- 开发者指南——活动嵌入
- 参考文档——androidx.window.embedding
- Codelab——高级活动嵌入
- 示例代码——活动嵌入