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

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

図 1 は、デスクトップ ウィンドウが有効になっている画面の構成を示しています。注意事項:

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

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

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

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

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

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

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

パソコンのような環境向けにアプリのレイアウトを最適化する

パソコン向けのエクスペリエンスの設計は、画面スペースの拡大、マウスとキーボードの入力の精度、高い生産性への期待などにより、モバイル設計とは大きく異なる場合があります。

Jetpack WindowManager は、情報密度が高く、ナビゲーション パターンが異なり、マウス操作が最適化されているデスクトップ UI を表示するタイミングをデベロッパーが判断するのに役立つ、独自の API を提供します。

lifecycleScope.launch(Dispatchers.Main) {
    lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
        windowInfoTracker.windowEngagementInfo(this@DesktopWindowingActivity)
            .collect { windowEngagementInfo ->
                if(windowEngagementInfo.hasEngagementMode(WindowEngagementInfo.EngagementMode.PRECISE_POINTER)){
                    showDesktopOptimizedUI()
                }else {
                    showTouchOptimizedUI()
                }
        }
    }
}

詳しくは、パソコン向けのデザインをご覧ください。

サイズ変更と互換モード

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

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

サイズ変更不可(つまり resizeableActivity = false)と宣言されたアプリは、同じアスペクト比を維持したまま UI が拡大縮小されます。

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

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

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

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

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

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

デスクトップ ウィンドウで実行されているすべてのアプリには、没入モードであってもヘッダーバーがあります。このバーをカスタマイズして、アプリのコンテンツが隠れないようにしたり、カスタム UI 要素をヘッダー スペースに直接描画したりできます。

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

実装

ヘッダーバーにカスタム コンテンツを描画する最初のステップは、ヘッダーバーの背景を透明にすることです。これを行うには、WindowInsetsController を指定して APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND フラグを使用します。

window.insetsController?.setSystemBarsAppearance(
    WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND,
    WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND
)

ヘッダーバーが透明になったら、アプリのデザインに合わせてヘッダー領域のスタイルを設定できます。WindowInsets.isCaptionBarVisible を使用してバーが存在するかどうかを検出し、レイアウトに適切な高さまたはパディングを適用します。

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun CaptionBar() {
    if (WindowInsets.isCaptionBarVisible) {
        Row(
            modifier = Modifier
                .windowInsetsTopHeight(WindowInsets.captionBar)
                .fillMaxWidth()
                .background(if (isSystemInDarkTheme()) Color.White else Color.Black),
            horizontalArrangement = Arrangement.Center,
            verticalAlignment = Alignment.CenterVertically
        ) {
            Text(
                text = "Caption Bar Title",
                style = MaterialTheme.typography.titleMedium,
                modifier = Modifier.padding(4.dp)
            )
        }
    }
}

  • setSystemBarsAppearance(appearance,mask): システムバーのビジュアル スタイルを構成します。最初のパラメータはターゲットの外観フラグを定義し、2 番目のパラメータはどの特定のフラグが変更されるかを制御するマスクとして機能します。

  • windowInsetsTopHeight(): システムのヘッダーバーに合わせて Composable の高さを自動的に設定します。これにより、カスタムの背景でキャプション領域を埋める際に、ピクセル値をハードコードする必要がなくなります。

  • WindowInsets.captionBar: デスクトップ ウィンドウ コントロール(閉じる最大化など)の寸法を提供します。これにより、デスクトップ ウィンドウの出入り時に UI を自動的にスケーリングまたは非表示にできます。

詳しくは、ウィンドウのインセットについてをご覧ください。タイトルに加えて、Google Chrome のタブ、検索バー、プロフィールのアバターなど、他の UI 要素をキャプション バーに表示できます。

ユーザー インターフェース

Android 15 では、UI がシステムボタンと重ならないように、WindowInsets#getBoundingRects() メソッドが用意されています。このメソッドは、システム要素が占有する領域を表す Rect オブジェクトのリストを返します。キャプション バーの残りのスペースは、カスタム コンテンツを安全に配置できるセーフゾーンです。

APPEARANCE_LIGHT_CAPTION_BARS を使用して、ライトモードとダークモードのシステム キャプション要素の表示を切り替えます。インセットにアクセスするには、Compose では WindowInsets.Companion.captionBar() を、View では WindowInsets.Type.captionBar() を使用します。

詳しくは、ウィンドウのインセットについてをご覧ください。

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

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

Android 15 以降では、PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI を使用できます。AndroidManifest.xml でこのプロパティを設定すると、システム UI がアプリを複数のインスタンスで起動するためのオプション([新しいウィンドウ] ボタンなど)を提供することを指定できます。

<application>
    <property
        android:name="android.window.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI"
        android:value="true" />
</application>

注: デスクトップ ウィンドウやその他のマルチウィンドウ環境では、新しいタスクは新しいウィンドウで開きます。アプリが複数のタスクを開始するたびに、ユーザー ジャーニーを再確認してください。

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

マルチウィンドウ モードでは、ユーザーはアプリのウィンドウから UI 要素(タブやドキュメントなど)をドラッグして、新しいアプリ インスタンスを開始できます。ユーザーは、同じアプリの異なるインスタンス間で要素を移動することもできます。

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

ドラッグ&ドロップでデータを転送する

ユーザーがコンテンツをアプリの別のインスタンスにドラッグしたり、画面の空き領域にコンテンツをドロップして新しいインスタンスを作成したりできるように、マルチインスタンスのドラッグ&ドロップのドラッグソースとしてコンポーザブルを構成するには、dragAndDropSource 修飾子を使用します。ラムダで DragAndDropTransferData を返し、転送するデータを含む ClipData と、マルチインスタンスの動作を構成するフラグを渡します。

Android 15 では、デスクトップ スタイルのウィンドウとマルチインスタンスの操作に関する 2 つの重要なフラグが導入されています。

  • DRAG_FLAG_GLOBAL_SAME_APPLICATION: ドラッグ操作がウィンドウの境界を越えることができることを示します(同じアプリの複数のインスタンスの場合)。このフラグを設定して startDragAndDrop() を呼び出すと、同じアプリに属する可視ウィンドウのみがドラッグ操作に参加して、ドラッグされたコンテンツを受け取ることができます。

Modifier.dragAndDropSource { _ ->
    DragAndDropTransferData(
        clipData = ClipData.newPlainText("label", "Your data"),
        flags = View.DRAG_FLAG_GLOBAL_SAME_APPLICATION
    )
}

  • DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG: 他のウィンドウがドロップを処理しない場合、ユーザーはドラッグしたコンテンツを画面の空の領域にドロップして、アプリの新しいインスタンスを開始できます。
    • このフラグを使用する場合は、ClipData.Item.Builder#setIntentSender() を使用して IntentSender を指定する必要があります。これは、処理されないドロップが発生した場合に、システムが新しいアクティビティを起動するために使用します。

Modifier.dragAndDropSource { _ ->
    val intent = Intent.makeMainActivity(activity.componentName).apply {
        putExtra("EXTRA_ITEM_ID", itemId)
        flags = Intent.FLAG_ACTIVITY_NEW_TASK or
                Intent.FLAG_ACTIVITY_MULTIPLE_TASK or
                Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT
    }

    val pendingIntent = PendingIntent.getActivity(
        activity, 0, intent, PendingIntent.FLAG_IMMUTABLE
    )

    val data = ClipData(
        "Item $itemId",
        arrayOf(ClipDescription.MIMETYPE_TEXT_INTENT),
        ClipData.Item.Builder().setIntentSender(pendingIntent.intentSender).build()
    )

    DragAndDropTransferData(
        clipData = data,
        flags = View.DRAG_FLAG_GLOBAL_SAME_APPLICATION or
                View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG,
    )
}

転送されたデータを受信する

別のインスタンスからデータを受け入れるには、dragAndDropTarget 修飾子を使用します。データが別のインスタンスまたはアプリから取得される場合は、権限を明示的にリクエストする必要があります。

Modifier.dragAndDropTarget(
    shouldStartDragAndDrop = { event ->
        event.toAndroidDragEvent().clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)
    },
    target = object : DragAndDropTarget {
        override fun onDrop(event: DragAndDropEvent): Boolean {
            requestDragAndDropPermissions(activity, event.toAndroidDragEvent())
            val clipData = event.toAndroidDragEvent().clipData
            val item = clipData?.getItemAt(0)?.text
            if (item != null) {
                // Process the dropped text item here
            }
            return item != null
        }
    }
)

主な手順:

  • フィルタ: shouldStartDragAndDrop を使用して、受信データ(MIME タイプ)がサポートされているかどうかを確認します。
  • 権限: requestDragAndDropPermissions(event) を呼び出してデータにアクセスします。
  • 処理: onDrop コールバックでデータを抽出します。

その他の最適化

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

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

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

デスクトップ スペースから全画面表示に切り替える

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