1. 简介
Activity 嵌入功能在 Android 12L(API 级别 32)中引入,使基于 Activity 的应用能够在大型屏幕上同时显示多个 Activity,从而创建双窗格布局,例如列表-详情布局。
《使用 Activity 嵌入和 Material Design 构建列表-详情布局》Codelab 介绍了如何使用 XML 或 Jetpack WindowManager API 调用来创建列表-详情布局。
本 Codelab 将指导您了解 Activity 嵌入的一些最新发布的功能,这些功能可进一步改善您的应用在大型屏幕设备上的体验。这些功能包括窗格展开、Activity 固定以及全屏对话框变暗。
前提条件
- 完成《使用 Activity 嵌入和 Material Design 构建列表-详情布局》Codelab
- 有在 Android Studio 中工作的经验,包括设置 Android 15 虚拟设备
学习内容
如何
- 启用窗格展开
- 实现其中一个拆分窗口的 Activity 固定
- 使用全屏对话框变暗
所需条件
- 最新版 Android Studio
- 安装 Android 15 的 Android 手机或模拟器
- 最小宽度大于 600dp 的 Android 大平板电脑或模拟器
2. 设置
获取示例应用
第 1 步:克隆仓库
克隆大型屏幕 Codelabs Git 仓库
git clone https://github.com/android/large-screen-codelabs
或下载并解压大型屏幕 Codelabs zip 文件
第 2 步:检查 Codelab 源文件
导航到 activity-embedding-advanced
文件夹。
第 3 步:打开 Codelab 项目
在 Android Studio 中,打开 Kotlin 或 Java 项目
仓库和 zip 文件中的 activity-embedding-advanced
文件夹包含两个 Android Studio 项目:一个是用 Kotlin 编写的,另一个是用 Java 编写的。打开您选择的项目。Codelab 代码片段以这两种语言提供。
创建虚拟设备
如果您没有 API 级别 35 或更高版本的 Android 手机、小平板电脑或大平板电脑,请在 Android Studio 中打开设备管理器并创建您需要的以下任何虚拟设备
- 手机 — Pixel 8,API 级别 35 或更高
- 平板电脑 — Pixel Tablet,API 级别 35 或更高
3. 运行应用
示例应用会显示一个项目列表。当用户选择一个项目时,应用会显示关于该项目的信息。
该应用包含三个 Activity
ListActivity
— 在RecyclerView
中包含一个项目列表DetailActivity
— 当从列表中选择项目时,显示有关列表项目的信息SummaryActivity
— 当选择“摘要”列表项目时,显示信息的摘要
继续之前的 Codelab
在《使用 Activity 嵌入和 Material Design 构建列表-详情布局》Codelab 中,我们开发了一个应用,该应用使用 Activity 嵌入功能实现了列表-详情视图,并通过导航栏和底部导航栏进行导航。
- 在大型平板电脑或 Pixel 模拟器上以纵向模式运行应用。您会看到主列表屏幕和底部的导航栏。
- 将平板电脑横向放置(横向)。显示屏应拆分,一侧显示列表,另一侧显示详情。底部的导航栏应替换为垂直导航栏。
Activity 嵌入的新特性
准备好升级您的双窗格布局了吗?在本 Codelab 中,我们将添加一些很酷的新功能来提升您的用户体验。以下是我们即将构建的内容
- 让这些窗格变得动态起来!我们将实现窗格展开功能,让您的用户可以调整(或展开)窗格大小以获得自定义视图。
- 让您的用户掌握优先权!借助 Activity 固定功能,用户可以将最重要的任务始终保留在屏幕上。
- 需要专注于特定任务?我们将添加全屏变暗功能,以便轻轻淡出干扰,让用户专注于最重要的事情。
4. 窗格展开
在大型屏幕上使用双窗格布局时,许多情况下用户需要专注于其中一个拆分窗格,同时将另一个窗格保留在屏幕上,例如,在一侧阅读文章,同时在另一侧保留聊天对话列表。用户通常希望调整窗格大小以便专注于一个 Activity。
为了实现这一目标,Activity 嵌入新增了一个 API,使您能够为用户提供更改拆分比例和自定义调整大小转换的机会。
添加依赖项
首先,将 WindowManager 1.4 添加到您的 build.gradle
文件中。
注意:此库中的某些功能仅适用于 Android 15(API 级别 35)及更高版本。
build.gradle
implementation 'androidx.window:window:1.4.0-alpha02'
自定义窗口分隔线
创建一个 DividerAttributes
实例并将其添加到 SplitAttributes
中。此对象配置拆分布局的整体行为。您可以使用 DividerAttributes
的颜色、宽度和拖动范围属性来增强用户体验。
自定义分隔线
- 检查 WindowManager Extensions API 级别。由于窗格展开功能仅在 API 级别 6 及更高版本上可用,因此这也适用于其余的新功能。
- 创建
DividerAttributes
:要设置窗格之间分隔线的样式,请创建一个DividerAttributes
对象。此对象允许您设置
- 颜色:更改分隔线的颜色以匹配您的应用主题或创建视觉分隔。
- widthDp:调整分隔线的宽度以获得更好的可见性或更细微的外观。
- 添加到
SplitAttributes
:自定义分隔线后,将其添加到您的SplitAttributes
对象中。 - 设置拖动范围(可选):您还可以控制用户拖动分隔线的距离以调整窗格大小。
DRAG_RANGE_SYSTEM_DEFAULT
:使用此特殊值让系统根据设备的屏幕尺寸和外形因素确定合适的拖动范围。- 自定义值(介于 0.33 和 0.66 之间):设置您自己的拖动范围以限制用户调整窗格大小的程度。请记住,如果他们拖动超出此限制,则拆分布局将被禁用。
将 splitAttributes
替换为以下代码。
SplitManager.kt
val splitAttributesBuilder: SplitAttributes.Builder = SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.ratio(0.33f))
.setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
if (WindowSdkExtensions.getInstance().extensionVersion >= 6) {
splitAttributesBuilder.setDividerAttributes(
DividerAttributes.DraggableDividerAttributes.Builder()
.setColor(getColor(context, R.color.divider_color))
.setWidthDp(4)
.setDragRange(
DividerAttributes.DragRange.DRAG_RANGE_SYSTEM_DEFAULT)
.build()
)
}
val splitAttributes: SplitAttributes = splitAttributesBuilder.build()
SplitManager.java
SplitAttributes.Builder splitAttributesBuilder = new SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.ratio(0.33f))
.setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT);
if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 6) {
splitAttributesBuilder.setDividerAttributes(
new DividerAttributes.DraggableDividerAttributes.Builder()
.setColor(ContextCompat.getColor(context, R.color.divider_color))
.setWidthDp(4)
.setDragRange(DividerAttributes.DragRange.DRAG_RANGE_SYSTEM_DEFAULT)
.build()
);
}
SplitAttributes splitAttributes = splitAttributesBuilder.build();
在 res/color
文件夹中创建 divider_color.xml
文件,内容如下。
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#669df6" />
</selector>
运行!
就是这样。构建并运行示例应用。
您应该会看到窗格展开并且能够拖动它。
在旧版本中更改拆分比例
重要的兼容性说明:窗格展开功能仅在 WindowManager Extensions 6 或更高版本上可用,这意味着您需要 Android 15(API 级别 35)或更高版本。
但是,您仍然希望为旧版 Android 上的用户提供良好的体验。
在 Android 14(API 级别 34)及更低版本上,您仍然可以使用 SplitAttributesCalculator
类提供动态拆分比例调整。这提供了一种即使没有窗格展开也能保持一定程度的用户对布局控制的方式。
想知道使用这些功能的最佳方式吗?我们将在“最佳实践”部分介绍所有最佳实践和内幕技巧。
5. Activity 固定
有没有想过在拆分屏幕视图中固定一部分,同时在另一部分自由导航?想象一下,在一侧阅读长篇文章,同时仍能在另一半屏幕上与应用的其他内容进行交互。
这就是 Activity 固定派上用场的地方!它允许您将其中一个拆分窗口固定在原位,以便即使您在另一个窗口中导航,它也能保持在屏幕上。这为您的用户提供了更集中和高效的多任务处理体验。
添加固定按钮
首先,我们在 DetailActivity.
中添加一个按钮。当用户点击该按钮时,应用会固定此 DetailActivity
。
对 activity_detail.xml
进行以下更改
- 为
ConstraintLayout
添加一个 ID
android:id="@+id/detailActivity"
- 在布局底部添加一个按钮
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/pinButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pin_this_activity"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textViewItemDetail"/>
- 将
TextView
的底部约束到按钮的顶部
app:layout_constraintBottom_toTopOf="@id/pinButton"
移除 TextView
中的这一行。
app:layout_constraintBottom_toBottomOf="parent"
这是您的 activity_detail.xml
布局文件的完整 XML 代码,包括我们刚刚添加的固定此 ACTIVITY 按钮
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/detailActivity"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
tools:context=".DetailActivity">
<TextView
android:id="@+id/textViewItemDetail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="36sp"
android:textColor="@color/obsidian"
app:layout_constraintBottom_toTopOf="@id/pinButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/pinButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pin_this_activity"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textViewItemDetail"/>
</androidx.constraintlayout.widget.ConstraintLayout>
将字符串 pin_this_activity
添加到 res/values/strings.xml
。
<string name="pin_this_activity">PIN THIS ACTIVITY</string>
连接固定按钮
- 声明变量:在您的
DetailActivity.kt
文件中,声明一个变量以保存对固定此 ACTIVITY 按钮的引用
DetailActivity.kt
private lateinit var pinButton: Button
DetailActivity.java
private Button pinButton;
- 在布局中找到按钮并添加一个
setOnClickListener()
回调。
DetailActivity.kt / onCreate
pinButton = findViewById(R.id.pinButton)
pinButton.setOnClickListener {
pinActivityStackExample(taskId)
}
DetailActivity.java / onCreate()
Button pinButton = findViewById(R.id.pinButton);
pinButton.setOnClickListener( (view) => {
pinActivityStack(getTaskId());
});
- 在您的
DetailActivity
类中创建一个名为pinActivityStackExample
的新方法。我们将在此处实现实际的固定逻辑。
DetailActivity.kt
private fun pinActivityStackExample(taskId: Int) {
val splitAttributes: SplitAttributes = SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.ratio(0.66f))
.setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
.build()
val pinSplitRule = SplitPinRule.Builder()
.setSticky(true)
.setDefaultSplitAttributes(splitAttributes)
.build()
SplitController.getInstance(applicationContext).pinTopActivityStack(taskId, pinSplitRule)
}
DetailActivity.java
private void pinActivityStackExample(int taskId) {
SplitAttributes splitAttributes = new SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.ratio(0.66f))
.setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
.build();
SplitPinRule pinSplitRule = new SplitPinRule.Builder()
.setSticky(true)
.setDefaultSplitAttributes(splitAttributes)
.build();
SplitController.getInstance(getApplicationContext()).pinTopActivityStack(taskId, pinSplitRule);
}
注意
- 一次只能固定一个 Activity。使用以下方法取消固定当前已固定的 Activity
unpinTopActivityStack()
在你再置顶一个之前。
- 要在固定 Activity 时启用窗格展开,请调用
setDividerAttributes()
用于新创建的
SplitAttributes
也一样。
返回导航变化
使用 WindowManager 1.4 后,返回导航的行为发生了变化。使用按钮导航时,返回事件会发送到最后聚焦的 Activity。
按钮导航
- 使用按钮导航,返回事件现在始终发送到最后聚焦的 Activity。这简化了返回导航行为,使其对用户更具可预测性。
手势导航
- Android 14(API 级别 34)及更低版本:返回手势将事件发送到发生手势的 Activity,这可能导致在拆分屏幕场景中出现意外行为。
- Android 15(API 级别 35)及更高版本
- 同应用 Activity:返回手势始终结束顶部 Activity,无论滑动方向如何,从而提供更统一的体验。
- 不同应用 Activity(叠加层):返回事件会转到最后聚焦的 Activity,这与按钮导航的行为一致。
运行!
构建并运行示例应用。
固定 Activity
- 导航到
DetailActivity
屏幕。 - 轻触固定此 ACTIVITY 按钮。
6. 全屏对话框变暗
Activity 嵌入有助于实现拆分屏幕布局,但在以前的版本中,对话框只会使其自身 Activity 的容器变暗。这可能会创建一种脱节的视觉体验,尤其是在您希望对话框占据中心位置时。
解决方案:WindowManager 1.4
- 我们已为您考虑周全!使用 WindowManager 1.4,对话框默认会使整个应用窗口变暗 (
DimAreaBehavior.Companion.ON_TASK
),从而提供更沉浸和聚焦的感觉。 - 需要恢复旧行为?没问题!您仍然可以使用
ON_ACTIVITY_STACK
选择仅使 Activity 的容器变暗。
|
|
以下是您如何使用 ActivityEmbeddingController
管理全屏变暗行为的方法
注意:全屏对话框变暗功能需要 WindowManager Extensions 5 或更高版本。
SplitManager.kt / createSplit()
with(ActivityEmbeddingController.getInstance(context)) {
if (WindowSdkExtensions.getInstance().extensionVersion >= 5) {
setEmbeddingConfiguration(
EmbeddingConfiguration.Builder()
.setDimAreaBehavior(ON_TASK)
.build()
)
}
}
SplitManager.java / createSplit()
ActivityEmbeddingController controller = ActivityEmbeddingController.getInstance(context);
if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 5) {
controller.setEmbeddingConfiguration(
new EmbeddingConfiguration.Builder()
.setDimAreaBehavior(EmbeddingConfiguration.DimAreaBehavior.ON_TASK)
.build()
);
}
为了展示全屏变暗功能,我们将引入一个警告对话框,该对话框在固定 Activity 之前提示用户进行确认。出现时,此对话框会使整个应用窗口变暗,而不仅仅是 Activity 所在的容器。
DetailActivity.kt
pinButton.setOnClickListener {
showAlertDialog(taskId)
}
...
private fun showAlertDialog(taskId: Int) {
val builder = AlertDialog.Builder(this)
builder.setTitle(getString(R.string.dialog_title))
builder.setMessage(getString(R.string.dialog_message))
builder.setPositiveButton(getString(R.string.button_yes)) { _, _ ->
if (WindowSdkExtensions.getInstance().extensionVersion >= 6) {
pinActivityStackExample(taskId)
}
}
builder.setNegativeButton(getString(R.string.button_cancel)) { _, _ ->
// Cancel
}
val dialog: AlertDialog = builder.create()
dialog.show()
}
DetailActivity.java
pinButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showAlertDialog(getTaskId());
}
});
...
private void showAlertDialog(int taskId) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getString(R.string.dialog_title));
builder.setMessage(getString(R.string.dialog_message));
builder.setPositiveButton(getString(R.string.button_yes), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 6) {
pinActivityStackExample(taskId);
}
}
});
builder.setNegativeButton(getString(R.string.button_cancel), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Cancel
}
});
AlertDialog dialog = builder.create();
dialog.show();
}
将以下字符串添加到 res/values/strings.xml
。
<!-- Dialog information -->
<string name="dialog_title">Activity Pinning</string>
<string name="dialog_message">Confirm to pin this activity</string>
<string name="button_yes">Yes</string>
<string name="button_cancel">Cancel</string>
运行!
构建并运行示例应用。
点击固定 Activity 按钮
- 出现一个警告对话框,提示您确认固定操作。
- 注意整个屏幕(包括两个拆分窗格)都变暗了,从而将注意力集中在对话框上。
7. 最佳实践
允许用户关闭双窗格布局
为了使向新布局的过渡更顺畅,我们让用户能够切换双窗格和单列视图。我们可以使用 SplitAttributesCalculator
和 SharedPreferences
来存储用户偏好设置,从而实现这一点。
在 Android 14 及更低版本上更改拆分比例
我们已经探讨了窗格展开功能,它为 Android 15 及更高版本的用户提供了一种调整拆分比例的极佳方式。但我们如何在旧版 Android 上的用户提供类似的灵活性呢?
让我们深入了解 SplitAttributesCalculator
如何帮助我们实现这一目标,并确保在更广泛的设备上提供一致的体验。
以下是它的外观示例
创建设置屏幕
首先,我们创建一个专用的设置屏幕用于用户配置。
在此设置屏幕中,我们将加入一个开关来启用或禁用整个应用的 Activity 嵌入功能。此外,我们将包含一个进度条,允许用户调整双窗格布局的拆分比例。请注意,只有当 Activity 嵌入开关打开时,拆分比例值才会生效。
用户在 SettingsActivity
中设置值后,我们将其保存在 SharedPreferences
中,以便稍后在应用的其他地方使用。
build.gradle
添加 preference 依赖项。
implementation 'androidx.preference:preference-ktx:1.2.1' // Kotlin
或
implementation 'androidx.preference:preference:1.2.1' // Java
SettingsActivity.kt
package com.example.activity_embedding
import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SeekBarPreference
import androidx.preference.SwitchPreferenceCompat
class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.settings_activity)
if (savedInstanceState == null) {
supportFragmentManager
.beginTransaction()
.replace(R.id.settings, SettingsFragment())
.commit()
}
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) finishActivity()
return super.onOptionsItemSelected(item)
}
private fun finishActivity() { finish() }
class SettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey)
findPreference<SwitchPreferenceCompat>("dual_pane")?.setOnPreferenceChangeListener { _, newValue ->
if (newValue as Boolean) {
this.activity?.let {
SharePref(it.applicationContext).setAEFlag(true)
}
} else {
this.activity?.let {
SharePref(it.applicationContext).setAEFlag(false)
}
}
this.activity?.finish()
true
}
val splitRatioPreference: SeekBarPreference? = findPreference("split_ratio")
splitRatioPreference?.setOnPreferenceChangeListener { _, newValue ->
if (newValue is Int) {
this.activity?.let { SharePref(it.applicationContext).setSplitRatio(newValue.toFloat()/100) }
}
true
}
}
}
}
SettingsActivity.java
package com.example.activity_embedding;
import android.os.Bundle;
import android.view.MenuItem;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.SeekBarPreference;
import androidx.preference.SwitchPreferenceCompat;
public class SettingsActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.settings_activity);
if (savedInstanceState == null) {
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.settings, new SettingsFragment())
.commit();
}
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finishActivity();
return true;
}
return super.onOptionsItemSelected(item);
}
private void finishActivity() {
finish();
}
public static class SettingsFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.root_preferences, rootKey);
SwitchPreferenceCompat dualPanePreference = findPreference("dual_pane");
if (dualPanePreference != null) {
dualPanePreference.setOnPreferenceChangeListener((preference, newValue) -> {
boolean isDualPane = (Boolean) newValue;
if (getActivity() != null) {
SharePref sharePref = new SharePref(getActivity().getApplicationContext());
sharePref.setAEFlag(isDualPane);
getActivity().finish();
}
return true;
});
}
SeekBarPreference splitRatioPreference = findPreference("split_ratio");
if (splitRatioPreference != null) {
splitRatioPreference.setOnPreferenceChangeListener((preference, newValue) -> {
if (newValue instanceof Integer) {
float splitRatio = ((Integer) newValue) / 100f;
if (getActivity() != null) {
SharePref sharePref = new SharePref(getActivity().getApplicationContext());
sharePref.setSplitRatio(splitRatio);
}
}
return true;
});
}
}
}
}
在 layout 文件夹中添加 settings_activity.xml
settings_activity.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/settings"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
将 SettingsActivity
添加到您的清单文件。
<activity
android:name=".SettingsActivity"
android:exported="false"
android:label="@string/title_activity_settings" />
配置 SettingsActivity
的拆分规则。
SplitManager.kt / createSplit()
val settingActivityFilter = ActivityFilter(
ComponentName(context, SettingsActivity::class.java),
null
)
val settingActivityFilterSet = setOf(settingActivityFilter)
val settingActivityRule = ActivityRule.Builder(settingActivityFilterSet)
.setAlwaysExpand(true)
.build()
ruleController.addRule(settingActivityRule)
SplitManager.java / createSplit()
Set<ActivityFilter> settingActivityFilterSet = new HashSet<>();
ActivityFilter settingActivityFilter = new ActivityFilter(
new ComponentName(context, SettingsActivity.class),
null
);
settingActivityFilterSet.add(settingActivityFilter);
ActivityRule settingActivityRule = new ActivityRule.Builder(settingActivityFilterSet)
.setAlwaysExpand(true).build();
ruleController.addRule(settingActivityRule);
以下是用于在 SharedPreferences
中保存用户设置的代码。
SharedPref.kt
package com.example.activity_embedding
import android.content.Context
import android.content.SharedPreferences
class SharePref(context: Context) {
private val sharedPreferences: SharedPreferences =
context.getSharedPreferences("my_app_preferences", Context.MODE_PRIVATE)
companion object {
private const val AE_FLAG = "is_activity_embedding_enabled"
private const val SPLIT_RATIO = "activity_embedding_split_ratio"
const val DEFAULT_SPLIT_RATIO = 0.3f
}
fun setAEFlag(isEnabled: Boolean) {
sharedPreferences.edit().putBoolean(AE_FLAG, isEnabled).apply()
}
fun getAEFlag(): Boolean = sharedPreferences.getBoolean(AE_FLAG, true)
fun getSplitRatio(): Float = sharedPreferences.getFloat(SPLIT_RATIO, DEFAULT_SPLIT_RATIO)
fun setSplitRatio(ratio: Float) {
sharedPreferences.edit().putFloat(SPLIT_RATIO, ratio).apply()
}
}
SharedPref.java
package com.example.activity_embedding;
import android.content.Context;
import android.content.SharedPreferences;
public class SharePref {
private static final String PREF_NAME = "my_app_preferences";
private static final String AE_FLAG = "is_activity_embedding_enabled";
private static final String SPLIT_RATIO = "activity_embedding_split_ratio";
public static final float DEFAULT_SPLIT_RATIO = 0.3f;
private final SharedPreferences sharedPreferences;
public SharePref(Context context) {
this.sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
}
public void setAEFlag(boolean isEnabled) {
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean(AE_FLAG, isEnabled);
editor.apply();
}
public boolean getAEFlag() {
return sharedPreferences.getBoolean(AE_FLAG, true);
}
public float getSplitRatio() {
return sharedPreferences.getFloat(SPLIT_RATIO, DEFAULT_SPLIT_RATIO);
}
public void setSplitRatio(float ratio) {
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putFloat(SPLIT_RATIO, ratio);
editor.apply();
}
}
您还需要一个偏好设置屏幕布局 xml,在 res/xml 下创建 root_preferences.xml
文件,内容如下。
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory app:title="@string/split_setting_header">
<SwitchPreferenceCompat
app:key="dual_pane"
app:title="@string/dual_pane_title" />
<SeekBarPreference
app:key="split_ratio"
app:title="@string/split_ratio_title"
android:min="0"
android:max="100"
app:defaultValue="50"
app:showSeekBarValue="true" />
</PreferenceCategory>
</PreferenceScreen>
并将以下内容添加到 res/values/strings.xml
。
<string name="title_activity_settings">SettingsActivity</string>
<string name="split_setting_header">Dual Pane Display</string>
<string name="dual_pane_title">Dual Pane</string>
<string name="split_ratio_title">Split Ratio</string>
将 SettingsActivity
添加到菜单
我们将新创建的 SettingsActivity
连接到导航目标,以便用户可以轻松地从应用的主界面访问它。
- 在您的
ListActivity
文件中,为底部导航栏和左侧导航轨道声明变量
ListActivity.kt
private lateinit var navRail: NavigationRailView private lateinit var bottomNav: BottomNavigationView
ListActivity.java
private NavigationRailView navRail; private BottomNavigationView bottomNav;
- 在您的
ListActivity
的onCreate()
方法内部,使用findViewById
将这些变量连接到布局中相应的视图; - 为底部导航栏和导航轨道添加一个
OnItemSelectedListener
以处理项目选择事件
ListActivity.kt / onCreate()
navRail = findViewById(R.id.navigationRailView)
bottomNav = findViewById(R.id.bottomNavigationView)
val menuListener = NavigationBarView.OnItemSelectedListener { item ->
when (item.itemId) {
R.id.navigation_home -> {
true
}
R.id.navigation_dashboard -> {
true
}
R.id.navigation_settings -> {
startActivity(Intent(this, SettingsActivity::class.java))
true
}
else -> false
}
}
navRail.setOnItemSelectedListener(menuListener)
bottomNav.setOnItemSelectedListener(menuListener)
ListActivity.java / onCreate()
NavigationRailView navRail = findViewById(R.id.navigationRailView);
BottomNavigationView bottomNav = findViewById(R.id.bottomNavigationView);
NavigationBarView.OnItemSelectedListener menuListener = new NavigationBarView.OnItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.navigation_home:
// Handle navigation_home selection
return true;
case R.id.navigation_dashboard:
// Handle navigation_dashboard selection
return true;
case R.id.navigation_settings:
startActivity(new Intent(ListActivity.this, SettingsActivity.class));
return true;
default:
return false;
}
}
};
navRail.setOnItemSelectedListener(menuListener);
bottomNav.setOnItemSelectedListener(menuListener);
应用会读取 SharedPreferences
并以拆分模式或 SPLIT_TYPE_EXPAND
模式渲染应用。
- 当窗口配置更改时,程序会检查是否满足拆分窗口约束(如果宽度 > 840dp)
- 然后应用检查
SharedPreferences
的值,查看用户是否已启用拆分窗口显示,否则它将返回带有SPLIT_TYPE_EXPAND
类型的SplitAttribute
。 - 如果启用了拆分窗口,应用会读取
SharedPreferences
的值以获取拆分比例。这仅在WindowSDKExtensions
版本小于 6 时有效,因为版本 6 已支持窗格展开并忽略拆分比例设置。开发者可以改为允许用户在 UI 上拖动分隔线。
ListActivity.kt / onCreate()
...
SplitController.getInstance(this).setSplitAttributesCalculator{
params -> params.defaultSplitAttributes
if (params.areDefaultConstraintsSatisfied) {
setWiderScreenNavigation(true)
if (SharePref(this.applicationContext).getAEFlag()) {
if (WindowSdkExtensions.getInstance().extensionVersion < 6) {
// Read a dynamic split ratio from shared preference.
val currentSplit = SharePref(this.applicationContext).getSplitRatio()
if (currentSplit != SharePref.DEFAULT_SPLIT_RATIO) {
return@setSplitAttributesCalculator SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.ratio(SharePref(this.applicationContext).getSplitRatio()))
.setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
.build()
}
}
return@setSplitAttributesCalculator params.defaultSplitAttributes
} else {
SplitAttributes.Builder()
.setSplitType(SPLIT_TYPE_EXPAND)
.build()
}
} else {
setWiderScreenNavigation(false)
SplitAttributes.Builder()
.setSplitType(SPLIT_TYPE_EXPAND)
.build()
}
}
...
ListActivity.java / onCreate()
...
SplitController.getInstance(this).setSplitAttributesCalculator(params -> {
if (params.areDefaultConstraintsSatisfied()) {
setWiderScreenNavigation(true);
SharePref sharedPreference = new SharePref(this.getApplicationContext());
if (sharedPreference.getAEFlag()) {
if (WindowSdkExtensions.getInstance().getExtensionVersion() < 6) {
// Read a dynamic split ratio from shared preference.
float currentSplit = sharedPreference.getSplitRatio();
if (currentSplit != SharePref.DEFAULT_SPLIT_RATIO) {
return new SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.ratio(sharedPreference.getSplitRatio()))
.setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
.build();
}
}
return params.getDefaultSplitAttributes();
} else {
return new SplitAttributes.Builder()
.setSplitType(SPLIT_TYPE_EXPAND)
.build();
}
} else {
setWiderScreenNavigation(false);
return new SplitAttributes.Builder()
.setSplitType(SPLIT_TYPE_EXPAND)
.build();
}
});
...
在设置更改后触发 SplitAttributesCalculator
,我们需要使当前属性失效。我们通过调用 ActivityEmbeddingController
的 invalidateVisibleActivityStacks
()
方法来实现这一点;在 WindowManager 1.4 之前,该方法称为
invalidateTopVisibleSplitAttributes
.
ListActivity.kt / onResume()
override fun onResume() {
super.onResume()
ActivityEmbeddingController.getInstance(this).invalidateVisibleActivityStacks()
}
ListActivity.java / onResume()
@Override
public void onResume() {
super.onResume();
ActivityEmbeddingController.getInstance(this).invalidateVisibleActivityStacks();
}
运行!
构建并运行示例应用。
探索设置
- 导航到设置屏幕。
- 打开和关闭启用拆分窗口开关。
- 调整拆分比例滑块(如果您的设备上可用)。
观察布局变化:
- 在运行 Android 14 及更低版本的设备上:布局应根据开关在单窗格和双窗格模式之间切换,并且当您调整滑块时,拆分比例应发生变化。
- 在运行 Android 15 及更高版本的设备上:窗格展开应允许您动态调整窗格大小,无论滑块设置如何。
8. 恭喜!
做得好!您已成功使用 Activity 嵌入和 WindowManager 的强大新功能增强了您的应用。无论用户的 Android 版本如何,他们现在都将在大型屏幕上享受到更灵活、更直观、更具吸引力的体验。
9. 了解更多
- 开发者指南 — Activity 嵌入
- 参考文档 — androidx.window.embedding