支持桌面窗口化

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

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

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

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

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

图 2. 按住并拖动应用窗口句柄,进入桌面窗口模式。

用户还可以通过以下方式调用桌面窗口化功能:点按或点击窗口句柄时,从窗口句柄下方显示的菜单中调用;或者使用键盘快捷键 Meta 键(Windows、Command 或 Search)+ Ctrl + 向下键

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

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

可调整大小的模式和兼容模式

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

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

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

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

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

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

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

图 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,应用可以设置该属性来指定应为应用显示系统界面,以便将应用作为多个实例启动。

您可以在应用的 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 引入了两个用于自定义拖动行为的标志:

以下示例演示了如何将这些标志与 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 应用的两个实例之间移动标签页。

其他优化

自定义应用启动,并将应用从桌面窗口化模式转换为全屏模式。

指定默认大小和位置

并非所有应用都需要大窗口才能为用户提供价值,即使是可调整大小的应用也是如此。您可以使用 ActivityOptions#setLaunchBounds() 方法在启动 activity 时指定默认大小和位置。

以下示例展示了如何为 activity 设置启动边界:

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() 进入全屏模式。 该方法可直接从桌面窗口显示全屏应用。

如需从 activity 请求全屏模式,请使用以下代码:

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