高度なアクティビティの埋め込み

1. はじめに

Android 12L(API レベル 32)で導入されたアクティビティの埋め込みを使用すると、アクティビティベースのアプリで、大きな画面に複数のアクティビティを同時に表示し、リストと詳細などの 2 ペイン レイアウトを作成できます。

Codelab「アクティビティの埋め込みとマテリアル デザインを使用してリストと詳細レイアウトを作成する」では、XML または Jetpack WindowManager API 呼び出しを使用してリストと詳細レイアウトを作成する方法について説明しました。

この Codelab では、大画面デバイスでのアプリ エクスペリエンスをさらに向上させるアクティビティの埋め込みについて、新しくリリースされた機能をいくつか紹介します。たとえば、ペインの展開、アクティビティの固定、全画面ダイアログを暗くする機能などがあります。

前提条件

学習内容

次のことを行う方法を学習します。

  • ペインの展開を有効にする
  • 分割ウィンドウのいずれかでアクティビティの固定を実装する
  • 全画面表示ダイアログを暗くする

必要なもの

  • Android Studio の最新バージョン
  • Android 15 を搭載した Android スマートフォンまたはエミュレータ
  • 最小幅が 600 dp 超の 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 ファイルのアクティビティ フォルダのファイルリスト。

リポジトリと zip ファイルの activity-embedding-advanced フォルダには、Kotlin 用と Java 用の 2 つの Android Studio プロジェクトが含まれています。いずれかのプロジェクトを開きます。Codelab スニペットはどちらの言語にも用意されています。

仮想デバイスを作成する

API レベル 35 以降を搭載した Android スマートフォン、小型タブレット、大型タブレットをお持ちでない場合は、Android Studio でデバイス マネージャーを開き、次のうち、いずれかの必要な仮想デバイスを作成します。

  • スマートフォン - Google Pixel 8、API レベル 35 以降
  • タブレット - Google Pixel Tablet、API レベル 35 以降

3. アプリを実行する

サンプルアプリでアイテムのリストが表示されます。ユーザーがアイテムを選択すると、そのアイテムに関する情報が表示されます。

アプリは次の 3 つのアクティビティで構成されています。

  • ListActivity - RecyclerView 内のアイテムのリストが含まれます。
  • DetailActivity - リストのアイテムが選択されたときに、そのアイテムに関する情報を表示します。
  • SummaryActivity - リストアイテムの [Summary] が選択されたときに、概要を表示します。

前の Codelab からの継続

Codelab「アクティビティの埋め込みとマテリアル デザインを使用してリストと詳細レイアウトを作成する」では、アクティビティの埋め込みを使用してリストと詳細ビューを備えたアプリを開発し、ナビゲーション レールとボトム ナビゲーション バーの両方を使用してナビゲーションを容易にしました。

  1. 大型タブレットまたは Google Pixel のエミュレータで、アプリをポートレート モードで実行します。メインのリスト画面と下部にナビゲーション バーが表示されます。

74906232acad76f.png

  1. タブレットを横向きにします。ディスプレイが分割され、片側にリスト、もう片側に詳細が表示されます。下部のナビゲーション バーは、縦型のナビゲーション レールに置き換える必要があります。

dc6a7d1c02c49cd4.png

アクティビティを埋め込んだ場合の新機能

デュアルペイン レイアウトをレベルアップする段階に入ります。この Codelab では、ユーザー エクスペリエンスを向上させる新機能を追加します。作成する内容は次のとおりです。

  1. これらのペインを動的にします。ペインの展開を実装し、ユーザーがペインのサイズを変更(または展開)してビューをカスタマイズできるようにします。

2ec5f7fd6df5d8cd.gif

  1. ユーザーが優先順位を設定できるようにします。アクティビティの固定を使用すると、ユーザーは最も重要なタスクを常に画面に表示できるようになります。

980d0033972737ed.gif

  1. 特定のタスクに集中できるようにする場合は、全画面表示を暗くする機能を追加して、気が散る要素を徐々にフェードアウトさせ、ユーザーが最も重要な作業に集中できるようにします。

2d3455e0f8901f95.png

4. ペインの展開

大画面でデュアルペイン レイアウトを使用するユーザーは、多くの場合、分割されたペインの一方に焦点を当てながら、もう一方を画面に表示しておく必要があります。たとえば、一方で記事を読みながら、もう一方でチャットの会話リストを表示しておく場合などです。ユーザーは、1 つのアクティビティに集中できるようにペインのサイズを変更することもよくあります。

こうしたニーズに対応するため、アクティビティの埋め込みに新しい 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 拡張機能の 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 拡張機能 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"

追加した「PIN THIS ACTIVITY」ボタンを含む activity_detail.xml レイアウト ファイルの XML コード全体は次のとおりです。

<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_activityres/values/strings.xml に追加します。

<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. 一度に固定できるアクティビティは 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 拡張機能 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. ベスト プラクティス

ユーザーがデュアルペイン レイアウトをオフにできるようにする

新しいレイアウトへの移行をスムーズにするため、ユーザーがデュアルペインと 1 列表示を切り替えられるようにしましょう。この機能は、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 モードでアプリをレンダリングします。

  • ウィンドウの設定が変更されると、プログラムは分割ウィンドウの制限内に収まっているかどうか(幅が 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 をトリガーするには、現在の属性を無効にする必要があります。そのためには、WindowManager 1.4 より前に ActivityEmbeddingController; から invalidateVisibleActivityStacks() を呼び出します。このメソッドは、

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. 完了

お疲れ様でした。アクティビティの埋め込みと WindowManager を使用した強力な新機能を追加して、アプリを改良できました。ユーザーは Android のバージョンに関係なく、大画面で柔軟かつ直感的に、UI を快適に操作できるようになりました。

9. 関連リンク