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

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

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

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

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

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

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

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

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

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

デスクトップのような環境に合わせてアプリのレイアウトを最適化する

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

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

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(): システムのヘッダーバーに合わせてコンポーザブルの高さを自動的に設定します。これにより、ピクセル値をハードコードすることなく、カスタム背景がキャプション領域を埋めることができます。

  • WindowInsets.captionBar: デスクトップ ウィンドウ コントロール([閉じる]、[最大化] など)のサイズを指定します。これにより、デスクトップ ウィンドウの開始時または終了時に UI を自動的にスケーリングまたは非表示にできます。

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

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

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

APPEARANCE_LIGHT_CAPTION_BARS を使用して、ライトモードとダークモードのシステム キャプション要素の外観を切り替えます。インセットにアクセスするには、Compose では WindowInsets.Companion.captionBar()、ビューでは 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 修飾子を使用します。そのラムダで、転送するデータを含む ClipData と、マルチインスタンスの動作を構成するフラグを渡して、DragAndDropTransferData を返します。

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() を呼び出すことで全画面表示にできます。このメソッドは、デスクトップ ウィンドウからアプリを全画面表示します。