進階活動嵌入功能

1. 簡介

Android 12L (API 級別 32) 中推出了活動嵌入功能,可讓以活動為依據的應用程式在大螢幕上同時顯示多個活動,進而建立雙窗格版面配置 (例如清單/詳細資料)。

使用活動嵌入功能和 Material Design 建構清單/詳細資料版面配置」程式碼研究室,介紹了如何使用 XML 或 Jetpack WindowManager API 呼叫來建立清單/詳細資料版面配置。

本程式碼研究室會引導您逐步加入幾項新推出的活動嵌入功能,進一步提升大螢幕裝置上的應用程式體驗。這些功能包括窗格擴展、活動固定和全螢幕對話方塊調暗。

必要條件

課程內容

操作說明:

  • 啟用窗格擴展功能
  • 使用其中一個分割視窗實作活動固定功能
  • 使用全螢幕對話方塊調暗功能

軟硬體需求

  • 最新版 Android Studio
  • 搭載 Android 15 的 Android 手機或模擬器
  • 最小寬度大於 600 dp 的 Android 大型平板電腦或模擬器

2. 設定

取得範例應用程式

步驟 1:複製存放區

複製大螢幕程式碼研究室 Git 存放區:

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

也可以下載並解壓縮大螢幕程式碼研究室的 ZIP 檔案:

下載原始碼

步驟 2:檢查程式碼實驗室的來源檔案

前往 activity-embedding-advanced 資料夾。

步驟 3:開啟程式碼實驗室專案

在 Android Studio 中開啟 Kotlin 或 Java 專案

存放區和 ZIP 檔案中「activity」資料夾的檔案清單。

存放區和 ZIP 檔案中的「activity-embedding-advanced」資料夾含有兩個 Android Studio 專案:一個是 Kotlin 專案,另一個是 Java 專案。開啟您選擇的專案。我們提供了本程式碼研究室的程式碼片段,兩種語言皆有。

建立虛擬裝置

如果您沒有 API 級別 35 以上的 Android 手機、小型平板電腦或大型平板電腦,請開啟 Android Studio 中的裝置管理工具,然後視需要建立下列任一虛擬裝置:

  • 手機:Pixel 8,API 級別 35 以上
  • 平板電腦:Pixel Tablet,API 級別 35 以上

3. 執行應用程式

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

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

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

接續先前的程式碼研究室

在「使用活動嵌入功能和 Material Design 建構清單/詳細資料版面配置」程式碼研究室中,我們開發了一個應用程式,其中的清單/詳細資料檢視畫面使用活動嵌入功能,並透過導覽邊欄和底部導覽列提供導覽功能。

  1. 在大型平板電腦或 Pixel 模擬器上,以直向模式執行應用程式。您會看到主要清單畫面和底部的導覽列。

74906232acad76f.png

  1. 將平板電腦轉為橫向,即可顯示分割畫面,一側列出清單,另一側顯示詳細資料。底部的導覽列應會替換為垂直導覽邊欄。

dc6a7d1c02c49cd4.png

運用活動嵌入的新功能

準備好升級雙窗格版面配置了嗎?在本程式碼研究室中,我們將新增一些很酷的新功能,提升使用者體驗。以下是我們要建構的內容:

  1. 讓我們製作動態窗格!我們會實作窗格擴展功能,讓使用者能夠調整 (或擴展) 窗格大小,自訂檢視畫面。

2ec5f7fd6df5d8cd.gif

  1. 讓使用者自行決定優先順序!透過活動固定功能,使用者可以讓最重要的工作持續顯示在畫面上。

980d0033972737ed.gif

  1. 想專注於特定工作嗎?我們會新增全螢幕調暗功能,平緩地減少干擾,讓使用者能專注於至關緊要的工作。

2d3455e0f8901f95.png

4. 窗格擴展功能

在大螢幕上使用雙窗格版面配置時,使用者常會需要專注於其中一個分割窗格,同時將另一個窗格保留在畫面上,例如在一側閱讀文章,同時在另一側保留即時通訊對話清單。使用者通常會想調整窗格大小,專注於單一活動。

為達成這個目標,活動嵌入功能新增了一個 API,可用來讓使用者變更分割比例,並自訂調整大小的轉換效果。

新增依附元件

首先,在 build.gradle 檔案中新增 WindowManager 1.4。

注意:這個程式庫中的部分功能僅適用於 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. 活動固定功能

您是否曾想要固定分割畫面中的某個部分,同時在其他部分任意瀏覽?舉例來說,您可以一邊閱讀長篇文章,一邊與其他應用程式內容互動。

這時活動固定功能就能派上用場!您可以「固定」其中一個分割視窗,即使在其他視窗中瀏覽,該視窗仍會保留在畫面上。這可為使用者提供更專注且更有效率的多工處理體驗。

新增固定按鈕

首先,讓我們在 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」按鈕:

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

res/values/strings.xml 中新增 pin_this_activity 字串。

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

連結固定按鈕

  1. 宣告變數:DetailActivity.kt 檔案中宣告一個變數,用於存放「PIN THIS 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. 一次只能固定一個活動。先使用以下函式取消固定目前已固定的活動
unpinTopActivityStack()

再固定其他活動。

  1. 如果同時呼叫
setDividerAttributes()

用於新建立的

SplitAttributes

則可在固定活動時啟用窗格擴展功能。

返回瀏覽的異動

WindowManager 1.4 中的返回導覽行為已變更。使用按鈕操作時,系統會將返回事件傳送至上一個聚焦的活動。

按鈕操作:

  • 使用按鈕操作時,系統一律會將返回事件傳送至「上一個聚焦的活動」。這可簡化返回導覽行為,讓使用者更容易預測。

手勢操作:

  • Android 14 (API 級別 34) 以下版本:返回手勢會將事件傳送至發生手勢的活動,這可能會導致在分割畫面情境中產生意外行為。
  • Android 15 (API 級別 35) 以上版本:
  • 同一個應用程式的活動:無論滑動方向為何,返回手勢一律會結束「頂層活動」,提供更統一的使用體驗。
  • 不同應用程式的活動 (疊加):返回事件會傳送至「最後一個聚焦的活動」,與按鈕操作的行為一致。

開始執行!

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

固定活動

  • 前往 DetailActivity 畫面。
  • 輕觸「PIN THIS ACTIVITY」按鈕。

980d0033972737ed.gif

6. 全螢幕對話方塊調暗功能

雖然活動嵌入功能提升了分割畫面版面配置的體驗,但先前版本中的對話方塊只會將自身活動的容器調暗。這可能會造成視覺體驗不連貫,尤其是當您希望對話方塊成為焦點時。

解決方案:WindowManager 1.4

  • 沒問題,我們都準備好了!在 WindowManager 1.4 中,對話方塊現在會預設將整個應用程式視窗調暗 (DimAreaBehavior.Companion.ON_TASK),提供沈浸其中的專注感受。
  • 需要恢復舊版行為嗎?沒問題!您仍可以選擇使用 ON_ACTIVITY_STACK,只將活動容器調暗。

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

為了展示全螢幕調暗功能,我們會採用一個快訊對話方塊,提示使用者確認要固定活動。這個對話方塊一出現,整個應用程式視窗都會變暗,不只是活動所在的容器而已。

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>

開始執行!

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

按一下固定活動按鈕:

  • 系統隨即顯示快訊對話方塊,提示您確認固定的動作。
  • 請注意,整個畫面 (包括兩個分割窗格) 都會變暗,讓使用者將注意力集中在對話方塊上。

2d3455e0f8901f95.png

7. 最佳做法

允許使用者關閉雙窗格版面配置

為了能更順暢地轉換至新的版面配置,我們要讓使用者能夠切換雙窗格和單欄檢視畫面,方法是使用 SplitAttributesCalculatorSharedPreferences 儲存使用者偏好設定。

在 Android 14 以下版本變更分割比例

我們已瞭解窗格擴展功能,這項功能可讓使用者在 Android 15 以上版本中調整分割比例。但是,如何才能為舊版 Android 使用者提供類似的彈性?

讓我們深入探討如何使用 SplitAttributesCalculator 達成這項目標,並確保能在多種裝置上提供一致的體驗。

以下範例展示這種機制的運作情形:

a87452341434c86d.gif

建立設定畫面

首先,我們來建立專屬的設定畫面,供使用者進行設定。

在這個設定畫面中,我們會加入切換鈕,用於啟用或停用整個應用程式的活動嵌入功能。此外,我們也會加入進度列,讓使用者調整雙窗格版面配置的分割比例。請注意,只有在活動嵌入切換鈕處於開啟狀態時,系統才會套用分割比例值。

使用者在 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 模式下顯示應用程式。

  • 視窗設定變更時,程式會檢查是否符合分割視窗限制條件 (寬度是否大於 840 dp)
  • 應用程式會檢查 SharedPreferences 值,判斷使用者是否已啟用分割視窗顯示設定,否則會傳回 SplitAttributeSPLIT_TYPE_EXPAND 類型。
  • 如果已啟用分割視窗,應用程式會讀取 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();
}

開始執行!

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

檢查設定:

  • 前往設定畫面。
  • 開啟或關閉「Enable Split Window」切換鈕。
  • 調整分割比例滑桿 (如果裝置支援)。

觀察版面配置變更:

  • 搭載 Android 14 以下版本的裝置:版面配置應根據切換鈕設定,切換單一窗格模式與雙窗格模式,並在您調整滑桿時變更分割比例。
  • 搭載 Android 15 以上版本的裝置:無論滑桿設定為何,您都應該可以透過窗格擴展功能,以動態方式調整窗格大小。

8. 恭喜!

非常好!您已成功使用活動嵌入功能和 WindowManager,為應用程式增添強大的新功能。無論使用者採用哪個 Android 版本,現在都能在大螢幕上享有更靈活、直覺且引人入勝的使用體驗。

9. 瞭解詳情