アクティビティの埋め込み

アクティビティの埋め込みは、大画面デバイス上のアプリのタスク ウィンドウを 2 つのアクティビティ、または同じアクティビティの 2 つのインスタンスに分割することでアプリを最適化する機能です。

図 1. 設定アプリのアクティビティを並べて表示

複数のアクティビティで構成されているアプリの場合、アクティビティを埋め込むことで、タブレット、折りたたみ式デバイス、ChromeOS デバイスでのユーザー エクスペリエンスを向上させることができます。

アクティビティの埋め込みでは、コードのリファクタリングは必要ありません。XML 構成ファイルを作成するか、Jetpack WindowManager API 呼び出しを行うことで、アプリのアクティビティを並べて表示するか重ねて表示するかを決定します。

小画面のサポートは自動的に維持されます。アプリが小画面のデバイスにある場合、アクティビティは重ねられます。大画面の場合、アクティビティは並べて表示されます。作成した構成に基づいてシステムが表示方法を決定します。分岐ロジックは必要ありません。

アクティビティの埋め込みはデバイスの向きの変更に対応します。折りたたみ式デバイスでもシームレスに動作し、デバイスの折りたたみと展開に合わせて、アクティビティの積み重ねと積み重ね解除を行います。

アクティビティの埋め込みは、Android 12L(API レベル 32)以上を搭載するほとんどの大画面デバイスでサポートされています。

分割タスク ウィンドウ

アクティビティの埋め込みは、アプリのタスク ウィンドウをプライマリとセカンダリという 2 つのコンテナに分割します。コンテナは、メイン アクティビティから起動されたアクティビティ、またはコンテナ内にすでに存在する他のアクティビティから起動されたアクティビティを保持します。

アクティビティは、起動時にセカンダリ コンテナ内で重ねられ、小画面ではセカンダリ コンテナがプライマリ コンテナの上に重ねられるため、アクティビティの積み重ねと「戻る」ナビゲーションは、アプリにすでに組み込まれているアクティビティの順序と一致します。

アクティビティの埋め込みを使用すると、さまざまな方法でアクティビティを表示できます。アプリは、2 つのアクティビティを同時に並べて起動することで、タスク ウィンドウを分割できます。

図 2. 2 つのアクティビティを並べて表示。

また、タスク ウィンドウ全体を占有するアクティビティは、新しいアクティビティを並べて起動することで、タスク ウィンドウを分割できます。

図 3. アクティビティ A がアクティビティ B を横に起動。

すでに分割されてタスク ウィンドウを共有しているアクティビティは、次の方法で他のアクティビティを起動できます。

  • 横のアクティビティの上に重ねて起動:

    図 4. アクティビティ A が、横のアクティビティ B の上にアクティビティ C を重ねて起動。
  • 横に起動し、分割を横にシフトして以前のプライマリ アクティビティを非表示にする:

    図 5. アクティビティ B が、横にアクティビティ C を起動して分割を横にシフト。
  • アクティビティをその場所の一番上(つまり同じアクティビティ スタック)に起動:

    図 6. アクティビティ B が、追加のインテント フラグなしでアクティビティ C を起動。
  • アクティビティをそのタスクのフルウィンドウで起動:

    図 7. アクティビティ A またはアクティビティ B が、タスク ウィンドウいっぱいにアクティビティ C を起動。

「戻る」ナビゲーション

アクティビティ間の依存関係やユーザーが「戻る」イベントをトリガーする方法に応じて、分割タスク ウィンドウ状態での「戻る」ナビゲーションのルールはアプリの種類によって異なる場合があります。次に例を示します。

  • 一緒に: アクティビティ同士に関連性があり、一方のアクティビティを他方のアクティビティなしで表示すべきでない場合、両方を終了させるように「戻る」ナビゲーションを設定できます。
  • 単独で: タスク ウィンドウ内のアクティビティ同士が完全に独立している場合、一方のアクティビティの「戻る」ナビゲーションは、他方のアクティビティの状態に影響を与えません。

ボタン ナビゲーションを使用する場合、「戻る」イベントは最後にフォーカスされたアクティビティに送信されます。

ジェスチャー ベースのナビゲーションの場合:

  • Android 14(API レベル 34)以前 - 「戻る」イベントは、ジェスチャーが発生したアクティビティに送信されます。ユーザーが画面の左側からスワイプすると、分割ウィンドウの左側のパネルにあるアクティビティに「戻る」イベントが送信されます。ユーザーが画面の右側からスワイプすると、右側のペインにあるアクティビティに「戻る」イベントが送信されます。

  • Android 15(API レベル 35)以降

    • 同じアプリの複数のアクティビティを処理する場合、スワイプの方向に関係なく、ジェスチャーで最上位のアクティビティが終了するため、操作性の統一感が高まります。

    • 異なるアプリの 2 つのアクティビティ(オーバーレイ)が関係するシナリオでは、ボタン ナビゲーションの動作に合わせて、最後にフォーカスされたアクティビティに「戻る」イベントが送信されます。

マルチペイン レイアウト

Jetpack WindowManager を使用すると、Android 12L(API レベル 32)以上を搭載した大画面デバイスと、以前のバージョンのプラットフォームを搭載した一部のデバイスで、マルチペイン レイアウトを埋め込むアクティビティを構築できます。フラグメントやビューベースのレイアウト(SlidingPaneLayout など)ではなく複数のアクティビティに基づく既存のアプリは、ソースコードをリファクタリングすることなく、大画面のユーザー エクスペリエンスを向上させることができます。

一般的な例としては、リストと詳細の分割が挙げられます。質の高い表示を確保するために、システムはリスト アクティビティを起動し、その後アプリが直ちに詳細アクティビティを起動します。遷移システムは、両方のアクティビティが描画されるまで待機してから、一緒に表示します。ユーザーにとっては、2 つのアクティビティが 1 つのものとして起動します。

図 8. マルチペイン レイアウトで同時に起動した 2 つのアクティビティ。

分割属性

分割されたコンテナ間でのタスク ウィンドウの比率と、コンテナ同士の相対的なレイアウトを指定できます。

XML 構成ファイルで定義されているルールについては、次の属性を設定します。

  • splitRatio: コンテナの比率を設定します。値は、開区間(0.0、1.0)の浮動小数点数です。
  • splitLayoutDirection: 分割されたコンテナ同士を相対的にレイアウトする方法を指定します。次の値があります。
    • ltr: 左から右
    • rtl: 右から左
    • locale: ltr または rtl のどちらかを、ロケール設定から決定します

例については、XML 構成のセクションをご覧ください。

WindowManager API を使用して作成されたルールについては、SplitAttributes.BuilderSplitAttributes オブジェクトを作成し、次のビルダー メソッドを呼び出します。

例については、WindowManager API セクションをご覧ください。

図 9. 2 つのアクティビティの分割は左から右にレイアウトされていますが、分割比率が異なります。

プレースホルダ

プレースホルダ アクティビティは、アクティビティ分割の領域を占有する空のセカンダリ アクティビティです。最終的には、コンテンツを含む別のアクティビティに置き換えられます。たとえばプレースホルダ アクティビティは、リストのアイテムが選択されるまで、リストと詳細のレイアウトでアクティビティ分割のセカンダリ側を占有します。選択された時点で、プレースホルダは選択されたリストアイテムの詳細情報を含むアクティビティに置き換えられます。

デフォルトでは、プレースホルダは、アクティビティ分割に対応する十分なスペースがある場合にのみ表示されます。プレースホルダは、ディスプレイ サイズが分割を表示できないほど小さな幅または高さに変更されると自動的に終了します。スペースが許せば、再初期化した状態でプレースホルダは再起動されます。

図 10. 折りたたみ式デバイスの折りたたみと展開。プレースホルダ アクティビティは終了し、ディスプレイ サイズが変更されると再作成されます。

ただし、SplitPlaceholder.BuilderSplitPlaceholderRule メソッドまたは setSticky() メソッドの stickyPlaceholder 属性は、デフォルトの動作をオーバーライドできます。属性またはメソッドに true の値が指定されている場合、ディスプレイを 2 ペイン ディスプレイから単一ペイン ディスプレイに縮小すると、システムにより、プレースホルダはタスク ウィンドウの一番上のアクティビティとして表示されます。(分割の設定で例をご覧ください)。

図 11. 折りたたみ式デバイスの折りたたみと展開。プレースホルダのアクティビティは固定されます。

ウィンドウ サイズの変更

デバイスの設定が変更され、タスク ウィンドウの幅が狭くなり、マルチペイン レイアウトに十分な大きさでなくなった場合(大画面の折りたたみ式デバイスがタブレットサイズからスマートフォン サイズに折りたたまれたときや、マルチウィンドウ モードでアプリ ウィンドウのサイズが変更されたときなど)は、タスク ウィンドウのセカンダリ ペインにあるプレースホルダ以外のアクティビティが、プライマリ ペイン内のアクティビティの上に重ねて表示されます。

プレースホルダのアクティビティは、分割するために十分な表示幅がある場合にのみ表示されます。小画面では、プレースホルダは自動的に閉じられます。表示領域が再び十分な大きさになると、プレースホルダが再作成されます。(プレースホルダ セクションを参照)。

アクティビティを重ねることができるのは、WindowManager がセカンダリ ペインのアクティビティをプライマリ ペインのアクティビティの上に Z オーダーで表示するためです。

セカンダリ ペインの複数のアクティビティ

アクティビティ B が追加のインテント フラグなしで、アクティビティ C を所定の位置で起動します。

アクティビティ C がアクティビティ B の上に重ねられた、アクティビティ A、B、C を含むアクティビティ分割。

その結果、同じタスクのアクティビティの Z オーダーは次のようになります。

アクティビティ B の上に重ねられたアクティビティ C を含むセカンダリ アクティビティ スタック。セカンダリ スタックが、アクティビティ A を含むプライマリ アクティビティ スタックの上に重ねられています。

タスク ウィンドウが小さくなるとアプリは 1 つのアクティビティに縮小され、アクティビティ C がスタックの一番上に重ねられます。

アクティビティ C のみを表示する小さいウィンドウ。

小さいウィンドウで「戻る」操作を行うと、重ねられた各アクティビティ間で移動します。

タスク ウィンドウ構成を、複数のペインに対応できる大きなサイズに復元すると、アクティビティは再び並んで表示されます。

分割の積み重ね

アクティビティ B が、横にアクティビティ C を起動して分割を横にシフトします。

アクティビティ A と B が表示され、次にアクティビティ B と C が表示されるタスク ウィンドウ。

その結果、同じタスクのアクティビティの Z オーダーは次のようになります。

1 つのスタック内にあるアクティビティ A、B、C。アクティビティは上から C、B、A の順に重ねられています。

アプリは、タスク ウィンドウが小さくなると、アクティビティ C が一番上にある 1 つのアクティビティに縮小されます。

アクティビティ C のみを表示する小さいウィンドウ。

固定縦向き

android:screenOrientation マニフェスト設定を使用すると、アプリのアクティビティを縦向きまたは横向きに制限できます。タブレットや折りたたみ式デバイスなどの大画面デバイスでのユーザー エクスペリエンスを改善するために、デバイス メーカー(OEM)は画面の向きのリクエストを無視して、横向きディスプレイでアプリを縦向きに、縦向きディスプレイでアプリを横向きにレターボックス表示できます。

図 12. レターボックス表示のアクティビティ: 横向きデバイスでの固定縦向き(左)と、縦向きデバイスでの固定横向き(右)。

同様に、アクティビティの埋め込みが有効になっていると、OEM は大画面デバイス(幅 600 dp 以上)をカスタマイズして、横向き時に固定縦向きのアクティビティをレターボックス表示できます。固定縦向きアクティビティが 2 つ目のアクティビティを起動する場合、2 つのアクティビティを 2 つのペインで並べて表示できます。

図 13. 固定縦向きアクティビティ A が、その横にアクティビティ B を起動。

必ずアプリのマニフェスト ファイルに android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED プロパティを追加して、アプリがアクティビティの埋め込みをサポートしていることをデバイスに伝えてください(分割の構成をご覧ください)。これにより、OEM によるカスタマイズがあるデバイスが、固定縦向きのアクティビティをレターボックス表示するかどうかを判断できます。

分割の設定

アクティビティ分割は、分割ルールによって設定します。分割ルールは、XML 構成ファイル内で定義するか、Jetpack WindowManager API 呼び出しを行って定義します。

どちらの方法を選択した場合でも、アプリから WindowManager ライブラリにアクセスし、アクティビティの埋め込みが実装されていることをシステムに通知する必要があります。

次のように対応します。

  1. 次の例のように、アプリのモジュール レベルの build.gradle ファイルに最新の WindowManager ライブラリ依存関係を追加します。

    implementation 'androidx.window:window:1.1.0-beta02'

    WindowManager ライブラリは、アクティビティの埋め込みに必要なすべてのコンポーネントを提供します。

  2. アプリがアクティビティの埋め込みを実装していることを、システムに通知します。

    アプリ マニフェスト ファイルの <application> 要素に android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED プロパティを追加し、次の例のように値を true に設定します。

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

    WindowManager リリース 1.1.0-alpha06 以降では、このプロパティをマニフェストに追加して true に設定していないと、アクティビティの埋め込みによる分割が無効になります。

    この設定は、アクティビティの埋め込みをサポートするアプリに対して、デバイス メーカーがカスタム機能を有効にする場合にも使用します。たとえば、横向きディスプレイに縦向きのみのアクティビティをレターボックス表示し、2 つ目のアクティビティが開始されたら 2 ペイン レイアウトに移行してアクティビティの向きを調整できます(縦向きに固定するをご覧ください)。

XML 構成

アクティビティの埋め込みの実装を XML ベースで作成する手順は次のとおりです。

  1. 次の処理を行う XML リソース ファイルを作成します。

    • 分割を共有するアクティビティを定義する
    • 分割オプションを設定する
    • コンテンツを利用できない場合に分割のセカンダリ コンテナのプレースホルダを作成する
    • 分割に入れないアクティビティを指定する

    次に例を示します。

    <!-- main_split_config.xml -->
    
    <resources
        xmlns:window="http://schemas.android.com/apk/res-auto">
    
        <!-- Define a split for the named activities. -->
        <SplitPairRule
            window:splitRatio="0.33"
            window:splitLayoutDirection="locale"
            window:splitMinWidthDp="840"
            window:splitMaxAspectRatioInPortrait="alwaysAllow"
            window:finishPrimaryWithSecondary="never"
            window:finishSecondaryWithPrimary="always"
            window:clearTop="false">
            <SplitPairFilter
                window:primaryActivityName=".ListActivity"
                window:secondaryActivityName=".DetailActivity"/>
        </SplitPairRule>
    
        <!-- Specify a placeholder for the secondary container when content is
             not available. -->
        <SplitPlaceholderRule
            window:placeholderActivityName=".PlaceholderActivity"
            window:splitRatio="0.33"
            window:splitLayoutDirection="locale"
            window:splitMinWidthDp="840"
            window:splitMaxAspectRatioInPortrait="alwaysAllow"
            window:stickyPlaceholder="false">
            <ActivityFilter
                window:activityName=".ListActivity"/>
        </SplitPlaceholderRule>
    
        <!-- Define activities that should never be part of a split. Note: Takes
             precedence over other split rules for the activity named in the
             rule. -->
        <ActivityRule
            window:alwaysExpand="true">
            <ActivityFilter
                window:activityName=".ExpandedActivity"/>
        </ActivityRule>
    
    </resources>
    
  2. イニシャライザを作成します。

    WindowManager の RuleController コンポーネントは、XML 構成ファイルを解析してシステムで利用できるルールをつくります。Jetpack の Startup ライブラリ Initializer を使用すると、アプリの起動時に XML ファイルが RuleController を利用できるようになり、アクティビティの開始時にルールが有効になります。

    イニシャライザを作成する手順は次のとおりです。

    1. 次の例のように、Jetpack Startup ライブラリの最新の依存関係をモジュール レベルの build.gradle ファイルに追加します。

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

    2. Initializer インターフェースを実装するクラスを作成します。

      イニシャライザは、XML 構成ファイル(main_split_config.xml)の ID を RuleController.parseRules() メソッドに渡すことで、分割ルールを RuleController で使用できるようにします。

      Kotlin

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

      Java

      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();
           }
      }
  3. ルール定義のコンテンツ プロバイダを作成します。

    androidx.startup.InitializationProvider<provider> としてアプリ マニフェスト ファイルに追加します。RuleController イニシャライザ、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>
    

    InitializationProviderSplitInitializer を見つけて初期化してから、アプリの onCreate() メソッドを呼び出します。そのため、分割ルールはアプリのメイン アクティビティが起動すると有効になります。

WindowManager API

アクティビティの埋め込みは、わずかな API 呼び出しを使用してプログラムで実装できます。Application のサブクラスの onCreate() メソッドで呼び出しを行い、アクティビティを起動する前にルールを有効にします。

アクティビティの分割をプログラムで作成する手順は、次のとおりです。

  1. 分割ルールを作成します。

    1. 分割を共有するアクティビティを識別するための SplitPairFilter を作成します。

      Kotlin

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

      Java

      SplitPairFilter splitPairFilter = new SplitPairFilter(
         new ComponentName(this, ListActivity.class),
         new ComponentName(this, DetailActivity.class),
         null
      );
    2. フィルタをフィルタセットに追加します。

      Kotlin

      val filterSet = setOf(splitPairFilter)

      Java

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

      Kotlin

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

      Java

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

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

      • setSplitType(): 利用可能な表示領域を各アクティビティ コンテナに割り当てる方法を定義します。比率分割タイプは、プライマリ コンテナに割り当てられた使用可能な表示領域の比率を指定し、セカンダリ コンテナは残りの使用可能な表示領域を占有します。
      • setLayoutDirection(): プライマリ コンテナから順に、アクティビティ コンテナ同士の相対的なレイアウトを指定します。
    4. SplitPairRule をビルドします。

      Kotlin

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

      Java

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

      SplitPairRule.Builder がルールを作成して設定します。

      • filterSet: 分割を共有するアクティビティを識別することで、ルールをいつ適用するか判断する分割ペアのフィルタが含まれます。
      • setDefaultSplitAttributes(): レイアウト属性をルールに適用します。
      • setMinWidthDp(): 分割を有効にする最小表示幅(密度非依存ピクセル、dp)を設定します。
      • setMinSmallestWidthDp(): デバイスの向きにかかわらず分割を有効にするために、2 つのディスプレイ サイズのうち小さい方が必要とする最小値(dp)を設定します。
      • setMaxAspectRatioInPortrait(): アクティビティの分割が表示される縦向きの最大表示アスペクト比(高さ:幅)を設定します。縦向きディスプレイのアスペクト比が最大アスペクト比を超えると、ディスプレイの幅に関係なく、分割が無効になります。注: デフォルト値は 1.4 です。これにより、ほとんどのタブレットでは、タスク ウィンドウ全体にアクティビティが縦向きに表示されます。SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULTsetMaxAspectRatioInLandscape() もご覧ください。横向きのデフォルト値は ALWAYS_ALLOW です。
      • setFinishPrimaryWithSecondary() - セカンダリ コンテナ内のすべてのアクティビティを終了すると、プライマリ コンテナのアクティビティにどのように影響するかを設定します。NEVER は、セカンダリ コンテナ内のすべてのアクティビティが終了したとき、システムはプライマリ アクティビティを終了してはならないことを示します(アクティビティを終了するを参照)。
      • setFinishSecondaryWithPrimary() - プライマリ コンテナ内のすべてのアクティビティを終了すると、セカンダリ コンテナのアクティビティにどのように影響するかを設定します。ALWAYS は、プライマリ コンテナ内のすべてのアクティビティが終了したときに、セカンダリ コンテナ内のアクティビティを常に終了させることを示します(アクティビティを終了するを参照)。
      • setClearTop(): セカンダリ コンテナ内で新しいアクティビティが起動したときに、そのコンテナ内のすべてのアクティビティを終了するかどうかを指定します。false 値は、セカンダリ コンテナにすでに存在するアクティビティの上に新しいアクティビティが重ねられることを指定します。
    5. WindowManager の RuleController のシングルトン インスタンスを取得し、ルールを追加します。

      Kotlin

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

      Java

        RuleController ruleController = RuleController.getInstance(this);
        ruleController.addRule(splitPairRule);
        
  2. コンテンツを利用できない場合に、セカンダリ コンテナのプレースホルダを作成します。

    1. このプレースホルダが、タスク ウィンドウの分割をどのアクティビティと共有するかを識別するための ActivityFilter を作成します。

      Kotlin

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

      Java

      ActivityFilter placeholderActivityFilter = new ActivityFilter(
          new ComponentName(this, ListActivity.class),
          null
      );
    2. フィルタをフィルタセットに追加します。

      Kotlin

      val placeholderActivityFilterSet = setOf(placeholderActivityFilter)

      Java

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

      Kotlin

      val splitPlaceholderRule = SplitPlaceholderRule.Builder(
            placeholderActivityFilterSet,
            Intent(context, PlaceholderActivity::class.java)
          ).setDefaultSplitAttributes(splitAttributes)
           .setMinWidthDp(840)
           .setMinSmallestWidthDp(600)
           .setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ratio(1.5f))
           .setFinishPrimaryWithPlaceholder(SplitRule.FinishBehavior.ALWAYS)
           .setSticky(false)
           .build()

      Java

      SplitPlaceholderRule splitPlaceholderRule = new SplitPlaceholderRule.Builder(
            placeholderActivityFilterSet,
            new Intent(context, PlaceholderActivity.class)
          ).setDefaultSplitAttributes(splitAttributes)
           .setMinWidthDp(840)
           .setMinSmallestWidthDp(600)
           .setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ratio(1.5f))
           .setFinishPrimaryWithPlaceholder(SplitRule.FinishBehavior.ALWAYS)
           .setSticky(false)
           .build();

      SplitPlaceholderRule.Builder がルールを作成して設定します。

      • placeholderActivityFilterSet: プレースホルダ アクティビティが関連付けられているアクティビティを特定することで、いつルールを適用するかを判断するアクティビティ フィルタが含まれます。
      • Intent: プレースホルダ アクティビティの起動を指定します。
      • setDefaultSplitAttributes(): レイアウト属性をルールに適用します。
      • setMinWidthDp(): 分割を許可する最小表示幅(密度非依存ピクセル、dp)を設定します。
      • setMinSmallestWidthDp(): デバイスの向きにかかわらず分割を有効にするために、2 つのディスプレイ サイズのうち小さい方が必要とする最小値(dp)を設定します。
      • setMaxAspectRatioInPortrait(): アクティビティの分割が表示される縦向きの最大表示アスペクト比(高さ:幅)を設定します。注: デフォルト値は 1.4 です。これにより、ほとんどのタブレットでは、アクティビティがタスク ウィンドウに縦向きに表示されます。SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULTsetMaxAspectRatioInLandscape() もご覧ください。横向きのデフォルト値は ALWAYS_ALLOW です。
      • setFinishPrimaryWithPlaceholder(): プレースホルダ アクティビティを終了すると、プライマリ コンテナのアクティビティにどのように影響するかを設定します。ALWAYS は、プレースホルダが終了したときに、システムはプライマリ コンテナのアクティビティを常に終了させる必要があることを示します(アクティビティを終了するを参照)。
      • setSticky(): 十分な最小幅の分割でプレースホルダが最初に表示されたときに、小さなディスプレイのアクティビティ スタックの上にプレースホルダ アクティビティを表示するかどうかを決定します。
    4. ルールを WindowManager RuleController に追加します。

      Kotlin

      ruleController.addRule(splitPlaceholderRule)

      Java

      ruleController.addRule(splitPlaceholderRule);
  3. 分割に入れないアクティビティを指定します。

    1. タスクの表示領域全体を常に占有するアクティビティを識別するための ActivityFilter を作成します。

      Kotlin

      val expandedActivityFilter = ActivityFilter(
        ComponentName(this, ExpandedActivity::class.java),
        null
      )

      Java

      ActivityFilter expandedActivityFilter = new ActivityFilter(
        new ComponentName(this, ExpandedActivity.class),
        null
      );
    2. フィルタをフィルタセットに追加します。

      Kotlin

      val expandedActivityFilterSet = setOf(expandedActivityFilter)

      Java

      Set<ActivityFilter> expandedActivityFilterSet = new HashSet<>();
      expandedActivityFilterSet.add(expandedActivityFilter);
    3. ActivityRule を作成します。

      Kotlin

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

      Java

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

      ActivityRule.Builder がルールを作成して設定します。

      • expandedActivityFilterSet: 分割から除外するアクティビティを識別することで、いつルールを適用するかを判断するアクティビティ フィルタが含まれます。
      • setAlwaysExpand(): アクティビティをタスク ウィンドウ全体に表示するかどうかを指定します。
    4. ルールを WindowManager RuleController に追加します。

      Kotlin

      ruleController.addRule(activityRule)

      Java

      ruleController.addRule(activityRule);

アプリ間の埋め込み

Android 13(API レベル 33)以降では、他のアプリからアクティビティを埋め込むことができます。アプリ間(UID 間)のアクティビティ埋め込みを使用すると、複数の Android アプリから来るアクティビティを視覚的に統合できます。単一アプリのアクティビティの埋め込みと同様に、ホストアプリのアクティビティと、別のアプリからの埋め込みアクティビティは、左右または上下に並べて画面上に表示されます。

たとえば、設定アプリに、WallpaperPicker アプリの壁紙セレクタ アクティビティを埋め込むことができます。

図 14. 設定アプリ(左のメニュー)。壁紙セレクタ アクティビティが埋め込まれている(右)。

信頼モデル

他のアプリからのアクティビティを埋め込むホストプロセスでは、埋め込みアクティビティの表示(サイズ、位置、切り抜き、透過性など)を再定義できます。悪意のあるホストはこの機能を利用して、ユーザーに誤解を与えたり、クリックジャッキングなどの UI 偽装攻撃を仕掛けたりすることがあります。

Android では、アプリ間のアクティビティの埋め込みが悪用されることを防ぐため、アクティビティの埋め込みを許可するよう、アプリでオプトインする必要があります。アプリは、ホストを信頼するか、信頼しないかを指定できます。

信頼するホスト

自身のアプリからアクティビティを埋め込み、その表示を完全に制御することを他のアプリに許可するには、<activity>android:knownActivityEmbeddingCerts 属性またはアプリのマニフェスト ファイルの <application> 要素にホストアプリの SHA-256 証明書を指定します。

android:knownActivityEmbeddingCerts の値を文字列として設定します。

<activity
    android:name=".MyEmbeddableActivity"
    android:knownActivityEmbeddingCerts="@string/known_host_certificate_digest"
    ... />

または、文字列の配列で複数の証明書を指定します。

<activity
    android:name=".MyEmbeddableActivity"
    android:knownActivityEmbeddingCerts="@array/known_host_certificate_digests"
    ... />

これは、次のようなリソースを参照します。

<resources>
    <string-array name="known_host_certificate_digests">
      <item>cert1</item>
      <item>cert2</item>
      ...
    </string-array>
</resources>

アプリ所有者は、Gradle signingReport タスクを実行することで SHA 証明書ダイジェストを取得できます。証明書ダイジェストは、コロンで区切られていない SHA-256 フィンガープリントです。詳しくは、署名レポートを実行するクライアントの認証をご覧ください。

信頼しないホスト

自身のアプリのアクティビティ埋め込みと、その表示の制御をすべてのアプリに許可するには、アプリ マニフェストの <activity> 要素または <application> 要素で android:allowUntrustedActivityEmbedding 属性を指定します。次に例を示します。

<activity
    android:name=".MyEmbeddableActivity"
    android:allowUntrustedActivityEmbedding="true"
    ... />

この属性のデフォルト値は false(アプリ間のアクティビティの埋め込みを禁止)です。

カスタム認証

信頼しないアクティビティの埋め込みのリスクを軽減するには、ホスト ID を検証するカスタム認証メカニズムを作成します。ホスト証明書がわかっている場合は、androidx.security.app.authenticator ライブラリを使用して認証します。アクティビティの埋め込み後にホストが認証すると、実際のコンテンツを表示できます。そうでない場合、アクションが許可されていないことをユーザーに知らせるとともに、コンテンツをブロックできます。

Jetpack WindowManager ライブラリの ActivityEmbeddingController#isActivityEmbedded() メソッドを使用して、ホストがアクティビティを埋め込んでいるかどうかを確認します。次に例を示します。

Kotlin

fun isActivityEmbedded(activity: Activity): Boolean {
    return ActivityEmbeddingController.getInstance(this).isActivityEmbedded(activity)
}

Java

boolean isActivityEmbedded(Activity activity) {
    return ActivityEmbeddingController.getInstance(this).isActivityEmbedded(activity);
}

最小サイズの制限

Android システムは、アプリ マニフェストの <layout> 要素で指定された最小の高さと幅を埋め込みアクティビティに適用します。アプリで最小の高さと幅が指定されていない場合は、システムのデフォルト値が適用されます(sw220dp)。

ホストが埋め込みコンテナを最小サイズより小さいサイズに変更しようとすると、埋め込みコンテナのサイズは、タスク境界いっぱいにまで拡大します。

<activity-alias>

信頼する(またはしない)アクティビティの埋め込みを <activity-alias> 要素で機能させるには、android:knownActivityEmbeddingCerts または android:allowUntrustedActivityEmbedding を、エイリアスではなくそのアクティビティに適用する必要があります。システム サーバー上でセキュリティを検証するポリシーは、エイリアスではなくそのアクティビティに設定されたフラグに基づきます。

ホストアプリ

ホストアプリがアプリ間のアクティビティの埋め込みを実装する方法は、単一アプリの場合と同じです。SplitPairRule オブジェクトと SplitPairFilter オブジェクト、または ActivityRule オブジェクトと ActivityFilter オブジェクトで、埋め込みアクティビティとタスク ウィンドウの分割を指定します。分割ルールは、XML で静的に、または Jetpack WindowManager API 呼び出しを使用して実行時に定義されます。

ホストアプリが、アプリ間の埋め込みをオプトインしていないアクティビティを埋め込もうとした場合、アクティビティはタスク境界いっぱいのスペースを占有します。そのため、ホストアプリは、ターゲット アクティビティでアプリ間の埋め込みが可能かどうかを確認する必要があります。

埋め込みアクティビティが同じタスク内で新しいアクティビティを起動し、新しいアクティビティでアプリ間埋め込みがオプトインされていない場合、新しいアクティビティは埋め込みコンテナでアクティビティをオーバーレイするのではなく、タスク境界いっぱいのスペースを占有します。

ホストアプリは、同じタスク内でアクティビティを起動する限り、制限なしで独自のアクティビティを埋め込むことができます。

分割の例

フルウィンドウからの分割

図 15. アクティビティ A がアクティビティ B を横に起動。

リファクタリングは必要ありません。分割の設定を静的に、またはランタイムに定義し、追加のパラメータなしで Context#startActivity() を呼び出すことができます。

<SplitPairRule>
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

デフォルトでの分割

アプリのランディング ページが、大画面で 2 つのコンテナに分割されるように設計されている場合、両方のアクティビティを同時に作成して表示すると最良のユーザー エクスペリエンスが得られます。ただし、ユーザーがプライマリ コンテナでアクティビティを操作する(ナビゲーション メニューからアイテムを選択するなど)まで、分割のセカンダリ コンテナでコンテンツを利用できない場合があります。プレースホルダ アクティビティは、分割のセカンダリ コンテナにコンテンツを表示できるようになるまで、空白を埋めることができます(プレースホルダのセクションをご覧ください)。

図 16. 2 つのアクティビティを同時に開くことで作成された分割。一方のアクティビティはプレースホルダです。

プレースホルダを伴う分割を作成するには、プレースホルダを作成し、プライマリ アクティビティに関連付けます。

<SplitPlaceholderRule
    window:placeholderActivityName=".PlaceholderActivity">
    <ActivityFilter
        window:activityName=".MainActivity"/>
</SplitPlaceholderRule>

アプリがインテントを受け取ると、ターゲット アクティビティをアクティビティ分割のセカンダリ部分として表示できます(リストのアイテムに関する情報を詳細画面に表示するリクエストなど)。詳細は、小さいディスプレイではタスク ウィンドウ全体に表示され、大きいデバイスではリストの横に表示されます。

図 17. ディープリンクの詳細アクティビティは、小画面では単独で表示され、大画面ではリスト アクティビティとともに表示される。

起動リクエストはメイン アクティビティにルーティングされ、ターゲットの詳細アクティビティは分割で起動する必要があります。システムは、利用可能な表示幅に基づいて、適切な表示方法(積み重ねまたは横並び)を自動的に選択します。

Kotlin

override fun onCreate(savedInstanceState Bundle?) {
    . . .
    RuleController.getInstance(this)
        .addRule(SplitPairRule.Builder(filterSet).build())
    startActivity(Intent(this, DetailActivity::class.java))
}

Java

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    . . .
    RuleController.getInstance(this)
        .addRule(new SplitPairRule.Builder(filterSet).build());
    startActivity(new Intent(this, DetailActivity.class));
}

「戻る」ナビゲーション スタックでユーザーが利用できるアクティビティが、ディープリンクのデスティネーションのみの場合があります。その場合も含めて、詳細アクティビティが閉じてメイン アクティビティだけが残る、ということがないようにしてください。

リスト アクティビティと詳細アクティビティを並べて表示した大きなディスプレイ。
          「戻る」ナビゲーションで、詳細アクティビティを閉じてリスト アクティビティを画面に残すことはできません。

詳細アクティビティのみの小さいディスプレイ。「戻る」ナビゲーションで、詳細アクティビティを閉じてリスト アクティビティを表示することはできません。

代わりに、finishPrimaryWithSecondary 属性を使用することで両方のアクティビティを同時に終了できます。

<SplitPairRule
    window:finishPrimaryWithSecondary="always">
    <SplitPairFilter
        window:primaryActivityName=".ListActivity"
        window:secondaryActivityName=".DetailActivity"/>
</SplitPairRule>

構成属性セクションをご覧ください。

分割コンテナ内の複数のアクティビティ

分割コンテナに複数のアクティビティを重ねると、ユーザーは深いコンテンツにアクセスできます。たとえば、リストと詳細の分割で、ユーザーがサブ詳細セクションに移動する必要がある場合でも、プライマリ アクティビティはそのまま表示し続けます。

図 18. タスク ウィンドウのセカンダリ ペインで所定の位置に開かれたアクティビティ。

Kotlin

class DetailActivity {
    . . .
    fun onOpenSubDetail() {
      startActivity(Intent(this, SubDetailActivity::class.java))
    }
}

Java

public class DetailActivity {
    . . .
    void onOpenSubDetail() {
        startActivity(new Intent(this, SubDetailActivity.class));
    }
}

サブ詳細アクティビティが、詳細アクティビティを隠すように、上に配置されます。

すると、ユーザーはスタック内をさかのぼって移動することで前の詳細レベルに戻ることができます。

図 19. スタック最上位からのアクティビティの削除。

同じセカンダリ コンテナ内のアクティビティからアクティビティを起動すると、アクティビティは互いに重ねられます。これがデフォルトの動作です。アクティブな分割の中でプライマリ コンテナから起動されたアクティビティも、アクティビティ スタックの一番上にあるセカンダリ コンテナに配置されます。

新しいタスクのアクティビティ

分割タスク ウィンドウ内のアクティビティが新しいタスクのアクティビティを起動すると、新しいタスクは、分割を含むタスクとは別に、ウィンドウ全体に表示されます。履歴画面には 2 つのタスク(分割の中のタスクと新しいタスク)が表示されます。

図 20. アクティビティ B から新しいタスクでアクティビティ C を起動。

アクティビティの置き換え

セカンダリ コンテナ スタック内でアクティビティを置き換えることができます(プライマリ アクティビティをトップレベル ナビゲーションに使用し、セカンダリ アクティビティが選択したデスティネーションである場合など)。トップレベル ナビゲーションから選択されるたびに、セカンダリ コンテナで新しいアクティビティが起動し、それまでそこにあったアクティビティは削除されます。

図 21. プライマリ ペインのトップレベル ナビゲーション アクティビティが、セカンダリ ペインのデスティネーション アクティビティに置き換わる。

ナビゲーションの選択が変更されたときにアプリがセカンダリ コンテナのアクティビティを終了しない場合、分割が閉じられた(デバイスが折りたたまれた)ときに「戻る」ナビゲーションがわかりにくくなる可能性があります。たとえば、プライマリ ペインにメニューがあり、セカンダリ ペインに画面 A と B が重ねられている場合、ユーザーがスマートフォンを折りたたむと、B が A の上になり、A がメニューの上になります。ユーザーが B から戻ると、メニューではなく A が表示されます。

このような場合は、画面 A をバックスタックから削除する必要があります。

既存の分割の上に新しいコンテナで横に起動する場合のデフォルト動作では、新しいセカンダリ コンテナが上に配置され、古いコンテナがバックスタックに保持されます。clearTop で以前のセカンダリ コンテナを消去して新しいアクティビティを通常どおり起動するように、分割を設定できます。

<SplitPairRule
    window:clearTop="true">
    <SplitPairFilter
        window:primaryActivityName=".Menu"
        window:secondaryActivityName=".ScreenA"/>
    <SplitPairFilter
        window:primaryActivityName=".Menu"
        window:secondaryActivityName=".ScreenB"/>
</SplitPairRule>

Kotlin

class MenuActivity {
    . . .
    fun onMenuItemSelected(selectedMenuItem: Int) {
        startActivity(Intent(this, classForItem(selectedMenuItem)))
    }
}

Java

public class MenuActivity {
    . . .
    void onMenuItemSelected(int selectedMenuItem) {
        startActivity(new Intent(this, classForItem(selectedMenuItem)));
    }
}

あるいは、同じセカンダリ アクティビティを使用し、プライマリ(メニュー)アクティビティから、同じインスタンスに解決されてセカンダリ コンテナで状態または UI の更新をトリガーする新しいインテントを送信します。

複数の分割

アプリは、追加のアクティビティを横に起動することで、多層の深いナビゲーションを提供できます。

セカンダリ コンテナのアクティビティが新しいアクティビティを横に起動すると、既存の分割の上に新しい分割が作成されます。

図 22. アクティビティ B がアクティビティ C を横に起動。

バックスタックには以前開かれたアクティビティがすべて含まれているため、ユーザーは C を終了した後に A/B 分割に移動できます。

スタック内にあるアクティビティ A、B、C。アクティビティは上から C、B、A の順に重ねられています。

新しい分割を作成するには、既存のセカンダリ コンテナから新しいアクティビティを横に起動します。A/B 分割と B/C 分割の両方の設定を宣言し、アクティビティ C を通常どおり B から起動します。

<SplitPairRule>
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
    <SplitPairFilter
        window:primaryActivityName=".B"
        window:secondaryActivityName=".C"/>
</SplitPairRule>

Kotlin

class B {
    . . .
    fun onOpenC() {
        startActivity(Intent(this, C::class.java))
    }
}

Java

public class B {
    . . .
    void onOpenC() {
        startActivity(new Intent(this, C.class));
    }
}

分割状態の変更に対応する

アプリ内の異なるアクティビティに、同じ機能を果たす UI 要素が存在することがあります(アカウント設定を含むウィンドウを開くコントロールなど)。

図 23. 機能が同じ UI 要素を持つ異なるアクティビティ。

分割の中に共通の UI 要素を持つ 2 つのアクティビティがある場合、その要素を両方のアクティビティに表示することは冗長であり、混乱を招くおそれがあります。

図 24. アクティビティの分割で UI 要素が重複。

アクティビティが分割されるときを知るには、SplitController.splitInfoList フローを確認するか、分割状態の変化についてリスナーを SplitControllerCallbackAdapter に登録します。次に、それに応じて UI を調整します。

Kotlin

val layout = layoutInflater.inflate(R.layout.activity_main, null)
val view = layout.findViewById<View>(R.id.infoButton)
lifecycleScope.launch {
    lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
        splitController.splitInfoList(this@SplitDeviceActivity) // The activity instance.
            .collect { list ->
                view.visibility = if (list.isEmpty()) View.VISIBLE else View.GONE
            }
    }
}

Java

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    . . .
    new SplitControllerCallbackAdapter(SplitController.getInstance(this))
        .addSplitListener(
            this,
            Runnable::run,
            splitInfoList -> {
                View layout = getLayoutInflater().inflate(R.layout.activity_main, null);
                layout.findViewById(R.id.infoButton).setVisibility(
                    splitInfoList.isEmpty() ? View.VISIBLE : View.GONE);
            });
}

コルーチンはどのライフサイクル状態でも起動できますが、通常はリソースを節約するために STARTED 状態で起動されます。(詳しくは、ライフサイクル対応コンポーネントで Kotlin コルーチンを使用するをご覧ください)。

コールバックは、アクティビティが停止している場合を含め、どのようなライフサイクル状態でも発生する可能性があります。リスナーは通常、onStart() で登録し、onStop() で登録を解除します。

フルウィンドウ モーダル

ログイン画面、ポリシーの確認画面、エラー メッセージなど、アクティビティの中には、指定されたアクションが行われるまでユーザーがアプリを操作できないようにするものもあります。モーダル アクティビティは、分割に表示されないようにする必要があります。

Expand 設定を使用することで、アクティビティが常にタスク ウィンドウ全体に表示されるよう強制できます。

<ActivityRule
    window:alwaysExpand="true">
    <ActivityFilter
        window:activityName=".FullWidthActivity"/>
</ActivityRule>

アクティビティを終了する

ディスプレイの端からスワイプすることで、分割のどちらかの側のアクティビティを終了できます。

図 25. スワイプ操作でアクティビティ B を終了。
図 26. スワイプ操作でアクティビティ A を終了。

デバイスがジェスチャー ナビゲーションではなく [戻る] ボタンを使用するよう設定されている場合、入力はフォーカスされているアクティビティ(最後にタップまたは起動されたアクティビティ)に送信されます。

コンテナ内のすべてのアクティビティを終了した場合、反対側のコンテナへの影響は分割の設定によって異なります。

構成属性

分割ペアのルール属性を指定して、そのペアの片方のアクティビティをすべて終了した場合、もう一方のアクティビティにどのように影響するかを構成できます。属性は次のとおりです。

  • window:finishPrimaryWithSecondary - セカンダリ コンテナ内のすべてのアクティビティを終了すると、プライマリ コンテナのアクティビティにどのように影響するか
  • window:finishSecondaryWithPrimary - プライマリ コンテナ内のすべてのアクティビティを終了すると、セカンダリ コンテナのアクティビティにどのように影響するか

指定可能な属性値は次のとおりです。

  • always - 関連付けられているコンテナのアクティビティを常に終了する
  • never - 関連付けられているコンテナのアクティビティを終了しない
  • adjacent - 2 つのコンテナが互いに隣接して表示される場合、関連付けられているコンテナのアクティビティを終了する(2 つのコンテナがスタックされている場合は終了しない)

例:

<SplitPairRule
    &lt;!-- Do not finish primary container activities when all secondary container activities finish. --&gt;
    window:finishPrimaryWithSecondary="never"
    &lt;!-- Finish secondary container activities when all primary container activities finish. --&gt;
    window:finishSecondaryWithPrimary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

デフォルト設定

分割された 1 つのコンテナ内のすべてのアクティビティが終了すると、残りのコンテナがウィンドウ全体を占有します。

<SplitPairRule>
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

アクティビティ A とアクティビティ B を含む分割。A が終了すると B がウィンドウ全体を占有します。

アクティビティ A とアクティビティ B を含む分割。B が終了すると A がウィンドウ全体を占有します。

アクティビティをまとめて終了する

セカンダリ コンテナ内のすべてのアクティビティが終了すると、プライマリ コンテナのアクティビティは自動的に終了します。

<SplitPairRule
    window:finishPrimaryWithSecondary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

アクティビティ A とアクティビティ B を含む分割。B が終了すると A も終了し、タスク ウィンドウは空になります。

アクティビティ A とアクティビティ B を含む分割。A が終了すると B だけがタスク ウィンドウに残ります。

プライマリ コンテナ内のすべてのアクティビティが終了すると、セカンダリ コンテナのアクティビティは自動的に終了します。

<SplitPairRule
    window:finishSecondaryWithPrimary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

アクティビティ A とアクティビティ B を含む分割。A が終了すると B も終了し、タスク ウィンドウは空になります。

アクティビティ A とアクティビティ B を含む分割。B が終了すると A だけがタスク ウィンドウに残ります。

プライマリ コンテナ内またはセカンダリ コンテナ内のすべてのアクティビティが終了したときに、アクティビティをまとめて終了します。

<SplitPairRule
    window:finishPrimaryWithSecondary="always"
    window:finishSecondaryWithPrimary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

アクティビティ A とアクティビティ B を含む分割。A が終了すると B も終了し、タスク ウィンドウは空になります。

アクティビティ A とアクティビティ B を含む分割。B が終了すると A も終了し、タスク ウィンドウは空になります。

コンテナの複数のアクティビティを終了する

分割コンテナで複数のアクティビティが重なっている場合、その一番下にあるアクティビティが終了しても、上にあるアクティビティが自動的に終了することはありません。

たとえば、2 つのアクティビティがセカンダリ コンテナにあり、B の上に C があるとします。

アクティビティ B の上に重ねられたアクティビティ C を含むセカンダリ アクティビティ スタックが、アクティビティ A を含むプライマリ アクティビティ スタックの上に重ねられています。

分割の設定はアクティビティ A とアクティビティ B の設定によって定義されます。

<SplitPairRule>
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

一番上のアクティビティを終了しても分割は保持されます。

プライマリ コンテナにアクティビティ A、セカンダリ コンテナにアクティビティ B と C を配置し、B の上に C を重ねた分割。C が終了すると、アクティビティ分割に A と B が残ります。

セカンダリ コンテナの一番下(ルート)にあるアクティビティを終了しても、その上にあるアクティビティは削除されません。そのため、分割も保持されます。

プライマリ コンテナにアクティビティ A、セカンダリ コンテナにアクティビティ B と C を配置し、B の上に C を重ねた分割。B が終了すると、アクティビティ分割に A と C が残ります。

プライマリ アクティビティとともにセカンダリ アクティビティを終了するなど、アクティビティをまとめて終了するための追加のルールも実施されます。

<SplitPairRule
    window:finishSecondaryWithPrimary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

プライマリ コンテナにアクティビティ A、セカンダリ コンテナにアクティビティ B と C を配置し、B の上に C を重ねた分割。A が終了すると、B と C も終了します。

分割がプライマリとセカンダリをまとめて終了するように設定されている場合は次のようになります。

<SplitPairRule
    window:finishPrimaryWithSecondary="always"
    window:finishSecondaryWithPrimary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

プライマリ コンテナにアクティビティ A、セカンダリ コンテナにアクティビティ B と C を配置し、B の上に C を重ねた分割。C が終了すると、アクティビティ分割に A と B が残ります。

プライマリ コンテナにアクティビティ A、セカンダリ コンテナにアクティビティ B と C を配置し、B の上に C を重ねた分割。B が終了すると、アクティビティ分割に A と C が残ります。

プライマリ コンテナにアクティビティ A、セカンダリ コンテナにアクティビティ B と C を配置し、B の上に C を重ねた分割。A が終了すると、B と C も終了します。

ランタイムに分割プロパティを変更する

アクティブで表示されている分割のプロパティは変更できません。分割ルールの変更は、追加のアクティビティ起動と新しいコンテナに影響しますが、既存のアクティブな分割には影響しません。

アクティブな分割のプロパティを変更するには、横のアクティビティまたは分割の中のアクティビティを終了し、新しい設定でアクティビティを再び横に起動します。

動的分割プロパティ

Jetpack WindowManager 1.4 以降でサポートされている Android 15(API レベル 35)以降では、アクティビティの埋め込み分割の構成を可能にする動的機能が提供されています。たとえば、次のような機能があります。

  • ペインの拡大: インタラクティブでドラッグ可能な分割線により、ユーザーは分割プレゼンテーションでペインのサイズを変更できます。
  • アクティビティ スタックの固定: ユーザーは、1 つのコンテナ内のコンテンツを固定し、そのコンテナ内のナビゲーションを他のコンテナ内のナビゲーションから分離できます。
  • ダイアログの全画面表示の暗くする: ダイアログを表示するときに、タスクウィンドウ全体を暗くするか、ダイアログを開いたコンテナのみを暗くするかをアプリで指定できます。

ペインの展開

ペインの展開を使用すると、デュアルペイン レイアウトの 2 つのアクティビティに割り当てられる画面スペースの量をユーザーが調整できます。

ウィンドウの分割線の外観をカスタマイズし、分割線のドラッグ可能な範囲を設定するには、次の操作を行います。

  1. DividerAttributes のインスタンスを作成する

  2. 分割線の属性をカスタマイズします。

    • color: ドラッグ可能なペイン分割線の色。

    • widthDp: ドラッグ可能なペイン分割線の幅。WIDTH_SYSTEM_DEFAULT に設定すると、システムが分割線の幅を決定します。

    • ドラッグ範囲: どちらかのペインが占有できる画面の最小割合。0.33 ~ 0.66 の範囲で指定できます。DRAG_RANGE_SYSTEM_DEFAULT に設定すると、システムがドラッグ範囲を決定します。

Kotlin

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

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

アクティビティ スタックの固定

アクティビティ スタックの固定を使用すると、分割ウィンドウのいずれかを固定して、ユーザーが他方のウィンドウ内で操作している間もアクティビティをそのまま維持できます。アクティビティ スタックの固定により、マルチタスク エクスペリエンスが向上します。

アプリでアクティビティ スタックの固定を有効にするには、次の操作を行います。

  1. 固定するアクティビティのレイアウト ファイル(リストと詳細レイアウトの詳細アクティビティなど)にボタンを追加します。

    <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>
    
  2. アクティビティの onCreate() メソッドで、ボタンに onclick リスナーを設定します。

    Kotlin

    pinButton = findViewById(R.id.pinButton)
    pinButton.setOnClickListener {
        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)
    }

    Java

    Button pinButton = findViewById(R.id.pinButton);
    pinButton.setOnClickListener( (view) => {
        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(getTaskId(), pinSplitRule);
    });

ダイアログの全画面表示を暗くする

通常、アクティビティはディスプレイを暗くしてダイアログに注意を向けさせます。アクティビティの埋め込みでは、統一された UI エクスペリエンスを実現するために、ダイアログを開いたアクティビティを含むペインだけでなく、デュアルペイン ディスプレイの両方のペインを暗くする必要があります。

WindowManager 1.4 以降では、ダイアログが開くと、デフォルトでアプリのウィンドウ全体が暗くなります(EmbeddingConfiguration.DimAreaBehavior.ON_TASK を参照)。

ダイアログを開いたアクティビティのコンテナのみを暗くするには、EmbeddingConfiguration.DimAreaBehavior.ON_ACTIVITY_STACK を使用します。

分割からアクティビティを抽出してフルウィンドウにする

横のアクティビティをフルウィンドウ表示する新しい設定を作成してから、同じインスタンスに解決されるインテントでそのアクティビティを再起動します。

ランタイムに分割のサポートを確認する

アクティビティの埋め込みは Android 12L(API レベル 32)以上でサポートされますが、それ以前のプラットフォーム バージョンを搭載している一部のデバイスでも利用できます。この機能が利用可能かどうかをランタイムに確認するには、SplitController.splitSupportStatus プロパティまたは SplitController.getSplitSupportStatus() メソッドを使用します。

Kotlin

if (SplitController.getInstance(this).splitSupportStatus ==
     SplitController.SplitSupportStatus.SPLIT_AVAILABLE) {
     // Device supports split activity features.
}

Java

if (SplitController.getInstance(this).getSplitSupportStatus() ==
     SplitController.SplitSupportStatus.SPLIT_AVAILABLE) {
     // Device supports split activity features.
}

分割がサポートされていない場合、アクティビティはアクティビティ スタックの上で起動されます(非アクティビティ埋め込みモデルに従います)。

システムのオーバーライドを禁止する

Android デバイスのメーカー(相手先ブランド製品製造企業、OEM)は、デバイス システムの機能としてアクティビティの埋め込みを実装できます。システムは、アプリのウィンドウ処理動作をオーバーライドして、マルチアクティビティ アプリの分割ルールを指定します。システムによるオーバーライドが行われると、マルチアクティビティ アプリはシステム定義のアクティビティ埋め込みモードになります。

システムのアクティビティの埋め込みにより、アプリに変更を加えることなく、リスト詳細などのマルチペイン レイアウトを使用してアプリの表示を改善できます。ただし、誤ったレイアウトでアプリが表示されたり、バグや、アプリ側で実装するアクティビティ埋め込みとの競合を引き起こしたりすることもあります。

システムのアクティビティの埋め込みを禁止または許可するには、アプリ マニフェスト ファイルでプロパティを設定します。次に例を示します。

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

プロパティ名は Jetpack WindowManager の WindowProperties オブジェクトで定義されます。アクティビティの埋め込みをアプリが実装する場合、またはシステムがアクティビティの埋め込みのルールをアプリに適用しないようにする場合は、値を false に設定します。値を true に設定すると、システムはシステム定義のアクティビティの埋め込みをアプリに適用できるようになります。

制約、制限、注意事項

  • タスク内の他のアクティビティを整理したり、埋め込んだりできるのは、タスクのホストアプリ(タスクのルート アクティビティのオーナーとして識別)に限られます。埋め込みと分割をサポートするアクティビティが、別のアプリに属するタスクで実行される場合、それらのアクティビティでは埋め込みと分割が機能しません。
  • アクティビティは 1 つのタスク内でのみ整理できます。新しいタスクでアクティビティを起動すると、常に既存の分割の外部で新しく開いたウィンドウに配置されます。
  • 整理して分割に配置できるのは、同じプロセスのアクティビティに限られます。異なるプロセスのアクティビティを把握する方法がないため、SplitInfo コールバックは同じプロセスに属するアクティビティのみをレポートします。
  • 各ペアまたは単一のアクティビティ ルールは、ルールが登録された後に発生するアクティビティ起動にしか適用されません。現在のところ、既存の分割やその視覚的なプロパティを更新する方法はありません。
  • 分割ペアのフィルタ設定は、アクティビティを起動するときに使用するインテントと完全に一致する必要があります。この照合はアプリプロセスから新しいアクティビティが起動された時点で起こるため、暗黙的インテントを使用する場合、システム プロセスの後半で解決されるコンポーネント名が不明となることがあります。起動時にコンポーネント名が不明な場合は、代わりにワイルドカード(「*/*」)を使用し、インテントのアクションに基づいてフィルタリングを実施できます。
  • 現在のところ、コンテナ間でアクティビティを移動する方法や、分割の作成後にその内外にアクティビティを移動する方法はありません。分割は、ルールが一致する新しいアクティビティが起動したときにのみ、WindowManager ライブラリによって作成され、分割コンテナ内の最後のアクティビティが終了したときに破棄されます。
  • アクティビティは構成変更に伴って再起動される場合があるため、分割が作成または削除されてアクティビティの境界が変化すると、それまでのインスタンスが完全に破棄され新たなインスタンスが再作成される可能性があります。そのため、アプリ デベロッパーは、ライフサイクル コールバックから新しいアクティビティを起動する場合などに注意する必要があります。
  • アクティビティの埋め込みをサポートするには、デバイスに window extensions インターフェースが組み込まれている必要があります。Android 12L(API レベル 32)以上を搭載するほぼすべての大画面デバイスには、このインターフェースが組み込まれています。ただし、複数のアクティビティの実行に対応していない大画面デバイスには、window extensions インターフェースが組み込まれていない場合があります。大画面デバイスがマルチウィンドウ モードをサポートしていない場合は、アクティビティの埋め込みがサポートされない可能性があります。

参考情報