支持桌面窗口化

借助窗口化模式,用户可以在可调整大小的应用窗口中同时运行多个应用,获享灵活多变、类似桌面设备的体验。

在图 1 中,您可以看到启用桌面窗口化模式后屏幕的组织方式。需注意的事项:

  • 用户可以同时并排运行多个应用。
  • 任务栏固定在显示屏底部,显示正在运行的应用。用户可以固定应用,以便快速访问。
  • 新的可自定义标题栏可装饰每个窗口的顶部,并提供最小化和最大化等控件。
图 1. 平板电脑上的窗口化模式。

默认情况下,应用会在 Android 平板电脑上以全屏模式打开。如需在窗口化模式下启动应用,请按住屏幕顶部的窗口手柄,然后在界面内拖动手柄,如图 2 所示。

当某个应用在桌面窗口化模式下打开时,其他应用也会在桌面窗口中打开。

图 2. 按住并拖动应用窗口手柄,即可进入窗口化模式。

用户还可以通过点按或点击窗口句柄,或使用键盘快捷键 Meta 键(Windows、Command 或 Search)+ Ctrl + 向下键,然后从窗口句柄下方显示的菜单中调用窗口化模式。

用户可以通过关闭所有有效窗口来退出窗口化模式,也可以通过抓取桌面窗口顶部的窗口句柄并将应用拖动到屏幕顶部来退出窗口化模式。Meta + H 键盘快捷键也会退出桌面窗口模式,并再次以全屏模式运行应用。

如需返回到窗口化模式,请在“最近”界面中点按或点击桌面空间功能块。

针对桌面设备类环境优化应用布局

由于屏幕空间更大、鼠标和键盘输入更精确,以及用户对高效率的期望,桌面版设计与移动版设计可能存在显著差异。

Jetpack WindowManager 提供了一个有主张的 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()
                }
        }
    }
}

如需了解详情,请参阅桌面设备设计

可调整大小性和兼容模式

在桌面窗口化模式下,锁定屏幕方向的应用可以自由调整大小。这意味着,即使 activity 锁定为纵向屏幕方向,用户仍可将应用调整为横向屏幕方向窗口。

图 3. 将仅限纵向的应用的窗口调整为横向。

声明为不可调整大小的应用(即 resizeableActivity = false)会在保持相同宽高比的情况下缩放界面。

图 4. 不可调整大小的应用的界面会随着窗口大小的调整而缩放。

锁定屏幕方向或声明为不可调整大小的相机应用会对其相机取景器进行特殊处理:窗口完全可调整大小,但取景器保持相同的宽高比。通过假设应用始终以纵向或横向模式运行,应用会硬编码或以其他方式做出假设,从而导致预览或拍摄的图片屏幕方向或宽高比计算错误,最终导致图片拉伸、方向有误或倒置。

在应用准备好实现完全响应式相机取景器之前,特殊处理可提供更基本的用户体验,从而减轻错误假设可能造成的影响。

如需详细了解相机应用的兼容性模式,请参阅设备兼容性模式

图 5. 相机取景器在窗口调整大小时会保持其宽高比。

可自定义的标题边衬区

在桌面窗口化模式下运行的所有应用都有标题栏,即使在沉浸模式下也是如此。您可以自定义此栏,以防止应用的内容被遮挡,并直接在标题空间中绘制自定义界面元素。

实施自定义标头前后的 Chrome。
图 6.实施自定义标头前后的 Chrome。

实现

如需在标题栏中绘制自定义内容,第一步是将标题栏背景设为透明。为此,您可以将 APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND 标志与 WindowInsetsController 搭配使用。

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):配置系统栏的视觉样式。第一个参数定义目标外观标志,而第二个参数充当掩码,用于控制修改哪些特定标志。

  • windowInsetsTopHeight():自动设置可组合项的高度以匹配系统的标题栏,帮助自定义背景填充字幕区域,而无需对像素值进行硬编码。

  • WindowInsets.captionBar:提供桌面窗口控制(关闭最大化等)的尺寸,使界面能够在进入或离开窗口化模式时自动缩放或隐藏。

如需了解详情,请参阅窗口边衬区简介。除了标题之外,您还可以在字幕栏中显示其他界面元素,例如标签页(如 Google Chrome 中的标签页)、搜索栏或个人资料头像。

界面

为避免界面与系统按钮重叠,Android 15 提供了 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 中设置此属性,您可以指定系统界面应提供用于以多个实例启动应用的选项(例如“新窗口”按钮)。

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

注意:在窗口化模式和其他多窗口模式环境中,新任务会在新窗口中打开,因此每当应用启动多个任务时,请务必仔细检查用户历程。

通过拖动手势管理应用实例

在多窗口模式下,用户可以通过将界面元素(例如标签页或文档)拖出应用窗口来启动新的应用实例。用户还可以在同一应用的不同实例之间移动元素。

图 7. 通过将标签页拖出桌面窗口来启动新的 Chrome 实例。

通过拖放转移数据

如需将可组合项配置为多实例拖放操作的拖动源,以允许用户将内容拖动到应用的另一个实例,或通过将内容放置到屏幕的空白区域来创建实例,请使用 dragAndDropSource 修饰符。在其 lambda 中,返回 DragAndDropTransferData,并传递包含要转移的数据的 ClipData 和用于配置多实例行为的标志。

Android 15 针对桌面风格的窗口化模式和多实例互动引入了两个关键标志

  • DRAG_FLAG_GLOBAL_SAME_APPLICATION:表示拖动操作可以跨越窗口边界(对于同一应用的多个实例)。当调用 startDragAndDrop() 时设置了此标志后,只有属于同一应用的可视窗口才能参与拖动操作并接收拖动的内容。

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

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 时使用的默认大小和位置。

从桌面空间进入全屏模式

应用可以通过调用 Activity#requestFullScreenMode() 进入全屏模式。该方法可直接从桌面窗口化模式全屏显示应用。