高级 activity 嵌入

1. 简介

Android 12L(API 级别 32)中引入的 activity 嵌入可让基于 activity 的应用在大屏设备上同时显示多个 activity,从而创建双窗格布局,例如列表-详情布局。

在 Codelab 使用 activity 嵌入和 Material Design 构建列表-详情布局中,介绍了如何使用 XML 或 Jetpack WindowManager API 调用来创建列表-详情布局。

本 Codelab 将引导您了解几项新发布的与 activity 嵌入有关的功能,这些功能可进一步改善大屏设备上的应用体验。这些功能包括窗格展开、activity 固定和全屏变暗对话框。

前提条件

学习内容

如何执行以下操作:

  • 启用窗格展开功能
  • 使用其中一个分屏实现 activity 固定
  • 使用全屏变暗对话框

所需条件

  • 最新版本的 Android Studio
  • 搭载 Android 15 的 Android 手机或模拟器
  • 最小宽度大于 600dp 的 Android 大屏平板电脑或模拟器

2. 设置

获取示例应用

第 1 步:克隆代码库

克隆大屏幕 Codelab Git 代码库:

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

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

下载源代码

第 2 步:检查 Codelab 源文件

导航到 activity-embedding-advanced 文件夹。

第 3 步:打开 Codelab 项目

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

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

代码库和 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

在 Codelab 使用 activity 嵌入和 Material Design 构建列表-详情布局中,我们开发了一个应用,该应用利用 activity 嵌入实现了列表-详情视图,并且通过侧边导航栏和底部导航栏两种方式来辅助导航。

  1. 在大屏平板电脑或 Pixel 模拟器上以纵向模式运行应用。您会看到主列表屏幕和底部的导航栏。

74906232acad76f.png

  1. 将平板电脑横向放置(横屏)。显示屏应分屏显示,一侧显示列表,另一侧显示详细信息。底部的导航栏应替换为垂直侧边导航栏。

dc6a7d1c02c49cd4.png

与 activity 嵌入相关的新功能

您准备好升级双窗格布局了吗?在此 Codelab 中,我们将添加一些很酷的新功能,以提升用户体验。我们将构建以下内容:

  1. 我们来将这些窗格设为动态窗格!我们将实现窗格扩展功能,让用户能够调整窗格的大小(或扩展窗格)以便自定义视图。

2ec5f7fd6df5d8cd.gif

  1. 让用户能够自行确定优先级!借助 activity 固定功能,用户可以始终在屏幕上显示最重要的任务。

980d0033972737ed.gif

  1. 需要专注于特定任务?我们将新增全屏变暗功能,柔和地淡化干扰,让用户专注于最重要的任务。

2d3455e0f8901f95.png

4. 窗格展开

在大屏幕上使用双窗格布局时,用户往往需要专注于其中一个窗格,同时将另一个窗格保留在屏幕上,例如在一侧阅读文章,同时在另一侧保留聊天对话列表。很多情况下,用户都希望能调整窗格大小,以便专注于某一项活动。

为实现此目标,activity 嵌入功能添加了一个新的 API,允许您为用户提供更改分屏比和自定义调整大小转换的机会。

添加依赖项

首先,将 WindowManager 1.4 添加到 build.gradle 文件中。

注意:此库中的部分功能仅适用于 Android 15(API 级别 35)及更高版本。

build.gradle

 implementation 'androidx.window:window:1.4.0-alpha02'

自定义窗口分隔线

创建 DividerAttributes 实例并将其添加到 SplitAttributes。此对象用于配置分屏布局的整体行为。您可以使用 DividerAttributes 的颜色、宽度和拖动范围属性来提升用户体验。

自定义分隔线:

  1. 检查 WindowManager Extensions API 级别。鉴于窗格展开功能仅适用于 API 级别 6 及更高级别,其他新功能也适用此要求。
  2. 创建 DividerAttributes:如需为窗格之间的分隔线设置样式,请创建一个 DividerAttributes 对象。借助此对象,您可以设置以下内容:
  • color:更改分隔线的颜色,以与应用的主题相配或创建视觉分隔效果。
  • widthDp:调整分隔线的宽度,以使其更醒目或更隐蔽。
  1. 添加到 SplitAttributes:自定义分隔线后,将其添加到 DividerAttributes 对象中。
  2. 设置拖动范围(可选):您还可以控制用户拖动分隔线调整窗格大小的最大距离。
  • 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>

开始运行!

就这么简单。构建并运行示例应用。

您应该会看到窗格展开,并且能够拖动分隔线。

2ec5f7fd6df5d8cd.gif

更改旧版本中的分屏比

重要兼容性注意事项:窗格展开功能仅适用于 WindowManager Extensions 6 或更高版本,这意味着您需要 Android 15(API 级别 35)或更高版本。

不过,您仍需要为使用旧版 Android 的用户提供良好的体验。

在 Android 14(API 级别 34)及更低版本中,您仍然可以使用 SplitAttributesCalculator 类支持用户动态调整分屏比。这样,即使没有窗格展开功能,用户仍然可以在一定程度上控制布局。

a36f8ba4226353c5.gif

想知道如何才能充分利用这些功能吗?我们将在“最佳实践”部分介绍所有最佳实践和专家建议。

5. activity 固定

您是否曾想过让分屏视图的一部分固定不动,同时在另一部分中自由导航?想象一下,您在屏幕一侧阅读长篇文章,同时仍能在屏幕另一侧与其他应用内容进行交互。

这时,activity 固定功能就派上用场了!有了这项功能,您可以将其中一个分屏窗口固定到屏幕上,这样即使您在另一个窗口中导航,该窗口也会保留在屏幕上。这样一来,您可以为用户提供更专注、更高效的多任务处理体验。

添加固定按钮

首先,我们在 DetailActivity. 中添加一个按钮。当用户点击该按钮时,应用会固定此 DetailActivity

activity_detail.xml 进行以下更改:

  1. ConstraintLayout 添加 ID
android:id="@+id/detailActivity"
  1. 在布局底部添加一个按钮
<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"/>
  1. TextView 底部限制在按钮顶部
app:layout_constraintBottom_toTopOf="@id/pinButton"

TextView 中移除此行。

app:layout_constraintBottom_toBottomOf="parent"

以下是 activity_detail.xml 布局文件的完整 XML 代码,其中包含我们刚刚添加的 PIN THIS ACTIVITY(固定此 activity)按钮:

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/detailActivity"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    tools:context=".DetailActivity">

  <TextView
      android:id="@+id/textViewItemDetail"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:textSize="36sp"
      android:textColor="@color/obsidian"
      app:layout_constraintBottom_toTopOf="@id/pinButton"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent" />

  <androidx.appcompat.widget.AppCompatButton
      android:id="@+id/pinButton"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@string/pin_this_activity"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@id/textViewItemDetail"/>

</androidx.constraintlayout.widget.ConstraintLayout>

将字符串 pin_this_activity 添加到 res/values/strings.xml

<string name="pin_this_activity">PIN THIS ACTIVITY</string>

连接固定按钮

  1. 声明变量:DetailActivity.kt 文件中,声明一个变量来保存对 PIN THIS ACTIVITY(固定此 activity)按钮的引用:

DetailActivity.kt

private lateinit var pinButton: Button

DetailActivity.java

private Button pinButton;
  1. 在布局中找到该按钮,并添加 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());

});
  1. 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);
}

注意:

  1. 一次只能固定一个 activity。使用以下代码取消固定当前固定的 activity:
unpinTopActivityStack()

然后再固定其他 activity。

  1. 如需在固定 activity 时启用窗格展开功能,还需要调用
setDividerAttributes()

此调用针对的是新创建的

SplitAttributes

返回导航更改

在 WindowManager 1.4 中,返回导航的行为发生了变化。使用按钮导航时,系统会将返回事件发送到上次聚焦的 activity。

按钮导航:

  • 使用按钮导航时,系统现在会始终将返回事件发送到上次聚焦的 activity。这简化了返回导航行为,使其对用户来说更易于预测。

手势导航:

  • Android 14(API 级别 34)及更低版本:返回手势会将事件发送到发生手势的 activity,这在分屏场景中可能会导致出现意外的行为。
  • Android 15(API 级别 35)及更高版本:
  • 同一应用的 activity:无论滑动方向如何,返回手势始终会结束顶层 activity,从而提供更统一的体验。
  • 不同应用的 activity(叠加层):返回事件会转到上次聚焦的 activity,与按钮导航的行为保持一致。

开始运行!

构建并运行示例应用。

固定 activity

  • 前往 DetailActivity 页面。
  • 点按PIN THIS ACTIVITY(固定此 activity)按钮。

980d0033972737ed.gif

6. 全屏变暗对话框

虽然 activity 嵌入功能有助于实现分屏布局,但在以前的版本中,对话框仅调暗其自身 activity 的容器。这可能会造成不连贯的视觉体验,尤其是当您希望对话框居于中心位置时。

解决方案:WindowManager 1.4

  • 我们为您考虑周全!借助 WindowManager 1.4,默认情况下对话框会调暗整个应用窗口 (DimAreaBehavior.Companion.ON_TASK),从而提供更具沉浸感、更加专注的体验。
  • 如果您需要恢复旧版行为,也没问题!您仍然可以选择使用 ON_ACTIVITY_STACK 仅将 activity 的容器变暗。

ON_ACTIVITY_STACK

ON_TASK

您可以使用 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 按钮:

  • 系统会显示一个提醒对话框,提示您确认固定操作。
  • 请注意,整个屏幕(包括两个分屏窗格)都会变暗,以便将注意力集中在对话框上。

2d3455e0f8901f95.png

7. 最佳实践

允许用户关闭双窗格布局

为了更顺畅地过渡到新布局,让我们为用户提供在双窗格视图和单列视图之间进行切换的功能。我们可以使用 SplitAttributesCalculatorSharedPreferences 来存储用户偏好设置,从而实现这一目的。

在 Android 14 及更低版本中更改分屏比

我们探讨了窗格扩展功能,该功能为用户提供了一种在 Android 15 及更高版本中调整分屏比的好方法。但我们如何才能为使用旧版 Android 的用户提供同等程度的灵活性?

接下来,我们来深入了解 SplitAttributesCalculator 如何帮助我们实现这一目标,并确保在更广泛的设备上提供一致的体验。

以下是一个示例:

a87452341434c86d.gif

创建设置界面

首先,让我们创建一个专门的设置界面供用户配置。

在此设置界面中,我们将添加一个开关,用于为整个应用启用或停用 activity 嵌入功能。此外,我们还将添加一个进度条,让用户可以调整双窗格布局的分屏比。请注意,只有在开启 activity 嵌入开关时,系统才会应用分屏比值。

用户在 SettingsActivity 中设置值后,我们会将其保存在 SharedPreferences 中,以便稍后在应用的其他位置使用。

build.gradle

添加偏好设置依赖项。

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

在布局文件夹中添加 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 连接到导航目的地,这样用户就可以轻松地从应用的主界面访问它。

  1. ListActivity 文件中,为底部导航栏和左侧导航栏声明变量:

ListActivity.kt

 private lateinit var navRail: NavigationRailView private lateinit var bottomNav: BottomNavigationView

ListActivity.java

 private NavigationRailView navRail;  private BottomNavigationView bottomNav;
  1. ListActivityonCreate() 方法内,使用 findViewById 将这些变量连接到布局中的相应视图;
  2. 向底部导航栏和侧边导航栏添加 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 已支持窗格扩展,并会忽略分屏比设置。相反,开发者可以允许用户在界面上拖动分隔线。

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

开始运行!

构建并运行示例应用。

探索设置:

  • 前往设置界面。
  • 开启和关闭 Enable Split Window(启用分屏窗口)开关。
  • 调整分屏比滑块(如果您的设备上提供此功能)。

观察布局更改

  • 在搭载 Android 14 及更低版本的设备上:布局应根据切换开关在单窗格模式和双窗格模式之间切换,并且当您调整滑块时,分屏比会随之发生变化。
  • 在搭载 Android 15 及更高版本的设备上:无论滑块设置如何,您都可以通过窗格展开功能来动态调整窗格大小。

8. 恭喜!

祝贺您!您已成功使用 activity 嵌入和 WindowManager 为应用添加了强大的新功能。现在,无论用户使用的是哪个 Android 版本,都能在大屏设备上获享更灵活、更直观、更引人入胜的体验。

9. 了解更多内容