アクティビティの埋め込みを使用してリストと詳細レイアウトを作成する

1. 概要

大きなディスプレイでは、アプリのレイアウトと UI を作成して、ユーザー エクスペリエンスの質とユーザーの生産性を高めることができます。しかし、折りたたみ式ではないスマートフォンの小さなディスプレイ向けに設計されているアプリでは、タブレットや折りたたみ式デバイス、ChromeOS デバイスなどの広いディスプレイ領域を十分に活用できない場合があります。

大きなディスプレイを最大限に活用できるようにアプリをアップデートすると、時間と費用がかかる場合があります。特に、複数のアクティビティに基づく旧式のアプリではその傾向が強くなります。

Android 12L(API レベル 32)で導入されたアクティビティの埋め込みを使用すると、アクティビティベースのアプリで、大きな画面に複数のアクティビティを同時に表示し、リストと詳細などの 2 ペイン レイアウトを作成できます。Kotlin または Java の再コーディングは必要ありません。依存関係の追加、XML 構成ファイルの作成、イニシャライザの実装を行い、アプリ マニフェストに何点か変更を加えます。コードを編集する場合は、アプリのメイン アクティビティの onCreate() メソッドに少数の Jetpack WindowManager API 呼び出しを追加するだけです。

前提条件

この Codelab を完了するには、次の経験が必要です。

  • Android アプリのビルド
  • アクティビティの操作
  • XML の記述
  • Android Studio の操作(仮想デバイスの設定など)

作成するアプリの概要

この Codelab では、アクティビティ ベースのアプリを更新して、SlidingPaneLayout に類似した動的な 2 ペイン レイアウトをサポートします。小さい画面の場合、アプリはタスク ウィンドウでアクティビティを重ねて表示(スタック)します。

タスク ウィンドウ内でアクティビティ A、B、C が積み重ねられている様子。

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

7533496502081c0c.png

学習内容

アクティビティの埋め込みを行う方法は次の 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 ファイルのアクティビティ フォルダのファイルリスト。

リポジトリと 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 内のアイテムのリストが含まれます。
  • DetailActivity - リストのアイテムが選択されたときに、そのアイテムに関する情報を表示します。
  • SummaryActivity - リストアイテムの [Summary] が選択されたときに、概要を表示します。

アクティビティを埋め込まない場合の動作

サンプルアプリを実行して、アクティビティを埋め込まない場合の動作を確認します。

  1. 大型タブレットまたは Google Pixel C エミュレータでサンプルアプリを実行します。メイン(リスト)アクティビティが表示されます。

サンプルアプリが縦向きで実行されている大型タブレット。リスト アクティビティを全画面表示しています。

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

サンプルアプリが縦向きで実行されている大型タブレット。詳細アクティビティを全画面表示しています。

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

サンプルアプリが横向きで実行されている大型タブレット。詳細アクティビティを全画面表示しています。

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

サンプルアプリが縦向きで実行されている大型タブレット。概要アクティビティが全画面表示されています。

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

サンプルアプリが横向きで実行されている大型タブレット。概要アクティビティが全画面表示されています。

アクティビティを埋め込んだ場合の動作

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

サンプルアプリが横向きで実行されている大型タブレット。リストアクティビティと詳細アクティビティを、リストと詳細レイアウトで表示しています。

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

サンプルアプリが横向きで実行されている大型タブレット。概要アクティビティが全画面表示されています。

4. 背景

アクティビティの埋め込みは、アプリのタスク ウィンドウをプライマリとセカンダリという 2 つのコンテナに分割します。どのアクティビティでも、別のアクティビティを起動することで分割を開始できます。最初のアクティビティがプライマリ コンテナを占有し、起動したアクティビティがセカンダリ コンテナを占有します。

プライマリ アクティビティは、セカンダリ コンテナで追加のアクティビティを起動できます。どちらのコンテナのアクティビティでも、それぞれのコンテナでアクティビティを起動できます。各コンテナには、アクティビティを積み重ねて表示できます。詳しくは、アクティビティの埋め込みに関するデベロッパー ガイドをご覧ください。

アプリの構成でアクティビティの埋め込みをサポートするには、XML 構成ファイルを作成するか、Jetpack WindowManager API 呼び出しを実行します。まず、XML 構成を使う方法から説明します。

5. XML 構成

アクティビティの埋め込みのコンテナと分割は、XML 構成ファイルで作成した分割ルールに基づき、Jetpack WindowManager ライブラリによって作成、管理されます。

WindowManager の依存関係を追加する

ライブラリの依存関係をアプリのモジュール レベルの build.gradle ファイルに追加することで、サンプルアプリが WindowManager ライブラリにアクセスできるようにします。次に例を示します。

build.gradle

 implementation 'androidx.window:window:1.1.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 ペイン レイアウトにスムーズに移行させることができます。

横向きディスプレイに、縦向き専用アプリのアクティビティの埋め込みを表示しています。レターボックス表示の縦向き専用アクティビティ A によって、埋め込みアクティビティ B が起動しています。

構成ファイルを作成する

アプリの 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 で次の操作を行います。

  1. サンプルアプリのソースフォルダ com.example.activity_embedding を右クリック(セカンダリ ボタンをクリック)します。
  2. [New] > [Activity] > [Empty Views Activity] を選択します。
  3. アクティビティに PlaceholdActivity という名前を付けます。
  4. [Finish] を選択します。

Android Studio は、サンプルアプリのパッケージにアクティビティを作成し、アプリ マニフェスト ファイルにそのアクティビティを追加します。また、res/layout フォルダに activity_placeholder.xml という名前のレイアウト リソース ファイルを作成します。

  1. サンプルアプリの AndroidManifest.xml ファイルで、プレースホルダ アクティビティのラベルを空の文字列に設定します。

AndroidManifest.xml

<activity
    android:name=".PlaceholderActivity"
    android:exported="false"
    android:label="" />
  1. res/layout フォルダの activity_placeholder.xml レイアウト ファイルの内容を次のように置き換えます。

activity_placeholder.xml

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

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

</androidx.constraintlayout.widget.ConstraintLayout>
  1. 最後に、res/values フォルダの strings.xml リソース ファイルに次の文字列リソースを追加します。

strings.xml

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

イニシャライザを作成する

WindowManager の RuleController コンポーネントは、XML 構成ファイルで定義されたルールを解析し、システム内でそのルールを使用できるようにします。

Jetpack Startup ライブラリのイニシャライザを使用すると、RuleController が構成ファイルにアクセスできるようになります。

Startup ライブラリは、アプリの起動時にコンポーネントの初期化を実行します。RuleController が分割ルールにアクセスし、必要に応じて適用できるように、アクティビティを開始する前に初期化を実行する必要があります。

Startup ライブラリの依存関係を追加する

起動機能を有効にするため、Startup ライブラリの依存関係をサンプルアプリのモジュール レベルの build.gradle ファイルに追加します。次に例を示します。

build.gradle

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

RuleController のイニシャライザを実装する

Startup Initializer インターフェースの実装を作成します。

Android Studio で次の操作を行います。

  1. サンプルアプリのソースフォルダを右クリック(セカンダリ ボタンをクリック)します。
  2. [New] > [Kotlin Class/File] または [New] > [Java Class] を選択します。
  3. クラスに SplitInitializer という名前を付けます。
  4. Enter キーを押します。Android Studio によって、サンプルアプリ パッケージ内にクラスが作成されます。
  5. クラスファイルの内容を次のように置き換えます。

SplitInitializer.kt

package com.example.activity_embedding

import android.content.Context
import androidx.startup.Initializer
import androidx.window.embedding.RuleController

class SplitInitializer : Initializer<RuleController> {

  override fun create(context: Context): RuleController {
    return RuleController.getInstance(context).apply {
      setRules(RuleController.parseRules(context, R.xml.main_split_config))
    }
  }

  override fun dependencies(): List<Class<out Initializer<*>>> {
    return emptyList()
  }
}

SplitInitializer.java

package com.example.activity_embedding;

import android.content.Context;
import androidx.annotation.NonNull;
import androidx.startup.Initializer;
import androidx.window.embedding.RuleController;
import java.util.Collections;
import java.util.List;

public class SplitInitializer implements Initializer<RuleController> {

   @NonNull
   @Override
   public RuleController create(@NonNull Context context) {
      RuleController ruleController = RuleController.getInstance(context);
      ruleController.setRules(
          RuleController.parseRules(context, R.xml.main_split_config)
      );
      return ruleController;
   }

   @NonNull
   @Override
   public List<Class<? extends Initializer<?>>> dependencies() {
       return Collections.emptyList();
   }
}

イニシャライザは、(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"
    tools:node="merge">
    <!-- Make SplitInitializer discoverable by InitializationProvider. -->
    <meta-data android:name="${applicationId}.SplitInitializer"
        android:value="androidx.startup" />
</provider>

InitializationProvider によって SplitInitializer が初期化されてから、XML 構成ファイル(main_split_config.xml)を解析する RuleController メソッドが呼び出され、RuleController にルールが追加されます(前述の「RuleController のイニシャライザを実装する」をご覧ください)。

InitializationProvider によって SplitInitializer が検出、初期化されてからアプリの onCreate() メソッドが実行されるため、メインのアプリのアクティビティが開始されたときに分割ルールが有効になります。

ソースファイル

完成したアプリ マニフェストは次のようになります。

AndroidManifest.xml

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

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

</manifest>

初期化のショートカット

XML 構成と WindowManager API を組み合わせても問題がなければ、Startup ライブラリ イニシャライザとマニフェスト プロバイダを省略して、実装を大幅に簡略化できます。

XML 構成ファイルを作成した後の手順は次のとおりです。

ステップ 1: Application のサブクラスを作成する

アプリのサブクラスが、アプリのプロセスを作成する際にインスタンス化される最初のクラスになります。サブクラスの onCreate() メソッドの RuleController に分割ルールを追加し、アクティビティが起動する前にルールが有効になるようにします。

Android Studio で次の操作を行います。

  1. サンプルアプリのソースフォルダを右クリック(セカンダリ ボタンをクリック)します。
  2. [New] > [Kotlin Class/File] または [New] > [Java Class] を選択します。
  3. クラスに SampleApplication という名前を付けます。
  4. Enter キーを押します。Android Studio によって、サンプルアプリ パッケージ内にクラスが作成されます。
  5. Application スーパータイプからクラスを拡張します。

SampleApplication.kt

package com.example.activity_embedding

import android.app.Application

/**
 * Initializer for activity embedding split rules.
 */
class SampleApplication : Application() {

}

SampleApplication.java

package com.example.activity_embedding;

import android.app.Application;

/**
 * Initializer for activity embedding split rules.
 */
public class SampleApplication extends Application {

}

ステップ 2: RuleController を初期化する

アプリケーションのサブクラスの onCreate() メソッドで、XML 構成ファイルの分割ルールを RuleController に追加します。

RuleController にルールを追加する手順は次のとおりです。

  1. RuleController のシングルトン インスタンスを取得します。
  2. RuleController の Java 静的または Kotlin コンパニオン parseRules() メソッドを使用して、XML ファイルを解析します。
  3. setRules() メソッドを使用して、解析されたルールを RuleController に追加します。

SampleApplication.kt

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

SampleApplication.java

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

ステップ 3: サブクラス名をマニフェストに追加する

サブクラスの名前をアプリ マニフェストの <application> 要素に追加します。

AndroidManifest.xml

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

実行

サンプルアプリをビルドして実行します。

折りたたみ式ではないスマートフォンでは、横向きであっても、アクティビティは常に重なって表示されます。

縦向きのスマートフォン上で、リスト(メイン)アクティビティの上に重ねて表示されている詳細(セカンダリ)アクティビティ。 横向きのスマートフォン上で、リスト(メイン)アクティビティの上に重ねて表示されている詳細(セカンダリ)アクティビティ。

Android 13(API レベル 33)以前では、分割の最小幅の仕様にかかわらず、折りたたみ式ではないスマートフォンではアクティビティの埋め込みが有効ではありません。

API レベルがそれより上位の折りたたみ式ではないスマートフォンでアクティビティの埋め込みがサポートされているかどうかは、デバイス メーカーがアクティビティの埋め込みを有効にしているかどうかによって異なります。

小型タブレットや 7 WSVGA(タブレット)エミュレータでは、縦向き状態で 2 つのアクティビティが積み重ねられて表示されますが、横向き状態では並べて表示されます。

縦向き状態の小型タブレットで、積み重ねられて表示されているリスト アクティビティと詳細アクティビティ。 横向き状態の小型タブレットで、並んで表示されているリスト アクティビティと詳細アクティビティ。

大型タブレットや Google Pixel C エミュレータの場合、縦向きではアクティビティは積み重ねられて表示されます(下記の「アスペクト比」を参照)。が、横向きでは並べて表示されます。

縦向き状態の大型タブレットで、積み重ねられて表示されているリスト アクティビティと詳細アクティビティ。 横向き状態の大型タブレットで、並んで表示されているリスト アクティビティと詳細アクティビティ。

概要は、分割内で起動された場合でも全画面表示されます。

横向き状態の大型タブレットで、分割の上に重ねて表示される概要アクティビティ。

アスペクト比

アクティビティの分割は、ディスプレイのアスペクト比と分割の最小幅によって制御されます。splitMaxAspectRatioInPortrait 属性と splitMaxAspectRatioInLandscape 属性によって、アクティビティの分割が表示されるディスプレイの最大アスペクト比(高さ:幅)を指定します。属性は、SplitRulemaxAspectRatioInPortrait プロパティと 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 パラメータを含む createSplit という名前のメソッドを持つ、SplitManager という名前のクラスを作成します(一部の API 呼び出しにはパラメータが必要です)。

SplitManager.kt

class SplitManager {

    companion object {

        fun createSplit(context: Context) {
        }
}

SplitManager.java

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.BuilderSplitPairRule を作成します。ビルダーは SplitPairFilter オブジェクトのセットを引数として受け取ります。フィルタでは、ルールを適用するタイミングを指定します。

ルールは、RuleController コンポーネントのシングルトン インスタンスを使用して登録します。これにより、分割ルールをシステムで使用できるようになります。

分割ルールを作成する方法は次のとおりです。

  1. 分割を共有するアクティビティとして ListActivityDetailActivity を識別する分割ペアのフィルタを作成します。

SplitManager.kt / createSplit()

val splitPairFilter = SplitPairFilter(
    ComponentName(context, ListActivity::class.java),
    ComponentName(context, DetailActivity::class.java),
    null
)

SplitManager.java / createSplit()

SplitPairFilter splitPairFilter = new SplitPairFilter(
    new ComponentName(context, ListActivity.class),
    new ComponentName(context, DetailActivity.class),
    null
);

このフィルタには、セカンダリ アクティビティ起動のインテントのアクション(3 つ目のパラメータ)を含めることができます。インテントのアクションを含めると、フィルタによって、アクティビティ名とともにそのアクションがチェックされます。実際のアプリのアクティビティでは、インテントのアクションをフィルタしないため、引数を null にしてもかまいません。

  1. フィルタをフィルタセットに追加します。

SplitManager.kt / createSplit()

val filterSet = setOf(splitPairFilter)

SplitManager.java / createSplit()

Set<SplitPairFilter> filterSet = new HashSet<>();
filterSet.add(splitPairFilter);
  1. 分割のレイアウト属性を作成します。

SplitManager.kt / createSplit()

val splitAttributes: SplitAttributes = SplitAttributes.Builder()
      .setSplitType(SplitAttributes.SplitType.ratio(0.33f))
      .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
      .build()

SplitManager.java / createSplit()

SplitAttributes splitAttributes = new SplitAttributes.Builder()
  .setSplitType(SplitAttributes.SplitType.ratio(0.33f))
  .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
  .build();

SplitAttributes.Builder は、レイアウト属性を含むオブジェクトを作成します。

  • setSplitType: 利用可能なディスプレイ領域を、各アクティビティ コンテナにどのように割り当てるかを定義します。比率の分割タイプでは、プライマリ コンテナが占有するディスプレイの割合を指定します。セカンダリ コンテナは残りのディスプレイ領域を占有します。
  • setLayoutDirection: プライマリ コンテナから順に、アクティビティ コンテナ同士の相対的なレイアウトを指定します。
  1. 分割ペアのルールを作成します。

SplitManager.kt / createSplit()

val splitPairRule = SplitPairRule.Builder(filterSet)
      .setDefaultSplitAttributes(splitAttributes)
      .setMinWidthDp(840)
      .setMinSmallestWidthDp(600)
      .setFinishPrimaryWithSecondary(SplitRule.FinishBehavior.NEVER)
      .setFinishSecondaryWithPrimary(SplitRule.FinishBehavior.ALWAYS)
      .setClearTop(false)
      .build()

SplitManager.java / createSplit()

SplitPairRule splitPairRule = new SplitPairRule.Builder(filterSet)
  .setDefaultSplitAttributes(splitAttributes)
  .setMinWidthDp(840)
  .setMinSmallestWidthDp(600)
  .setFinishPrimaryWithSecondary(SplitRule.FinishBehavior.NEVER)
  .setFinishSecondaryWithPrimary(SplitRule.FinishBehavior.ALWAYS)
  .setClearTop(false)
  .build();

SplitPairRule.Builder はルールを作成して構成します。

  • filterSet: 分割を共有するアクティビティを識別することで、ルールをいつ適用するか判断する分割ペアのフィルタが含まれます。サンプルアプリでは、分割ペアのフィルタで ListActivityDetailActivity が指定されています(前の手順を参照)。
  • setDefaultSplitAttributes: レイアウト属性をルールに適用します。
  • setMinWidthDp: 分割を許可する最小表示幅(密度非依存ピクセル、dp)を設定します。
  • setMinSmallestWidthDp: デバイスの向きにかかわらず分割を有効にするために、2 つのディスプレイ サイズのうち小さい方が必要とする最小値(dp)を設定します。
  • setFinishPrimaryWithSecondary - セカンダリ コンテナ内のすべてのアクティビティを終了したときに、プライマリ コンテナのアクティビティにどのように影響するかを設定します。NEVER は、セカンダリ コンテナ内のすべてのアクティビティが終了しても、プライマリ アクティビティは終了しないことを示します(アクティビティを終了するをご覧ください)。
  • setFinishSecondaryWithPrimary - プライマリ コンテナ内のすべてのアクティビティが終了したときに、セカンダリ コンテナ内のアクティビティをどのように処理するかを設定します。ALWAYS は、プライマリ コンテナ内のすべてのアクティビティが終了すると、セカンダリ コンテナ内のアクティビティも必ず終了させることを示します(アクティビティを終了するをご覧ください)。
  • setClearTop: セカンダリ コンテナ内で新しいアクティビティが起動したときに、そのコンテナ内のすべてのアクティビティを終了するどうかを指定します。False は、セカンダリ コンテナにすでに存在するアクティビティの上に新しいアクティビティが重ねられることを指定します。
  1. WindowManager の RuleController のシングルトン インスタンスを取得し、ルールを追加します。

SplitManager.kt / createSplit()

val ruleController = RuleController.getInstance(context)
ruleController.addRule(splitPairRule)

SplitManager.java / createSplit()

RuleController ruleController = RuleController.getInstance(context);
ruleController.addRule(splitPairRule);

プレースホルダ ルールを作成する

必要な API は次のとおりです。

SplitPlaceholderRule は、セカンダリ コンテナに表示できるコンテンツがない場合に、そのコンテナを占有するアクティビティのルールを定義します。プレースホルダ アクティビティを作成する方法については、この Codelab の「XML 構成」セクションの「プレースホルダ アクティビティを作成する」をご覧ください(詳細については、アクティビティの埋め込みに関するデベロッパー ガイドのプレースホルダをご覧ください)。

SplitPlaceholderRule.BuilderSplitPlaceholderRule を作成します。ビルダーは ActivityFilter オブジェクトのセットを引数として受け取ります。このオブジェクトによって、プレースホルダ ルールが関連付けられるアクティビティを指定します。フィルタが開始されたアクティビティと一致する場合は、プレースホルダ ルールが適用されます。

ルールは RuleController コンポーネントを使用して登録します。

分割プレースホルダ ルールを作成する方法は次のとおりです。

  1. ActivityFilter を作成します。

SplitManager.kt / createSplit()

val placeholderActivityFilter = ActivityFilter(
    ComponentName(context, ListActivity::class.java),
    null
)

SplitManager.java / createSplit()

ActivityFilter placeholderActivityFilter = new ActivityFilter(
    new ComponentName(context, ListActivity.class),
    null
);

フィルタによって、ルールがサンプルアプリのメイン アクティビティ ListActivity に関連付けられます。そのため、リストと詳細レイアウトで使用できる詳細コンテンツがない場合は、プレースホルダが詳細領域全体に表示されます。

このフィルタには、関連付けられているアクティビティ起動(ListActivity の起動)のインテントのアクション(2 つ目のパラメータ)を含めることができます。インテントのアクションを含めると、フィルタによって、アクティビティ名とともにそのアクションがチェックされます。実際のアプリのアクティビティでは、インテントのアクションをフィルタしないため、引数を null にしてもかまいません。

  1. フィルタをフィルタセットに追加します。

SplitManager.kt / createSplit()

val placeholderActivityFilterSet = setOf(placeholderActivityFilter)

SplitManager.java / createSplit()

Set<ActivityFilter> placeholderActivityFilterSet = new HashSet<>();
placeholderActivityFilterSet.add(placeholderActivityFilter);
  1. SplitPlaceholderRule を作成します。

SplitManager.kt / createSplit()

val splitPlaceholderRule = SplitPlaceholderRule.Builder(
      placeholderActivityFilterSet,
      Intent(context, PlaceholderActivity::class.java)
    ).setDefaultSplitAttributes(splitAttributes)
     .setMinWidthDp(840)
     .setMinSmallestWidthDp(600)
     .setFinishPrimaryWithPlaceholder(SplitRule.FinishBehavior.ALWAYS)
     .build()

SplitManager.java / createSplit()

SplitPlaceholderRule splitPlaceholderRule = new SplitPlaceholderRule.Builder(
  placeholderActivityFilterSet,
  new Intent(context, PlaceholderActivity.class)
).setDefaultSplitAttributes(splitAttributes)
 .setMinWidthDp(840)
 .setMinSmallestWidthDp(600)
 .setFinishPrimaryWithPlaceholder(SplitRule.FinishBehavior.ALWAYS)
 .build();

SplitPlaceholderRule.Builder はルールを作成して構成します。

  • placeholderActivityFilterSet: プレースホルダ アクティビティが関連付けられているアクティビティを特定することで、いつルールを適用するかを判断するアクティビティ フィルタが含まれます。
  • Intent: プレースホルダ アクティビティの起動を指定します。
  • setDefaultSplitAttributes: レイアウト属性をルールに適用します。
  • setMinWidthDp: 分割を許可する最小表示幅(密度非依存ピクセル、dp)を設定します。
  • setMinSmallestWidthDp: デバイスの向きにかかわらず分割を有効にするために、2 つのディスプレイ サイズのうち小さい方が必要とする最小値(dp)を設定します。
  • setFinishPrimaryWithPlaceholder: プレースホルダ アクティビティを終了すると、プライマリ コンテナのアクティビティにどのように影響するかを設定します。ALWAYS は、プレースホルダが終了したときに、必ずプライマリ コンテナのアクティビティも終了させることを示します(アクティビティを終了するをご覧ください)。
  1. ルールを WindowManager の RuleController に追加します。

SplitManager.kt / createSplit()

ruleController.addRule(splitPlaceholderRule)

SplitManager.java / createSplit()

ruleController.addRule(splitPlaceholderRule);

アクティビティ ルールを作成する

必要な API は次のとおりです。

ActivityRule を使用して、モーダル ダイアログなど、タスク ウィンドウ全体を占有するアクティビティのルールを定義できます(詳細については、アクティビティの埋め込みに関するデベロッパー ガイドのフルウィンドウ モーダルをご覧ください)。

SplitPlaceholderRule.BuilderSplitPlaceholderRule を作成します。ビルダーは ActivityFilter オブジェクトのセットを引数として受け取ります。このオブジェクトによって、プレースホルダ ルールが関連付けられるアクティビティを指定します。フィルタが開始されたアクティビティと一致する場合は、プレースホルダ ルールが適用されます。

ルールは RuleController コンポーネントを使用して登録します。

アクティビティ ルールの作成手順は次のとおりです。

  1. ActivityFilter を作成します。

SplitManager.kt / createSplit()

val summaryActivityFilter = ActivityFilter(
    ComponentName(context, SummaryActivity::class.java),
    null
)

SplitManager.java / createSplit()

ActivityFilter summaryActivityFilter = new ActivityFilter(
    new ComponentName(context, SummaryActivity.class),
    null
);

フィルタでは、ルールが適用されるアクティビティ(SummaryActivity)を指定します。

このフィルタには、関連付けられているアクティビティ起動(SummaryActivity の起動)のインテントのアクション(2 つ目のパラメータ)を含めることができます。インテントのアクションを含めると、フィルタによって、アクティビティ名とともにそのアクションがチェックされます。実際のアプリのアクティビティでは、インテントのアクションをフィルタしないため、引数を null にしてもかまいません。

  1. フィルタをフィルタセットに追加します。

SplitManager.kt / createSplit()

val summaryActivityFilterSet = setOf(summaryActivityFilter)

SplitManager.java / createSplit()

Set<ActivityFilter> summaryActivityFilterSet = new HashSet<>();
summaryActivityFilterSet.add(summaryActivityFilter);
  1. ActivityRule を作成します。

SplitManager.kt / createSplit()

val activityRule = ActivityRule.Builder(summaryActivityFilterSet)
      .setAlwaysExpand(true)
      .build()

SplitManager.java / createSplit()

ActivityRule activityRule = new ActivityRule.Builder(
    summaryActivityFilterSet
).setAlwaysExpand(true)
 .build();

ActivityRule.Builder はルールを作成して構成します。

  • summaryActivityFilterSet: 分割から除外するアクティビティを識別することで、いつルールを適用するかを判断するアクティビティ フィルタが含まれます。
  • setAlwaysExpand - アクティビティを利用可能なディスプレイ スペース全体に拡張するかどうかを指定します。
  1. ルールを WindowManager の RuleController に追加します。

SplitManager.kt / createSplit()

ruleController.addRule(activityRule)

SplitManager.java / createSplit()

ruleController.addRule(activityRule);

実行

サンプルアプリをビルドして実行します。

アプリは、XML 構成ファイルを使用してカスタマイズした場合と同じように動作する必要があります。

この Codelab の「XML 構成」セクションの「実行」をご覧ください。

他のトピック

SplitPairRule.BuilderSplitPlaceholderRule.BuildersetMaxAspectRatioInPortrait メソッドと 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. 完了

お疲れさまでした。今回は、大きな画面でリストと詳細レイアウトを使用することで、アクティビティ ベースのアプリを最適化しました。

アクティビティの埋め込みを実装する方法として、次の 2 つを学びました。

  • XML 構成ファイルを使用する
  • Jetpack API 呼び出しを実行する

また、アプリの Kotlin または Java のソースコードは編集しませんでした。

アクティビティの埋め込みを使用して、大画面向けに本番環境アプリを最適化できるようになりました。

8. 関連リンク