使用活動嵌入功能和 Material Design 建構清單/詳細資料版面配置

1. 簡介

大螢幕可讓您建立應用程式版面配置和 UI,改善使用者體驗並提升他們的工作效率。但如果應用程式是專為非摺疊式裝置的小型螢幕而設計,則可能無法充分運用平板電腦、摺疊式裝置和 ChromeOS 裝置提供的額外顯示空間。

如要更新應用程式來充分運用大螢幕,可能十分耗時且成本高昂,尤其是對執行多項活動的舊版應用程式而言。

Android 12L (API 級別 32) 中推出了活動嵌入功能,可讓以活動為依據的應用程式在大螢幕上同時顯示多個活動,進而建立雙窗格版面配置 (例如清單/詳細資料)。不需要使用 Kotlin 或 Java 重新編碼。您需要新增一些依附元件、建立 XML 設定檔、實作 Initializer,並在應用程式資訊清單中新增一些內容。或者,如果您偏好使用程式碼,可以在應用程式主要活動的 onCreate() 方法中加入幾個 Jetpack WindowManager API 呼叫。

必要條件

如要完成本程式碼研究室,您需要有以下經驗:

  • 建構 Android 應用程式
  • 使用活動
  • 編寫 XML
  • 在 Android Studio 中執行工作,包括虛擬裝置設定

建構項目

在本程式碼研究室中,您將更新以活動為依據的應用程式,以便支援與 SlidingPaneLayout 類似的動態雙窗格版面配置。在小螢幕上,應用程式會在工作視窗中疊加 (堆疊) 活動。

在工作視窗中堆疊的 活動 A、B 和 C。

在大螢幕上,應用程式會在畫面上同時顯示兩個活動,根據規格決定會並排顯示或堆疊顯示。

4b27b07b7361d6d8.png

課程內容

實作活動嵌入功能的方法有兩種:

  • 使用 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」資料夾的檔案清單。

存放區和 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. 執行應用程式

範例應用程式會顯示項目清單。使用者選取某個項目時,應用程式會顯示該項目的相關資訊。

這個應用程式由三個活動組成:

  • ListActivity:包含 RecyclerView 中的項目清單
  • DetailActivity:在清單中選取項目時,顯示所選清單項目的相關資訊
  • SummaryActivity:選取摘要清單項目時,顯示資訊摘要

不含活動嵌入功能的行為

執行範例應用程式,查看應用程式不含活動嵌入功能時的行為:

  1. 在大型平板電腦或 Pixel C 模擬器上執行範例應用程式。主要 (清單) 活動會顯示:

以直向模式執行範例應用程式的大型平板電腦。全螢幕模式下的清單活動。

  1. 選取清單項目即可啟動次要 (詳細資料) 活動。詳細資料活動會疊加在清單活動上:

以直向模式執行範例應用程式的大型平板電腦。全螢幕模式下的詳細資料活動。

  1. 將平板電腦旋轉為橫向。次要活動仍會疊加在主要活動上,並占用整個螢幕:

以橫向模式執行範例應用程式的大型平板電腦。全螢幕模式下的詳細資料活動。

  1. 選取返回控制項 (應用程式列中的向左箭頭) 即可返回清單。
  2. 選取清單中的最後一個項目「Summary」,將摘要活動做為次要活動來啟動。摘要活動會疊加在清單活動上:

以直向模式執行範例應用程式的大型平板電腦。以全螢幕模式顯示摘要活動。

  1. 將平板電腦旋轉為橫向。次要活動仍會疊加在主要活動上,並占用整個螢幕:

以橫向模式執行範例應用程式的大型平板電腦。以全螢幕模式顯示摘要活動。

包含活動嵌入功能的行為

完成本程式碼研究室後,應用程式會在清單/詳細資料版面配置中,以橫向方式並排顯示清單活動和詳細資料活動:

以橫向模式執行範例應用程式的大型平板電腦。清單/詳細資料版面配置中的清單活動和詳細資料活動。

不過,即使活動是在分割模式下啟動,您還是可以將摘要活動設定為以全螢幕模式顯示。摘要活動會疊加在分割畫面上:

以橫向模式執行範例應用程式的大型平板電腦。以全螢幕模式顯示摘要活動。

4. 背景

活動嵌入功能會將應用程式工作視窗分割為兩個容器:主要容器與次要容器。任何活動都可以啟動另一個活動來開始分割作業。啟動的活動會占用主要容器,而另一個遭啟動的活動則會占用次要容器。

主要活動可在次要容器中啟動其他活動。這樣一來,兩個容器中的活動就可以在各自的容器中啟動活動。每個容器皆可包含活動的堆疊。詳情請參閱「活動嵌入」開發人員指南。

您可以建立 XML 設定檔或發出 Jetpack WindowManager API 呼叫,以支援應用程式嵌入活動。我們會先從 XML 設定方法開始。

5. XML 設定

活動嵌入功能的容器和分割項目是由 Jetpack WindowManager 程式庫建立及管理,取決於您在 XML 設定檔中建立的分割規則。

新增 WindowManager 依附元件

將程式庫依附元件新增至應用程式的模組層級 build.gradle 檔案,即可讓範例應用程式存取 WindowManager 程式庫,例如:

build.gradle

 implementation 'androidx.window:window:1.2.0'

通知系統

請告知系統您的應用程式已實作活動嵌入功能。

在應用程式資訊清單檔案的 <application> 元素中新增 android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED 屬性,然後將值設為 true:

AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application>
        <property
            android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"
            android:value="true" />
    </application>
</manifest>

裝置製造商會利用這項設定,為支援活動嵌入功能的應用程式啟用自訂功能。舉例來說,如果螢幕處於橫向模式,裝置可以在僅限直向顯示的活動 (請參閱 android:screenOrientation) 兩側加上黑邊,讓活動順利轉換為嵌入兩個窗格的版面配置:

在橫向螢幕上對僅支援直向模式的應用程式使用活動嵌入功能。兩側有黑邊且僅支援直向模式的活動 A 會啟動嵌入活動 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>

規則會執行下列作業:

  • 為共用分割畫面的活動設定分割選項:
  • splitRatio:指定主要活動要占用的工作視窗比例 (33%),其餘空間則留給次要活動。
  • splitMinWidthDp:指定要在畫面上同時顯示兩個活動所需的最小顯示寬度 (840),單位為螢幕密度獨立像素 (dp)。
  • finishPrimaryWithSecondary:指明當次要容器中的所有活動完成時,主要分割容器內的活動是否完成 (never)。
  • finishSecondaryWithPrimary:指明當主要容器中的所有活動完成時,次要分割容器內的活動是否完成 (always)。
  • 加入分割篩選器,用於定義共用工作視窗分割畫面的活動。主要活動為 ListActivity,次要活動為 DetailActivity

預留位置規則

如果該容器中沒有可用內容,例如開啟了某個清單/詳細資料的分割畫面,但尚未選取清單項目,預留位置活動就會占用活動分割畫面的次要容器 (詳情請參閱「活動嵌入」開發人員指南中的「預留位置」)。

在設定檔中新增下列預留位置規則:

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>

規則會執行下列作業:

  • 識別預留位置活動 PlaceholderActivity (我們會在下一個步驟中建立這項活動)
  • 設定預留位置的選項:
  • splitRatio:指明主要活動要占用的工作視窗比例 (33%),其餘空間則留給預留位置。一般而言,這個值應符合與預留位置相關聯的分割畫面組規則的分割比例。
  • splitMinWidthDp:指明要在畫面上同時顯示預留位置與主要活動所需的最小顯示寬度 (840)。一般而言,這個值應符合與預留位置相關聯的分割畫面組規則的最小寬度。單位為螢幕密度獨立像素 (dp)。
  • finishPrimaryWithPlaceholder:指明當預留位置完成時,主要分割容器內的活動是否完成 (always)。
  • 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 中執行下列操作:

  1. 在範例應用程式的來源資料夾 (com.example.activity_embedding) 上按一下滑鼠右鍵 (次要按鈕 - 點擊)
  2. 依序選取「New」>「Activity」>「Empty View Activity」
  3. 將活動命名為「PlaceholderActivity」
  4. 選取「Finish」

Android Studio 會在範例應用程式套件中建立活動,並將活動新增至應用程式資訊清單檔案,接著在 res/layout 資料夾中建立名為 activity_placeholder.xml 的版面配置資源檔案。

  1. 在範例應用程式的 AndroidManifest.xml 檔案中,將預留位置活動的標籤設為空字串:

AndroidManifest.xml

<activity
    android:name=".PlaceholderActivity"
    android:exported="false"
    android:label="" />
  1. res/layout 資料夾中的 activity_placeholder.xml 版面配置檔案內容,替換成以下內容:

activity_placeholder.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:background="@color/gray"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".PlaceholderActivity">

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

</androidx.constraintlayout.widget.ConstraintLayout>
  1. 最後,將下列字串資源新增至 res/values 資料夾中的 strings.xml 資源檔案:

strings.xml

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

建立初始化器。

WindowManager RuleController 元件會剖析 XML 設定檔中定義的規則,為系統提供規則。

Jetpack Startup 程式庫 Initializer 可讓 RuleController 存取設定檔。

Startup 程式庫會在應用程式啟動時進行元件初始化作業。初始化作業必須在所有活動開始前執行,以便 RuleController 存取分割規則,並視需要套用。

新增 Startup 程式庫依附元件

如要啟用啟動功能,請將 Startup 程式庫依附元件新增至範例應用程式的模組層級 build.gradle 檔案,例如:

build.gradle

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

為 RuleController 實作初始化器

實作 Startup 的 Initializer 介面。

在 Android Studio 中執行下列操作:

  1. 在範例應用程式的來源資料夾 (com.example.activity_embedding) 上按一下滑鼠右鍵 (次要按鈕 - 點擊)
  2. 依序選取「New」>「Kotlin Class/File」或「New」>「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();
   }
}

Initializer 會將包含定義 (main_split_config 的 XML 資源檔案所用 ID 傳遞給元件的 parseRules() 方法,進而向 RuleController 提供分割規則。setRules() 方法會將剖析的規則新增至 RuleController

建立初始化供應器

供應器會叫用分割規則初始化程序。

請將 androidx.startup.InitializationProvider 做為供應器,加到範例應用程式資訊清單檔案的 <application> 元素,並參照 SplitInitializer

AndroidManifest.xml

<provider android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false"
    tools:node="merge">
    <!-- Make SplitInitializer discoverable by InitializationProvider. -->
    <meta-data android:name="${applicationId}.SplitInitializer"
        android:value="androidx.startup" />
</provider>

InitializationProvider 會初始化 SplitInitializer,後者會叫用剖析 XML 設定檔 (main_split_config.xml) 的 RuleController 方法,並新增規則至 RuleController (請參閱上方的「為 RuleController 實作 Initializer」)。

InitializationProvider 會先探索並初始化 SplitInitializer,再執行應用程式的 onCreate() 方法,因此當主要應用程式活動開始時,分割規則就會生效。

來源檔案

以下是已完成的應用程式資訊清單:

AndroidManifest.xml

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

  <application
      android:allowBackup="true"
      android:dataExtractionRules="@xml/data_extraction_rules"
      android:fullBackupContent="@xml/backup_rules"
      android:icon="@mipmap/ic_launcher"
      android:label="@string/app_name"
      android:roundIcon="@mipmap/ic_launcher_round"
      android:supportsRtl="true"
      android:theme="@style/Theme.Activity_Embedding"
      tools:targetApi="32">
    <activity
        android:name=".ListActivity"
        android:exported="true">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
    <activity
        android:name=".DetailActivity"
        android:exported="false"
        android:label="" />
    <activity
        android:name=".SummaryActivity"
        android:exported="false"
        android:label="" />
    <activity
        android:name=".PlaceholderActivity"
        android:exported="false"
        android:label="" />
    <property
        android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"
        android:value="true" />
    <provider
        android:name="androidx.startup.InitializationProvider"
        android:authorities="${applicationId}.androidx-startup"
        android:exported="false"
        tools:node="merge">
      <!-- Make SplitInitializer discoverable by InitializationProvider. -->
      <meta-data
          android:name="${applicationId}.SplitInitializer"
          android:value="androidx.startup" />
    </provider>
  </application>

</manifest>

初始化捷徑

如果您偏好混合使用 XML 設定和 WindowManager API,可以移除 Startup 程式庫的 Initializer 和資訊清單供應器,以便簡化實作程序。

建立 XML 設定檔後,請按照下列步驟操作:

步驟 1:建立 Application 的子類別

建立應用程式程序時,應用程式子類別將成為第一個例項化的類別。請將分割規則新增至子類別的 onCreate() 方法中的 RuleController,確保規則在活動啟動前生效。

在 Android Studio 中執行下列操作:

  1. 在範例應用程式的來源資料夾 (com.example.activity_embedding) 上按一下滑鼠右鍵 (次要按鈕 - 點擊)
  2. 依序選取「New」>「Kotlin Class/File」或「New」>「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

將分割規則從 XML 設定檔新增至應用程式子類別的 onCreate() 方法中的 RuleController

如要將規則新增至 RuleController,請按照下列步驟操作:

  1. 取得 RuleController 的單例模式例項
  2. 使用 RuleController 的 Java 靜態或 Kotlin 伴生物件 parseRules() 方法,剖析 XML 檔案
  3. 使用 setRules() 方法將剖析規則新增至 RuleController

SampleApplication.kt

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

SampleApplication.java

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

步驟 3:將子類別名稱新增至資訊清單

將子類別的名稱新增至應用程式資訊清單的 <application> 元素:

AndroidManifest.xml

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

開始執行!

建構並執行範例應用程式。

在摺疊式手機上,活動一律會堆疊,即使手機為橫向模式也是如此:

詳細資料 (次要) 活動在直向模式的手機上與清單 (主要) 活動堆疊。 詳細資料 (次要) 活動在橫向模式的手機上與清單 (主要) 活動堆疊。

在 Android 13 (API 級別 33) 以下版本中,無論分割畫面最小寬度規格為何,非摺疊式手機都無法使用活動嵌入功能。

API 級別較高的非摺疊式手機能否使用活動嵌入功能,取決於裝置製造商是否讓裝置支援活動嵌入功能。

在小型平板電腦或 7 WSVGA (平板電腦) 模擬器中,這兩個活動在直向模式會堆疊顯示,在橫向模式則會並排顯示:

清單活動和詳細資料活動在小型平板電腦的直向模式中堆疊顯示。 清單活動和詳細資料活動在小型平板電腦的橫向模式中並排顯示。

在大型平板電腦或 Pixel C 模擬器中,活動在直向模式中會堆疊顯示 (請見下方「顯示比例」),但在橫向模式中則會並排顯示:

清單活動和詳細資料活動在大型平板電腦的直向模式中堆疊顯示。 清單活動和詳細資料活動在大型平板電腦的橫向模式中並排顯示。

即使是在分割模式下啟動,摘要活動在橫向模式中仍會以全螢幕方式顯示:

摘要活動在大型平板電腦的橫向模式中疊加分割畫面。

顯示比例

除了分割的最小寬度外,活動分割畫面會受到螢幕畫面的顯示比例控管。splitMaxAspectRatioInPortraitsplitMaxAspectRatioInLandscape 屬性可指定活動分割畫面呈現的顯示比例上限 (高度:寬度)。這些屬性代表 SplitRulemaxAspectRatioInPortraitmaxAspectRatioInLandscape 屬性。

如果有任一方向的顯示比例超過上限值,則無論螢幕寬度為何,系統都會停用分割作業。直向螢幕方向的預設值是 1.4 (請參閱 SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT),這會禁止高且窄的顯示畫面包含分割畫面。根據預設,橫向模式中一律允許分割畫面 (請參閱 SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT)。

PIxel C 模擬器的直向顯示寬度為 900dp,高於範例應用程式 XML 設定檔中的 splitMinWidthDp 設定,因此模擬器應會顯示活動分割畫面。不過,Pixel C 的直向螢幕顯示比例大於 1.4,因此活動分割畫面無法在直向模式下顯示。

您可以在 SplitPairRuleSplitPlaceholderRule 元素的 XML 設定檔中,設定螢幕直向和橫向的顯示比例上限,例如:

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 程式庫。請參閱本程式碼研究室的「XML 設定」一節,瞭解如何將 WindowManager 依附元件新增至應用程式。

通知系統

無論您是使用 XML 設定檔或 WindowManager API 呼叫,應用程式都必須告知系統,說明應用程式已實作活動嵌入功能。請參閱本程式碼研究室的「XML 設定」一節,瞭解如何將實作項目告知系統。

建立管理分割畫面的類別

在程式碼研究室的此部分,您將直接在單一靜態或伴生物件方法中實作活動分割,您將從範例應用程式的主要活動 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 的原因和方式,請參閱本程式碼研究室「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 元件的單例模式例項註冊規則,將分割規則提供給系統。

如要建立分割規則,請按照下列步驟操作:

  1. 建立分割畫面組篩選器,將 ListActivityDetailActivity 指定為共用分割畫面的活動:

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

篩選器可以為次要活動的啟動加入意圖動作 (第三個參數)。如果您加入了意圖動作,篩選器會檢查該動作及其活動名稱。針對您自己的應用程式中的活動,您可能不用篩選意圖動作,因此引數可以是空值。

  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:定義可用的顯示區域如何分配至各個活動容器。比例分割類型會指定主要容器占用的顯示區域比例;次要容器會占用剩餘的顯示區域。
  • setLayoutDirection:指定活動容器彼此間的版面配置方式,主要容器優先。
  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:包含分割畫面組的篩選器,可識別共用分割畫面的活動,決定套用規則的時機。本範例應用程式會在分割畫面組篩選器中指定 ListActivityDetailActivity (請參閱前述步驟)。
  • setDefaultSplitAttributes:將版面配置屬性套用至規則。
  • setMinWidthDp:設定可允許分割作業的最小螢幕寬度 (以密度獨立像素 dp 為單位)。
  • setMinSmallestWidthDp:設定無論裝置螢幕方向為何,只要兩個顯示畫面尺寸中較小者達到這個下限 (以 dp 為單位),就必須允許分割作業。
  • setFinishPrimaryWithSecondary:設定若完成次要容器中的所有活動,對主要容器中的活動有何影響。NEVER 表示在次要容器中的所有活動完成時,系統應不會完成主要活動 (請參閱「完成活動」)。
  • setFinishSecondaryWithPrimary:設定若完成主要容器中的所有活動,對次要容器中的活動有何影響。ALWAYS 表示在主要容器中的所有活動完成時,系統應一律完成次要容器中的活動 (請參閱「完成活動」)。
  • setClearTop:指定在次要容器中啟動新活動時,是否會完成該容器中的所有活動。如設為 False,系統會指定新活動堆疊在次要容器中的現有活動之上。
  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 為活動定義了一項規則,該活動會在次要容器中沒有任何內容時占用該容器。如要建立預留位置活動,請參閱本程式碼研究室中「XML 設定」一節的「建立預留位置活動」(詳情請參閱「活動嵌入」開發人員指南中的「預留位置」)。

SplitPlaceholderRule.Builder 可建立 SplitPlaceholderRule。該建構工具將一組 ActivityFilter 物件做為引數,這些物件會指定與預留位置規則相關聯的活動。如果篩選器比對出已啟動的活動,系統就會套用預留位置規則。

您可以使用 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
);

這個篩選器會將規則與範例應用程式的主要活動 ListActivity 建立關聯。因此,當清單/詳細資料版面配置中沒有詳細資料內容時,預留位置會填滿詳細資料區域。

篩選器可以為相關聯活動的啟動 (ListActivity 啟動) 加入意圖動作 (第二個參數)。如果您加入了意圖動作,篩選器會檢查該動作及其活動名稱。針對您自己的應用程式中的活動,您可能不用篩選意圖動作,因此引數可以是空值。

  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:包含活動篩選器,可識別與預留位置活動相關聯的活動,決定套用規則的時機。
  • Intent:指定預留位置活動的啟動意圖。
  • setDefaultSplitAttributes:將版面配置屬性套用至規則。
  • setMinWidthDp:設定可允許分割作業的最小螢幕寬度 (以密度獨立像素 dp 為單位)。
  • setMinSmallestWidthDp:設定無論裝置螢幕方向為何,只要兩個顯示畫面尺寸中較小者達到這個下限 (以 dp 為單位),就必須允許分割作業。
  • setFinishPrimaryWithPlaceholder:設定若完成預留位置活動,對主要容器中的活動有何影響。ALWAYS 表示在預留位置完成時,系統應一律完成主要容器中的活動 (請參閱「完成活動」)。
  1. 將規則新增至 WindowManager RuleController

SplitManager.kt/createSplit()

ruleController.addRule(splitPlaceholderRule)

SplitManager.java/createSplit()

ruleController.addRule(splitPlaceholderRule);

建立活動規則

必要的 API:

ActivityRule 可用來為占用整個工作視窗的活動定義規則,例如強制回應對話方塊 (詳情請參閱「活動嵌入」開發人員指南中的「互動式全視窗」)。

SplitPlaceholderRule.Builder 可建立 SplitPlaceholderRule。該建構工具將一組 ActivityFilter 物件做為引數,這些物件會指定與預留位置規則相關聯的活動。如果篩選器比對出已啟動的活動,系統就會套用預留位置規則。

您可以使用 RuleController 元件註冊規則。

如要建立活動規則,請按照下列步驟操作:

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

篩選器會指定規則適用的活動 SummaryActivity

篩選器可以為相關聯活動的啟動 (SummaryActivity 啟動) 加入意圖動作 (第二個參數)。如果您加入了意圖動作,篩選器會檢查該動作及其活動名稱。針對您自己的應用程式中的活動,您可能不用篩選意圖動作,因此引數可以是空值。

  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:包含活動篩選器,可識別要從分割作業中排除的活動,決定套用規則的時機。
  • setAlwaysExpand:指定是否要展開活動,以填滿所有可用的顯示空間。
  1. 將規則新增至 WindowManager RuleController

SplitManager.kt/createSplit()

ruleController.addRule(activityRule)

SplitManager.java/createSplit()

ruleController.addRule(activityRule);

開始執行!

建構並執行範例應用程式。

應用程式的行為應與使用 XML 設定檔進行自訂時的行為相同。

請參閱本程式碼研究室中「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 屬性。

執行應用程式,看看結果如何。

詳情請參閱本程式碼研究室「XML 設定」一節的「顯示比例」。

7. Material Design 導覽

Material Design 指南會針對不同畫面大小指定不同的導覽元件:寬度大於等於 840dp 的情況,會指定導覽邊欄;若是小於 840dp,則會指定底部導覽列。

fb47462060f4818d.gif

在活動嵌入作業中,無法使用 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。將選單檔案的內容替換成以下內容:

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

接著,在版面配置中新增導覽列和導覽邊欄。將瀏覽權限設為 gone,兩者的初始狀態就會是隱藏的。之後我們會根據版面配置尺寸顯示這兩種元件。

activity_list.xml

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

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

撰寫函式處理導覽列和導覽邊欄的切換作業。

ListActivity.kt / setWiderScreenNavigation()

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

ListActivity.java / setWiderScreenNavigation()

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

分割屬性計算機

SplitController 會取得目前運作中的活動分割畫面相關資訊,並提供互動點,讓使用者自訂分割畫面及產生新的分割畫面。

在先前的章節,我們在 XML 檔案的 <SplitPairRule><SplitPlaceHolderRule> 標記中指定 splitRatio 和其他屬性 (或是利用 SplitPairRule.Builder#setDefaultSplitAttributes()SplitPlaceholderRule.Builder#setDefaultSplitAttributes() API),藉此設定了分割畫面的預設屬性。

如果父項容器的 WindowMetrics 滿足 SplitRule 的尺寸規定 (minWidthDpminHeightDpminSmallestWidthDp),系統就會套用預設分割畫面屬性。

我們會設定分割畫面屬性計算機,取代預設分割畫面屬性。視窗或裝置狀態發生變化 (例如螢幕方向或折疊狀態改變) 之後,計算機就會更新現有的分割畫面組合。

開發人員可藉此瞭解裝置或視窗狀態,並在不同的情境 (包括直向/橫向螢幕,以及桌面型態) 中設定不同的分割畫面屬性。

建立分割畫面屬性計算機時,平台會傳遞 SplitAttributesCalculatorParams 物件給 setSplitAttributesCalculator() 函式。parentWindowMetrics 屬性會提供應用程式視窗指標。

在以下程式碼中,活動會檢查是否有滿足預設限制,也就是寬度大於 840dp,最小寬度大於 600dp。若有達到條件要求,系統會將活動嵌入雙窗格版面配置,應用程式會使用導覽邊欄,而非底部導覽列。若是未達條件要求,系統會以全螢幕附加底部導覽列的形式顯示活動。

ListActivity.kt / setSplitAttributesCalculator()

SplitController.getInstance(this).setSplitAttributesCalculator
       params ->

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

ListActivity.java / setSplitAttributesCalculator()

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

很好,現在您的活動嵌入應用程式已經符合 Material Design 導覽指南的規定了!

8. 恭喜!

非常好!您已對以活動為依據的應用程式進行最佳化,調整為適用於大螢幕的清單/詳細資料版面配置,並新增了 Material Design 導覽。

您已瞭解實作活動嵌入功能的兩種方法:

  • 使用 XML 設定檔
  • 發出 Jetpack API 呼叫
  • 透過活動嵌入功能實作彈性導覽

此外,您並未重新編寫任何應用程式的 Kotlin 或 Java 原始碼。

您已做好準備,可以使用活動嵌入功能,針對大螢幕裝置為正式版應用程式進行最佳化調整了!

9. 瞭解詳情