1. はじめに
大きなディスプレイでは、アプリのレイアウトと UI を作成して、ユーザー エクスペリエンスの質とユーザーの生産性を高めることができます。しかし、折りたたみ式ではないスマートフォンの小さなディスプレイ向けに設計されているアプリでは、タブレットや折りたたみ式デバイス、ChromeOS デバイスなどの広いディスプレイ領域を十分に活用できない場合があります。
大きなディスプレイを最大限に活用できるようにアプリをアップデートすると、時間と費用がかかる場合があります。特に、複数のアクティビティに基づく旧式のアプリではその傾向が強くなります。
Android 12L(API レベル 32)で導入されたアクティビティの埋め込みを使用すると、アクティビティベースのアプリで、大きな画面に複数のアクティビティを同時に表示し、リストと詳細などの 2 ペイン レイアウトを作成できます。Kotlin または Java の再コーディングは必要ありません。依存関係の追加、XML 構成ファイルの作成、イニシャライザの実装を行い、アプリ マニフェストに何点か変更を加えます。コードを編集する場合は、アプリのメイン アクティビティの onCreate() メソッドに少数の Jetpack WindowManager API 呼び出しを追加するだけです。
前提条件
この Codelab を完了するには、次の経験が必要です。
- Android アプリのビルド
- アクティビティの操作
- XML の記述
- Android Studio の操作(仮想デバイスの設定など)
作成するアプリの概要
この Codelab では、アクティビティ ベースのアプリを更新して、SlidingPaneLayout に類似した動的な 2 ペイン レイアウトをサポートします。小さい画面の場合、アプリはタスク ウィンドウでアクティビティを重ねて表示(スタック)します。

大きな画面の場合は、アプリは仕様に基づいて、画面上に 2 つのアクティビティを左右または上下に並べて表示します。

学習内容
アクティビティの埋め込みを行う方法は次の 2 つです。
- XML 構成ファイルを使用する
- Jetpack WindowManager API 呼び出しを使用する
必要なもの
- Android Studio の最新バージョン
- Android スマートフォンまたはエミュレータ
- Android 搭載の小型タブレットまたはエミュレータ
- Android 搭載の大型タブレットまたはエミュレータ
2. セットアップ
サンプルアプリを入手する
ステップ 1: リポジトリのクローンを作成する
大きな画面向けの Codelab の Git リポジトリのクローンを作成します。
git clone https://github.com/android/large-screen-codelabs
または、大きな画面向けの Codelab の zip ファイルをダウンロードしてアーカイブ解除します。
ステップ 2: Codelab のソースファイルを調べる
activity-embedding フォルダに移動します。
ステップ 3: Codelab プロジェクトを開く
Android Studio で Kotlin プロジェクトまたは Java プロジェクトを開きます。

リポジトリと zip ファイルの activity-embedding フォルダには、Kotlin 用と Java 用の 2 つの Android Studio プロジェクトが含まれています。いずれかのプロジェクトを開きます。Codelab スニペットはどちらの言語にも用意されています。
仮想デバイスを作成する
API レベル 32 以降を搭載した Android スマートフォン、小型タブレット、大型タブレットをお持ちでない場合は、Android Studio でデバイス マネージャーを開き、次のうち、いずれかの必要な仮想デバイスを作成します。
- スマートフォン - Google Pixel 6、API レベル 32 以降
- 小型タブレット - 7 WSVGA(タブレット)、API レベル 32 以降
- 大型タブレット - Google Pixel C、API レベル 32 以降
3. アプリを実行する
サンプルアプリでアイテムのリストが表示されます。ユーザーがアイテムを選択すると、そのアイテムに関する情報が表示されます。
アプリは次の 3 つのアクティビティで構成されています。
- ListActivity-- RecyclerView- .内のアイテムのリストが含まれます。- ItemAdapterクラスは- RecyclerViewのアイテムのリストを作成します。
- DetailActivity- リストのアイテムが選択されたときに、そのアイテムに関する情報を表示します。
- SummaryActivity- リストアイテムの [Summary] が選択されたときに、概要を表示します。
アクティビティを埋め込まない場合の動作
サンプルアプリを実行して、アクティビティを埋め込まない場合の動作を確認します。
- 大型タブレットまたは Google Pixel C エミュレータでサンプルアプリを実行します。メイン(リスト)アクティビティが表示されます。

- セカンダリ(詳細)アクティビティを起動するリストアイテムを選択します。詳細アクティビティは、リスト アクティビティの上に重ねて表示されます。

- タブレットを回転させて横向きにします。セカンダリ アクティビティは引き続きメイン アクティビティの上に重ねて表示され、ディスプレイ全体を覆います。

- 戻るボタン(アプリバーの左矢印)を選択してリストに戻ります。
- デバイスを回転させて、画面の向きを横向きにします。
- リスト内の最後のアイテムである [Summary] を選択して、概要アクティビティをセカンダリ アクティビティとして起動します。この概要アクティビティは、リスト アクティビティの上に重ねて表示されます。

- タブレットを回転させて横向きにします。セカンダリ アクティビティは引き続きメイン アクティビティの上に重ねて表示され、ディスプレイ全体を覆います。

アクティビティを埋め込んだ場合の動作
この Codelab を終えると、横向きのときに、リストと詳細レイアウトでリスト アクティビティと詳細アクティビティが並んで表示されるようになります。

ただし、アクティビティが分割内から起動された場合でも、概要を全画面表示するよう設定することもできます。概要は分割の上に重ねて表示されます。

4. 背景
アクティビティの埋め込みは、アプリのタスク ウィンドウをプライマリとセカンダリという 2 つのコンテナに分割します。どのアクティビティでも、別のアクティビティを起動することで分割を開始できます。最初のアクティビティがプライマリ コンテナを占有し、起動したアクティビティがセカンダリ コンテナを占有します。
プライマリ アクティビティは、セカンダリ コンテナで追加のアクティビティを起動できます。どちらのコンテナのアクティビティでも、それぞれのコンテナでアクティビティを起動できます。各コンテナには、アクティビティを積み重ねて表示できます。詳しくは、アクティビティの埋め込みに関するデベロッパー ガイドをご覧ください。
アプリの構成でアクティビティの埋め込みをサポートするには、XML 構成ファイルを作成するか、Jetpack WindowManager API 呼び出しを実行します。まず、XML 構成を使う方法から説明します。
5. XML 構成
アクティビティの埋め込みのコンテナと分割は、XML 構成ファイルで作成した分割ルールに基づき、Jetpack WindowManager ライブラリによって作成、管理されます。
WindowManager の依存関係を追加する
ライブラリの依存関係をアプリのモジュール レベルの build.gradle ファイルに追加することで、サンプルアプリが WindowManager ライブラリにアクセスできるようにします。次に例を示します。
build.gradle
 implementation 'androidx.window:window:1.3.0'
システムに知らせる
アプリにアクティビティの埋め込みが実装されていることをシステムに通知します。
android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED プロパティをアプリ マニフェスト ファイルの <application> 要素に追加し、値を 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>
この設定は、アクティビティの埋め込みをサポートするアプリに対して、デバイス メーカー(OEM)がカスタム機能を有効にする場合にも使用します。たとえば、横向きディスプレイに縦向きのみのアクティビティ(android:screenOrientation を参照)をレターボックス表示し、アクティビティの向きを調整し、アクティビティの埋め込みの 2 ペイン レイアウトにスムーズに移行させることができます。

構成ファイルを作成する
アプリの res/xml フォルダに、resources をルート要素とする main_split_config.xml という名前の XML リソース ファイルを作成します。
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- ディスプレイが 2 ペイン ディスプレイから 1 ペイン ディスプレイにサイズが変更されたとき(折りたたみ式デバイスが折りたたまれたときなど)に、最上位のアクティビティとしてプレースホルダを画面上に表示し続ける(false)かどうかを指定します。
- プレースホルダがタスク ウィンドウ分割を共有するアクティビティ(ListActivity)を指定するアクティビティ フィルタが含まれます。
プレースホルダは、プライマリ アクティビティがプレースホルダ アクティビティ フィルタのアクティビティと一致する、分割ペアのルールのセカンダリ アクティビティを表します(詳細はこの Codelab の「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 で次の操作を行います。
- サンプルアプリのソースフォルダ com.example.activity_embeddingを右クリック(セカンダリ ボタンをクリック)します。
- [New] > [Activity] > [Empty Views Activity] を選択します。
- アクティビティに PlaceholderActivity という名前を付けます。
- ソース言語には Kotlin か Java を設定します。
- [Finish] を選択します。
Android Studio は、サンプルアプリのパッケージにアクティビティを作成し、アプリ マニフェスト ファイルにそのアクティビティを追加します。また、res/layout フォルダに activity_placeholder.xml という名前のレイアウト リソース ファイルを作成します。
- Kotlin クラスの場合は enableEdgeToEdge()の呼び出しを削除し、Java クラスの場合はEdgeToEdge.enable(this);を削除します(アプリの他のアクティビティ、特にDetailActivityは、エッジ ツー エッジではありません)。
修正後のクラスファイルは次のようになります。
PlaceholderActivity.kt
package com.example.activity_embedding
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
class PlaceholderActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_placeholder)
    ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) {
      v, insets ->
        val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
        v.setPadding(systemBars.left, 
                     systemBars.top,
                     systemBars.right, 
                     systemBars.bottom)
        insets
    }
  }
}
PlaceholderActivity.java
package com.example.activity_embedding;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
public class PlaceholderActivity extends AppCompatActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_placeholder);
    ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), 
      (v, insets) -> {
        Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
        v.setPadding(systemBars.left, 
                     systemBars.top, 
                     systemBars.right, 
                     systemBars.bottom);
      return insets;
    });
  }
}
- サンプルアプリの AndroidManifest.xmlファイルで、プレースホルダ アクティビティのラベルを空の文字列に設定します。
AndroidManifest.xml
<activity
    android:name=".PlaceholderActivity"
    android:exported="false"
    android:label="" />
- 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:id="@+id/main"
    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>
- 最後に、res/valuesフォルダのstrings.xmlリソース ファイルに次の文字列リソースを追加します。
strings.xml
<string name="placeholder_text">Placeholder</string>
イニシャライザを作成する
WindowManager の RuleController コンポーネントは、XML 構成ファイルで定義されたルールを解析し、システム内でそのルールを使用できるようにします。
Jetpack Startup ライブラリのイニシャライザを使用すると、RuleController が構成ファイルにアクセスできるようになります。
Startup ライブラリは、アプリの起動時にコンポーネントの初期化を実行します。RuleController が分割ルールにアクセスし、必要に応じて適用できるように、アクティビティを開始する前に初期化を実行する必要があります。
Startup ライブラリの依存関係を追加する
起動機能を有効にするため、Startup ライブラリの依存関係をサンプルアプリのモジュール レベルの build.gradle ファイルに追加します。次に例を示します。
build.gradle
implementation 'androidx.startup:startup-runtime:1.2.0'
RuleController のイニシャライザを実装する
Startup Initializer インターフェースの実装を作成します。
Android Studio で次の操作を行います。
- サンプルアプリのソースフォルダ com.example.activity_embeddingを右クリック(セカンダリ ボタンをクリック)します。
- [New] > [Kotlin Class/File] または [New] > [Java Class] を選択します。
- クラスに SplitInitializer という名前を付けます。
- Enter キーを押します。Android Studio によって、サンプルアプリ パッケージ内にクラスが作成されます。
- クラスファイルの内容を次のように置き換えます。
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();
   }
}
イニシャライザは、(main_split_config) 定義を含む XML リソース ファイルの ID をコンポーネントの parseRules() メソッドに渡すことで、RuleController コンポーネントが分割ルールを利用できるようにします。setRules() メソッドは、解析されたルールを RuleController に追加します。
初期化プロバイダを作成する
プロバイダは、分割ルールの初期化プロセスを呼び出します。
サンプルアプリのマニフェスト ファイルの <application> 要素に、androidx.startup.InitializationProvider をプロバイダとして追加し、SplitInitializer を参照します。
AndroidManifest.xml
<provider android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false">
    <!-- 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 のイニシャライザを実装する」をご覧ください)。
InitializationProvider によって SplitInitializer が検出、初期化されてからアプリの onCreate() メソッドが実行されるため、メインのアプリのアクティビティが開始されたときに分割ルールが有効になります。
ソースファイル
完成したアプリ マニフェストは次のようになります。
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
  <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">
    <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">
      <!-- Make SplitInitializer discoverable by InitializationProvider. -->
      <meta-data
          android:name="${applicationId}.SplitInitializer"
          android:value="androidx.startup" />
    </provider>
  </application>
</manifest>
初期化のショートカット
XML 構成と WindowManager API を組み合わせても問題がなければ、Startup ライブラリ イニシャライザとマニフェスト プロバイダを省略して、実装を大幅に簡略化できます。
XML 構成ファイルを作成した後の手順は次のとおりです。
ステップ 1: Application のサブクラスを作成する
アプリのサブクラスが、アプリのプロセスを作成する際にインスタンス化される最初のクラスになります。サブクラスの onCreate() メソッドの RuleController に分割ルールを追加し、アクティビティが起動する前にルールが有効になるようにします。
Android Studio で次の操作を行います。
- サンプルアプリのソースフォルダ com.example.activity_embeddingを右クリック(セカンダリ ボタンをクリック)します。
- [New] > [Kotlin Class/File] または [New] > [Java Class] を選択します。
- クラスに SampleApplication という名前を付けます。
- Enter キーを押します。Android Studio によって、サンプルアプリ パッケージ内にクラスが作成されます。
- 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 を初期化する
アプリケーションのサブクラスの onCreate() メソッドで、XML 構成ファイルの分割ルールを RuleController に追加します。
RuleController にルールを追加する手順は次のとおりです。
- RuleControllerのシングルトン インスタンスを取得します。
- RuleControllerの Java 静的または Kotlin コンパニオン- parseRules()メソッドを使用して、XML ファイルを解析します。
- 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(タブレット)エミュレータでは、縦向き状態で 2 つのアクティビティが積み重ねられて表示されますが、横向き状態では並べて表示されます。
 
         
大型タブレットや Google Pixel C エミュレータの場合、縦向きではアクティビティは積み重ねられて表示されます(下記の「アスペクト比」を参照)。が、横向きでは並べて表示されます。
 
         
概要は、分割内で起動された場合でも全画面表示されます。

アスペクト比
アクティビティの分割は、ディスプレイのアスペクト比と分割の最小幅によって制御されます。splitMaxAspectRatioInPortrait 属性と splitMaxAspectRatioInLandscape 属性によって、アクティビティの分割が表示されるディスプレイの最大アスペクト比(高さ:幅)を指定します。属性は、SplitRule の maxAspectRatioInPortrait プロパティと maxAspectRatioInLandscape プロパティを表します。
いずれかの向きでディスプレイのアスペクト比がこの値を超えると、ディスプレイの幅に関係なく、分割が無効になります。縦向きのデフォルト値は 1.4(高さ / 幅、SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT を参照)です。これにより、縦長のディスプレイで分割が表示されるのを防ぐことができます。デフォルトでは、分割は横向きでは常に許可されます(SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT を参照)。
Google PIxel C エミュレータの縦向きの表示幅は 900 dp です。これは、サンプルアプリの XML 構成ファイルの splitMinWidthDp の設定よりも広いため、エミュレータではアクティビティの分割が表示されます。ただし、Google Pixel C の縦向きのアスペクト比は 1.4 を超えているため、アクティビティの分割が縦向きでは表示されません。
縦向きと横向きでのディスプレイの最大アスペクト比は、XML 構成ファイルの SplitPairRule 要素と SplitPlaceholderRule 要素で設定できます。次に例を示します。
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>
縦向きでのディスプレイの幅が 840 dp 以上の大型タブレットまたは Google Pixel C エミュレータでは、アクティビティは縦向きの場合は並んで表示されますが、横向きの場合は重なって表示されます。
 
         
他のトピック
上の例のように、縦向きと横向きでのサンプルアプリのアスペクト比を設定してみてください。大型タブレット(縦向きの幅が 840 dp 以上)または Google Pixel C エミュレータで設定をテストします。アクティビティの分割が縦向きでは表示されますが、横向きでは表示されないはずです。
大型タブレットの縦向きのアスペクト比(Google Pixel C のアスペクト比は 1.4 より少し大きくなります)を確認します。splitMaxAspectRatioInPortrait を、アスペクト比よりも高い値、低い値に設定します。アプリを実行して、結果を確認します。
6. WindowManager API
分割を開始するアクティビティの onCreate() メソッド内から呼び出される単一のメソッドを使って、コード内でアクティビティの埋め込みを有効にすることができます。XML ではなくコードを編集する方がよい場合は、この方法を使用します。
WindowManager の依存関係を追加する
XML ベースの実装を作成する場合でも API 呼び出しを使用する場合でも、アプリは WindowManager ライブラリにアクセスする必要があります。アプリに WindowManager の依存関係を追加する方法については、この Codelab の「XML 構成」セクションをご覧ください。
システムに知らせる
XML 構成ファイルを使用する場合でも WindowManager API 呼び出しを使用する場合でも、アプリはアクティビティの埋め込みを実装していることをシステムに通知する必要があります。実装をシステムに通知する方法については、この Codelab の「XML 構成」セクションをご覧ください。
分割を管理するクラスを作成する
Codelab のこのセクションでは、単一の静的またはコンパニオン オブジェクト メソッド内のみで、アクティビティ分割を実装します。このメソッドは、サンプルアプリのメイン アクティビティである ListActivity から呼び出します。
context パラメータ(一部の API 呼び出しで必要)を含んだ createSplit というメソッドを持つ SplitManager というクラスを作成します。
SplitManager.kt
import android.content.Context
class SplitManager {
    companion object {
        fun createSplit(context: Context) {
        }
    }
}
SplitManager.java
import android.content.Context
class SplitManager {
    static void createSplit(Context context) {
    }
}
Application クラスのサブクラスの onCreate() メソッドで、メソッドを呼び出します。
Application をサブクラス化する理由と方法について詳しくは、この Codelab の「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 コンポーネントのシングルトン インスタンスを使用して登録します。これにより、分割ルールをシステムで使用できるようになります。
分割ルールを作成する方法は次のとおりです。
- 分割を共有するアクティビティとして ListActivityとDetailActivityを識別する分割ペアのフィルタを作成します。
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
);
このフィルタには、セカンダリ アクティビティ起動のインテントのアクション(3 つ目のパラメータ)を含めることができます。インテントのアクションを含めると、フィルタによって、アクティビティ名とともにそのアクションがチェックされます。実際のアプリのアクティビティでは、インテントのアクションをフィルタしないため、引数を null にしてもかまいません。
- フィルタをフィルタセットに追加します。
SplitManager.kt / createSplit()
val filterSet = setOf(splitPairFilter)
SplitManager.java / createSplit()
Set<SplitPairFilter> filterSet = new HashSet<>();
filterSet.add(splitPairFilter);
- 分割のレイアウト属性を作成します。
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: プライマリ コンテナから順に、アクティビティ コンテナ同士の相対的なレイアウトを指定します。
- 分割ペアのルールを作成します。
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: 分割を共有するアクティビティを識別することで、ルールをいつ適用するか判断する分割ペアのフィルタが含まれます。サンプルアプリでは、分割ペアのフィルタで- ListActivityと- DetailActivityが指定されています(前の手順を参照)。
- setDefaultSplitAttributes: レイアウト属性をルールに適用します。
- setMinWidthDp: 分割を許可する最小表示幅(密度非依存ピクセル、dp)を設定します。
- setMinSmallestWidthDp: デバイスの向きにかかわらず分割を有効にするために、2 つのディスプレイ サイズのうち小さい方が必要とする最小値(dp)を設定します。
- setFinishPrimaryWithSecondary- セカンダリ コンテナ内のすべてのアクティビティを終了したときに、プライマリ コンテナのアクティビティにどのように影響するかを設定します。- NEVERは、セカンダリ コンテナ内のすべてのアクティビティが終了しても、プライマリ アクティビティは終了しないことを示します(アクティビティを終了するをご覧ください)。
- setFinishSecondaryWithPrimary- プライマリ コンテナ内のすべてのアクティビティが終了したときに、セカンダリ コンテナ内のアクティビティをどのように処理するかを設定します。- ALWAYSは、プライマリ コンテナ内のすべてのアクティビティが終了すると、セカンダリ コンテナ内のアクティビティも必ず終了させることを示します(アクティビティを終了するをご覧ください)。
- setClearTop: セカンダリ コンテナ内で新しいアクティビティが起動したときに、そのコンテナ内のすべてのアクティビティを終了するどうかを指定します。False は、セカンダリ コンテナにすでに存在するアクティビティの上に新しいアクティビティが重ねられることを指定します。
- 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 は、セカンダリ コンテナに表示できるコンテンツがない場合に、そのコンテナを占有するアクティビティのルールを定義します。プレースホルダ アクティビティを作成する方法については、この Codelab の「XML 構成」セクションの「プレースホルダ アクティビティを作成する」をご覧ください(詳細については、アクティビティの埋め込みに関するデベロッパー ガイドのプレースホルダをご覧ください)。
SplitPlaceholderRule.Builder は SplitPlaceholderRule を作成します。ビルダーは ActivityFilter オブジェクトのセットを引数として受け取ります。このオブジェクトによって、プレースホルダ ルールが関連付けられるアクティビティを指定します。フィルタが開始されたアクティビティと一致する場合は、プレースホルダ ルールが適用されます。
ルールは RuleController コンポーネントを使用して登録します。
分割プレースホルダ ルールを作成する方法は次のとおりです。
- 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 の起動)のインテントのアクション(2 つ目のパラメータ)を含めることができます。インテントのアクションを含めると、フィルタによって、アクティビティ名とともにそのアクションがチェックされます。実際のアプリのアクティビティでは、インテントのアクションをフィルタしないため、引数を null にしてもかまいません。
- フィルタをフィルタセットに追加します。
SplitManager.kt / createSplit()
val placeholderActivityFilterSet = setOf(placeholderActivityFilter)
SplitManager.java / createSplit()
Set<ActivityFilter> placeholderActivityFilterSet = new HashSet<>();
placeholderActivityFilterSet.add(placeholderActivityFilter);
- 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: デバイスの向きにかかわらず分割を有効にするために、2 つのディスプレイ サイズのうち小さい方が必要とする最小値(dp)を設定します。
- setFinishPrimaryWithPlaceholder: プレースホルダ アクティビティを終了すると、プライマリ コンテナのアクティビティにどのように影響するかを設定します。- ALWAYSは、プレースホルダが終了したときに、必ずプライマリ コンテナのアクティビティも終了させることを示します(アクティビティを終了するをご覧ください)。
- ルールを WindowManager の RuleControllerに追加します。
SplitManager.kt / createSplit()
ruleController.addRule(splitPlaceholderRule)
SplitManager.java / createSplit()
ruleController.addRule(splitPlaceholderRule);
アクティビティ ルールを作成する
必要な API は次のとおりです。
ActivityRule を使用して、モーダル ダイアログなど、タスク ウィンドウ全体を占有するアクティビティのルールを定義できます(詳細については、アクティビティの埋め込みに関するデベロッパー ガイドのフルウィンドウ モーダルをご覧ください)。
ActivityRule.Builder が ActivityRule を作成します。ビルダーは ActivityFilter オブジェクトのセットを引数として受け取ります。このオブジェクトによって、アクティビティ ルールが関連付けられるアクティビティを指定します。フィルタが開始されたアクティビティと一致する場合は、アクティビティ ルールが適用されます。
ルールは RuleController コンポーネントを使用して登録します。
アクティビティ ルールの作成手順は次のとおりです。
- 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 の起動)のインテントのアクション(2 つ目のパラメータ)を含めることができます。インテントのアクションを含めると、フィルタによって、アクティビティ名とともにそのアクションがチェックされます。実際のアプリのアクティビティでは、インテントのアクションをフィルタしないため、引数を null にしてもかまいません。
- フィルタをフィルタセットに追加します。
SplitManager.kt / createSplit()
val summaryActivityFilterSet = setOf(summaryActivityFilter)
SplitManager.java / createSplit()
Set<ActivityFilter> summaryActivityFilterSet = new HashSet<>();
summaryActivityFilterSet.add(summaryActivityFilter);
- 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- アクティビティを利用可能なディスプレイ スペース全体に拡張するかどうかを指定します。
- ルールを WindowManager の RuleControllerに追加します。
SplitManager.kt / createSplit()
ruleController.addRule(activityRule)
SplitManager.java / createSplit()
ruleController.addRule(activityRule);
ソースファイル
完成した SplitManager ファイルは次のようになります。
SplitManager.kt
package com.example.activity_embedding
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import androidx.window.embedding.ActivityFilter
import androidx.window.embedding.ActivityRule
import androidx.window.embedding.EmbeddingAspectRatio
import androidx.window.embedding.RuleController
import androidx.window.embedding.SplitAttributes
import androidx.window.embedding.SplitPairFilter
import androidx.window.embedding.SplitPairRule
import androidx.window.embedding.SplitPlaceholderRule
import androidx.window.embedding.SplitRule
/**
 * Creates activity embedding splits.
 */
class SplitManager {
    companion object {
        fun createSplit(context: Context) {
            val splitPairFilter = SplitPairFilter(
                ComponentName(context, ListActivity::class.java),
                ComponentName(context, DetailActivity::class.java),
                null
            )
            val filterSet = setOf(splitPairFilter)
            val splitAttributes: SplitAttributes = SplitAttributes.Builder()
                  .setSplitType(SplitAttributes.SplitType.ratio(0.33f))
                  .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
                  .build()
            val splitPairRule = SplitPairRule.Builder(filterSet)
                  .setDefaultSplitAttributes(splitAttributes)
                  .setMinWidthDp(840)
                  .setMinSmallestWidthDp(600)
                  .setFinishPrimaryWithSecondary(SplitRule.FinishBehavior.NEVER)
                  .setFinishSecondaryWithPrimary(SplitRule.FinishBehavior.ALWAYS)
                  .setClearTop(false)
                  .build()
            val ruleController = RuleController.getInstance(context)
            ruleController.addRule(splitPairRule)
            val placeholderActivityFilter = ActivityFilter(
                ComponentName(context, ListActivity::class.java),
                null
            )
            val placeholderActivityFilterSet = setOf(placeholderActivityFilter)
            val splitPlaceholderRule = SplitPlaceholderRule.Builder(
                  placeholderActivityFilterSet,
                  Intent(context, PlaceholderActivity::class.java)
                ).setDefaultSplitAttributes(splitAttributes)
                 .setMinWidthDp(840)
                 .setMinSmallestWidthDp(600)
                 .setFinishPrimaryWithPlaceholder(SplitRule.FinishBehavior.ALWAYS)
                 .build()
            ruleController.addRule(splitPlaceholderRule)
            val summaryActivityFilter = ActivityFilter(
                ComponentName(context, SummaryActivity::class.java),
                null
            )
            val summaryActivityFilterSet = setOf(summaryActivityFilter)
            val activityRule = ActivityRule.Builder(summaryActivityFilterSet)
                  .setAlwaysExpand(true)
                  .build()
            ruleController.addRule(activityRule)
        }
    }
}
SplitManager.java
package com.example.activity_embedding;
import android.content.ComponentName;
import android.content.Context;import android.content.Intent;
import androidx.window.embedding.ActivityFilter;
import androidx.window.embedding.ActivityRule;
import androidx.window.embedding.RuleController;
import androidx.window.embedding.SplitAttributes;
import androidx.window.embedding.SplitPairFilter;
import androidx.window.embedding.SplitPairRule;
import androidx.window.embedding.SplitPlaceholderRule;
import androidx.window.embedding.SplitRule;
import java.util.*;
/**
 * Creates activity embedding splits.
 */
class SplitManager   {
    static void createSplit(Context context) {
        SplitPairFilter splitPairFilter = new SplitPairFilter(
            new ComponentName(context, ListActivity.class),
            new ComponentName(context, DetailActivity.class),
            null
        );
        Set<SplitPairFilter> filterSet = new HashSet<>();
        filterSet.add(splitPairFilter);
        SplitAttributes splitAttributes = new SplitAttributes.Builder()
          .setSplitType(SplitAttributes.SplitType.ratio(0.33f))
          .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
          .build();
        SplitPairRule splitPairRule = new SplitPairRule.Builder(filterSet)
          .setDefaultSplitAttributes(splitAttributes)
          .setMinWidthDp(840)
          .setMinSmallestWidthDp(600)
          .setFinishPrimaryWithSecondary(SplitRule.FinishBehavior.NEVER)
          .setFinishSecondaryWithPrimary(SplitRule.FinishBehavior.ALWAYS)
          .setClearTop(false)
          .build();
        RuleController ruleController = RuleController.getInstance(context);
        ruleController.addRule(splitPairRule);
        ActivityFilter placeholderActivityFilter = new ActivityFilter(
            new ComponentName(context, ListActivity.class),
            null
        );
        Set<ActivityFilter> placeholderActivityFilterSet = new HashSet<>();
        placeholderActivityFilterSet.add(placeholderActivityFilter);
        SplitPlaceholderRule splitPlaceholderRule = new SplitPlaceholderRule.Builder(
          placeholderActivityFilterSet,
          new Intent(context, PlaceholderActivity.class)
        ).setDefaultSplitAttributes(splitAttributes)
         .setMinWidthDp(840)
         .setMinSmallestWidthDp(600)
         .setFinishPrimaryWithPlaceholder(SplitRule.FinishBehavior.ALWAYS)
         .build();
        ruleController.addRule(splitPlaceholderRule);
        ActivityFilter summaryActivityFilter = new ActivityFilter(
            new ComponentName(context, SummaryActivity.class),
            null
        );
        Set<ActivityFilter> summaryActivityFilterSet = new HashSet<>();
        summaryActivityFilterSet.add(summaryActivityFilter);
        ActivityRule activityRule = new ActivityRule.Builder(
            summaryActivityFilterSet
        ).setAlwaysExpand(true)
         .build();
        ruleController.addRule(activityRule);
    }
}
実行
サンプルアプリをビルドして実行します。
アプリは、XML 構成ファイルを使用してカスタマイズした場合と同じように動作する必要があります。
この Codelab の「XML 構成」セクションの「実行」をご覧ください。
他のトピック
SplitPairRule.Builder と SplitPlaceholderRule.Builder の setMaxAspectRatioInPortrait メソッドと setMaxAspectRatioInLandscape メソッドを使用して、サンプルアプリでアスペクト比を設定してみましょう。EmbeddingAspectRatio クラスのプロパティとメソッドを使用して値を指定します。次に例を示します。
SplitPairRule.Builder(filterSet)
  . . .
  .setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ratio(1.5f))
  . . .
.build()
大型タブレットまたは Google Pixel C エミュレータを使用して設定をテストします。
大型タブレットの縦向きのアスペクト比(Google Pixel C のアスペクト比は 1.4 より少し大きくなります)を確認します。縦向きの最大アスペクト比を、タブレットまたは Google Pixel C のアスペクト比よりも高い値、低い値に設定します。ALWAYS_ALLOW プロパティと ALWAYS_DISALLOW プロパティを使用してみましょう。
アプリを実行して、結果を確認します。
詳しくは、この Codelab の「XML 構成」セクションの「アスペクト比」をご覧ください。
7. マテリアル デザインのナビゲーション
マテリアル デザインのガイドラインでは、画面サイズごとに異なるナビゲーション コンポーネントが指定されています。たとえば、幅が 840dp 以上の画面ではナビゲーション レール、840dp より小さい画面では下部のナビゲーション バーを使用します。

アクティビティの埋め込みでは、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 に統合し、画面サイズの更新に応じてナビゲーション コンポーネントの変更をトリガーします。
ナビゲーション レイアウトを作成する
まず、ナビゲーション レールとナビゲーション バーの生成に使用するメニューを作成します。
Android Studio で次の操作を行います。
- サンプルアプリのリソース フォルダ resを右クリック(セカンダリ ボタンをクリック)します。
- [New] > [Android Resource File] を選択します。
- クラスに nav_menu という名前を付けます。
- [Resource type] を [Menu] に設定します。
- [Directory name] が menu になっているのを確認します。
- [OK] をクリックすると、Android Studio が 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>
次に、レイアウトにナビゲーション バーとナビゲーション レールを追加します。最初は非表示になるよう visibility を 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: NavigationRailView  = findViewById(R.id.navigationRailView)
   val bottomNav: BottomNavigationView = 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() API と SplitPlaceholderRule.Builder#setDefaultSplitAttributes() API を使用して、分割のデフォルト属性を設定しました。
デフォルトの分割属性は、親コンテナの WindowMetrics が SplitRule の寸法要件(minWidthDp、minHeightDp、minSmallestWidthDp)を満たす場合に適用されます。
ここでは、分割属性計算ツールを設定してデフォルトの分割属性を置き換えます。計算ツールにより、画面の向きや折りたたみの状態が変化するなど、ウィンドウやデバイスの状態が変わると、既存の分割ペアが更新されます。
このため、デベロッパーはデバイスやウィンドウの状態を把握し、縦向きと横向き、テーブルトップ形状など、シナリオごとに異なる分割属性を設定できます。
分割属性計算ツールを作成する場合、プラットフォームは SplitAttributesCalculatorParams オブジェクトを setSplitAttributesCalculator() 関数に渡します。parentWindowMetrics プロパティはアプリ ウィンドウの指標を提供します。
以下のコードで、アクティビティはデフォルトの制約(つまり、幅が 600dp 以上、840dp 未満)を満たしているかどうかをチェックします。条件を満たしている場合、アクティビティはデュアルペイン レイアウトに埋め込まれ、アプリは、ボトム ナビゲーション バーではなく、ナビゲーション レールを使用します。条件を満たしていない場合、アクティビティは全画面表示となり、ボトム ナビゲーション バーが使用されます。
ListActivity.kt / onCreate()
if (WindowSdkExtensions.getInstance().extensionVersion >= 2) {
    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 / onCreate()
if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 2) {
    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 840 dp.
           return new SplitAttributes.Builder()
                          .setSplitType(SplitType.SPLIT_TYPE_EXPAND)
                          .build();
       }
    });
}
実行
お疲れさまでした。これで、アクティビティ埋め込みアプリはマテリアル デザインのナビゲーション ガイドラインに準拠しています。
8. 完了
おめでとうございます。今回は、大きな画面でリストと詳細レイアウトを使用することで、アクティビティ ベースのアプリを最適化し、マテリアル デザインのナビゲーションを追加しました。
アクティビティの埋め込みを実装する方法として、次の 2 つを学びました。
- XML 構成ファイルを使用する
- Jetpack API 呼び出しを実行する
- アクティビティの埋め込みを使用してフレキシブルなナビゲーションを実装する
また、アプリの Kotlin または Java のソースコードは編集しませんでした。
アクティビティの埋め込みを使用して、大画面向けに製品版アプリを最適化できるようになりました。
9. 関連リンク
- デベロッパー ガイド - アクティビティの埋め込み
- リファレンス ドキュメント - androidx.window.embedding
- Codelab - 高度なアクティビティの埋め込み
- サンプルコード - アクティビティの埋め込み
