デスクトップ ウィンドウをサポートする

デスクトップ ウィンドウ機能を使用すると、複数のアプリをサイズ変更可能なアプリ ウィンドウで同時に実行でき、デスクトップのような多機能な操作性を実現できます。

図 1 は、パソコン ウィンドウ機能が有効になっている画面の構成を示しています。注意事項:

  • 複数のアプリを並べて同時に実行できます。
  • タスクバーはディスプレイの下部に固定され、実行中のアプリが表示されています。ユーザーはアプリを固定して、すばやくアクセスできます。
  • カスタマイズ可能な新しいヘッダーバーにより、各ウィンドウの上部に最小化や最大化などのコントロールが表示されます。
タブレットのディスプレイに、サイズ変更可能なウィンドウで複数のアプリが実行され、下部にタスクバーが表示されている。
図 1. タブレットのデスクトップ ウィンドウ。

デフォルトでは、Android タブレットでアプリは全画面表示で開きます。デスクトップ ウィンドウでアプリを起動するには、図 2 に示すように、画面上部のウィンドウ ハンドルを長押しして、UI 内でハンドルをドラッグします。

アプリがデスクトップ ウィンドウで開いている場合、他のアプリもデスクトップ ウィンドウで開きます。

図 2.アプリ ウィンドウのハンドルを長押ししてドラッグし、デスクトップ ウィンドウ表示にします。

ユーザーは、ハンドルをタップまたはクリックしたとき、またはキーボード ショートカット Meta キー(Windows、Command、または Search)+ Ctrl + 下矢印を使用したときにウィンドウ ハンドルの下に表示されるメニューから、デスクトップ ウィンドウ分割を呼び出すこともできます。

ユーザーは、アクティブなウィンドウをすべて閉じるか、デスクトップ ウィンドウの上部にあるウィンドウ ハンドルを掴んでアプリを画面の上部にドラッグすることで、デスクトップ ウィンドウを終了します。キーボード ショートカットの Meta + H でも、デスクトップ ウィンドウ表示を終了して、アプリを再度全画面表示で実行できます。

デスクトップ ウィンドウ表示に戻るには、[最近] 画面でデスクトップ スペース タイルをタップまたはクリックします。

サイズ変更と互換モード

デスクトップ ウィンドウでは、向きがロックされているアプリのサイズを自由に調整できます。つまり、アクティビティが縦向きにロックされていても、ユーザーはアプリのサイズを変更して横向きのウィンドウにすることができます。

図 3. 縦向き限定のアプリのウィンドウを横向きにサイズ変更する。

サイズ変更不可(つまり resizeableActivity = false)と宣言されたアプリの UI は、同じアスペクト比を維持しながらスケーリングされます。

図 4. サイズ変更できないアプリの UI は、ウィンドウのサイズ変更に合わせて拡大縮小されます。

向きをロックする、またはサイズ変更不可として宣言されているカメラアプリでは、カメラ ビューファインダーが特別に扱われます。ウィンドウは完全にサイズ変更可能ですが、ビューファインダーのアスペクト比は同じままです。アプリが常に縦向きまたは横向きで実行されることを前提として、アプリがプレビューまたはキャプチャされた画像の向きやアスペクト比の誤った計算につながる前提をハードコードしたり、その他の方法で設定したりすると、画像が引き伸ばされたり、横向きになったり、上下逆になったりします。

アプリが完全にレスポンシブなカメラ ビューファインダーを実装する準備が整うまで、この特別な処理により、誤った想定がもたらす影響を軽減する、より基本的なユーザー エクスペリエンスが提供されます。

カメラアプリの互換モードについて詳しくは、デバイスの互換モードをご覧ください。

図 5. ウィンドウのサイズが変更されても、カメラのビューファインダーのアスペクト比が維持されます。

カスタマイズ可能なヘッダー インセット

デスクトップ ウィンドウで実行されているすべてのアプリには、イマーシブ モードでもヘッダーバーがあります。アプリのコンテンツがヘッダーバーで隠れていないことを確認します。ヘッダーバーはキャプション バー インセット タイプ(WindowInsets.Companion.captionBar())です。ビューでは、WindowInsets.Type.captionBar() となり、システムバーの一部です。

インセットの処理について詳しくは、アプリでコンテンツをエッジ ツー エッジで表示し、Compose でウィンドウ インセットを処理するをご覧ください。

ヘッダーバーもカスタマイズ可能です。Android 15 では、ヘッダーバーを透明にして、アプリがヘッダーバー内にカスタム コンテンツを描画できるようにする外観タイプ APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND が導入されました。

アプリは、コンテンツの上部をキャプション バーのようにスタイル設定する責任を負うことになります(背景、カスタム コンテンツなど)。ただし、システム キャプション要素(閉じるボタンと最大化ボタン)は除きます。これらは、アプリの最上部にある透明なキャプション バーにシステムによって描画されます。

アプリは、ステータスバーやナビゲーション バーが切り替えられるのと同様に、APPEARANCE_LIGHT_CAPTION_BARS を使用して、ライトモードとダークモードのキャプション内のシステム要素の外観を切り替えることができます。

Android 15 では、アプリがキャプション バーのインセットをより詳細にイントロスペクションできる WindowInsets#getBoundingRects() メソッドも導入されました。アプリは、システムがシステム要素を描画する領域と、アプリがシステム要素と重複することなくカスタム コンテンツを配置できる未使用の領域を区別できます。

API から返される Rect オブジェクトのリストは、避けるべきシステム領域を示します。残りのスペース(キャプション バーの Insets から長方形を差し引いて計算)は、アプリがシステム要素と重ならずに、入力を受け取ることができる領域です。

カスタム ヘッダーを実装する前後の Chrome。
図 6. カスタム ヘッダーを実装する前後の Chrome。

カスタム ヘッダーのシステム ジェスチャー除外矩形を設定するには、ビューまたはコンポーザブルで次の処理を実装します。

// In a custom View's onLayout or a similar lifecycle method
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
    super.onLayout(changed, left, top, right, bottom)
    if (changed) {
        // Calculate the height of your custom header
        val customHeaderHeight = 100 // Replace with your actual header height in pixels

        // Create a Rect covering your custom header area
        val exclusionRect = Rect(0, 0, width, customHeaderHeight)

        // Set the exclusion rects for the system
        systemGestureExclusionRects = listOf(exclusionRect)
    }
}

マルチタスクとマルチインスタンスのサポート

マルチタスクはデスクトップ ウィンドウ機能の中核であり、アプリの複数のインスタンスを許可することで、ユーザーの生産性を大幅に向上させることができます。

Android 15 では、PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI が導入されました。アプリはこれを設定して、アプリを複数のインスタンスとして起動できるように、システム UI を表示することを指定できます。

アプリの AndroidManifest.xml<activity> タグ内で PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI を宣言できます。

<activity
    android:name=".MyActivity"
    android:exported="true"
    android:resizeableActivity="true">
    <meta-data
        android:name="android.window.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI"
        android:value="true" />
</activity>

ドラッグ ジェスチャーでアプリ インスタンスを管理する

マルチ ウィンドウ モードでは、ユーザーはアプリのウィンドウからビュー要素をドラッグすることで、新しいアプリ インスタンスを開始できます。ユーザーは、同じアプリのインスタンス間で要素を移動することもできます。

図 7. タブをデスクトップ ウィンドウからドラッグして、Chrome の新しいインスタンスを開始します。

Android 15 では、ドラッグ動作をカスタマイズするための 2 つのフラグが導入されています。

  • DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG: 可視ウィンドウがドロップを処理しない場合、未処理のドラッグをシステムに委任して開始することを示します。このフラグを使用する場合、呼び出し元は、起動するアクティビティへの不変の IntentSender を含む ClipData.ItemClipData に指定する必要があります(ClipData.Item.Builder#setIntentSender() を参照)。システムは、現在の画面サイズやウィンドウ モードなどの要因に基づいて、インテントを起動するかどうかを決定できます。システムがインテントを起動しない場合、通常のドラッグフローによってインテントはキャンセルされます。

  • DRAG_FLAG_GLOBAL_SAME_APPLICATION: ドラッグ オペレーションがウィンドウの境界を越えることができることを示します(同じアプリケーションの複数のインスタンスの場合)。

    このフラグが設定された状態で [startDragAndDrop()][20] が呼び出されると、同じアプリに属する表示中のウィンドウのみがドラッグ オペレーションに参加して、ドラッグされたコンテンツを受け取ることができます。

次の例は、startDragAndDrop() でこれらのフラグを使用する方法を示しています。

// Assuming 'view' is the View that initiates the drag
view.setOnLongClickListener {
    // Create an IntentSender for the activity you want to launch
    val launchIntent = Intent(view.context, NewInstanceActivity::class.java)
    val pendingIntent = PendingIntent.getActivity(
        view.context,
        0,
        launchIntent,
        PendingIntent.FLAG_IMMUTABLE // Ensure the PendingIntent is immutable
    )

    // Build the ClipData.Item with the IntentSender
    val item = ClipData.Item.Builder()
        .setIntentSender(pendingIntent.intentSender)
        .build()

    // Create ClipData with a simple description and the item
    val dragData = ClipData(
        ClipDescription("New Instance Drag", arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN)),
        item
    )

    // Combine the drag flags
    val dragFlags = View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG or
                    View.DRAG_FLAG_GLOBAL_SAME_APPLICATION

    // Start the drag operation
    view.startDragAndDrop(
        dragData,                     // The ClipData to drag
        View.DragShadowBuilder(view), // A visual representation of the dragged item
        null,                         // Local state object (not used here)
        dragFlags                     // The drag flags
    )
    true // Indicate that the long click was consumed
}
図 8.Chrome アプリの 2 つのインスタンス間でタブを移動します。

その他の最適化

アプリの起動をカスタマイズし、デスクトップ ウィンドウから全画面にアプリを切り替えます。

デフォルトのサイズと位置を指定する

サイズ変更可能なアプリであっても、ユーザーに価値を提供するために大きなウィンドウが必要なわけではありません。ActivityOptions#setLaunchBounds() メソッドを使用すると、アクティビティの起動時にデフォルトのサイズと位置を指定できます。

アクティビティの起動境界を設定する方法の例を次に示します。

val options = ActivityOptions.makeBasic()

// Define the desired launch bounds (left, top, right, bottom in pixels)
val launchBounds = Rect(100, 100, 700, 600) // Example: 600x500 window at (100,100)

// Apply the launch bounds to the ActivityOptions
options.setLaunchBounds(launchBounds)

// Start the activity with the specified options
val intent = Intent(this, MyActivity::class.java)
startActivity(intent, options.toBundle())

デスクトップ スペースから全画面表示にする

アプリは Activity#requestFullScreenMode() を呼び出すことで全画面表示にできます。このメソッドは、デスクトップ ウィンドウからアプリを直接全画面表示します。

アクティビティから全画面モードをリクエストするには、次のコードを使用します。

// In an Activity
fun enterFullScreen() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // Android 15 (U)
        requestFullScreenMode()
    }
}