使用画中画 (PiP) 添加视频

从 Android 8.0(API 级别 26)开始,Android 允许活动以画中画 (PiP) 模式启动。PiP 是一种特殊的多分屏模式,主要用于视频播放。它允许用户在屏幕一角固定的小窗口中观看视频,同时在应用程序之间导航或浏览主屏幕上的内容。

PiP 利用 Android 7.0 中提供的多分屏 API 提供固定的视频叠加窗口。要向您的应用添加 PiP,您需要注册支持 PiP 的活动,根据需要将您的活动切换到 PiP 模式,并确保在活动处于 PiP 模式时隐藏 UI 元素并继续视频播放。

PiP 窗口显示在屏幕的最顶层,位于系统选择的角落。

运行 Android 14(API 级别 34)或更高版本的兼容 Android TV OS 设备也支持 PiP。虽然有很多相似之处,但在使用 电视上的 PiP 时需要考虑其他事项。

用户如何与 PiP 窗口交互

用户可以将 PiP 窗口拖动到另一个位置。从 Android 12 开始,用户还可以

  • 轻点窗口以显示全屏切换、关闭按钮、设置按钮以及您的应用提供的自定义操作(例如,播放控件)。

  • 双击窗口以在当前 PiP 大小和最大或最小 PiP 大小之间切换——例如,双击最大化窗口将其最小化,反之亦然。

  • 通过将其拖动到左侧或右侧边缘来隐藏窗口。要取消隐藏窗口,请轻点隐藏窗口的可见部分或将其拖出。

  • 使用捏合缩放调整 PiP 窗口的大小。

您的应用控制当前活动何时进入 PiP 模式。以下是一些示例

  • 当用户点击主屏幕按钮或向上滑动到主屏幕时,活动可以进入 PiP 模式。这就是 Google 地图在用户同时运行另一个活动时继续显示路线指示的方式。

  • 当用户从视频导航回浏览其他内容时,您的应用可以将视频移动到 PiP 模式。

  • 当用户观看内容剧集的结尾时,您的应用可以将视频切换到 PiP 模式。主屏幕显示有关该系列下一集的促销或摘要信息。

  • 您的应用可以为用户提供一种在观看视频时排队更多内容的方法。视频在 PiP 模式下继续播放,而主屏幕显示内容选择活动。

声明 PiP 支持

默认情况下,系统不会自动支持应用的 PiP。如果您希望在您的应用中支持 PiP,请通过将 android:supportsPictureInPicture 设置为 true 在清单中注册您的视频活动。此外,请指定您的活动处理布局配置更改,以便在 PiP 模式转换期间发生布局更改时,您的活动不会重新启动。

<activity android:name="VideoActivity"
    android:supportsPictureInPicture="true"
    android:configChanges=
        "screenSize|smallestScreenSize|screenLayout|orientation"
    ...

将您的活动切换到 PiP

从 Android 12 开始,您可以通过将 setAutoEnterEnabled 标志设置为 true 将您的活动切换到 PiP 模式。使用此设置,活动会根据需要自动切换到 PiP 模式,而无需在 onUserLeaveHint 中显式调用 enterPictureInPictureMode()。这还有助于提供更流畅的转换。有关详细信息,请参阅 从手势导航使向 PiP 模式的转换更流畅

如果您面向 Android 11 或更低版本,则活动必须调用 enterPictureInPictureMode() 以切换到 PiP 模式。例如,以下代码在用户点击应用 UI 中的专用按钮时将活动切换到 PiP 模式

Kotlin

override fun onActionClicked(action: Action) {
    if (action.id.toInt() == R.id.lb_control_picture_in_picture) {
        activity?.enterPictureInPictureMode()
        return
    }
}

Java

@Override
public void onActionClicked(Action action) {
    if (action.getId() == R.id.lb_control_picture_in_picture) {
        getActivity().enterPictureInPictureMode();
        return;
    }
    ...
}

您可能希望包含将活动切换到 PiP 模式而不是转到后台的逻辑。例如,如果用户在应用正在导航时按下主屏幕或最近使用的按钮,则 Google 地图会切换到 PiP 模式。您可以通过覆盖 onUserLeaveHint() 来捕获此情况

Kotlin

override fun onUserLeaveHint() {
    if (iWantToBeInPipModeNow()) {
        enterPictureInPictureMode()
    }
}

Java

@Override
public void onUserLeaveHint () {
    if (iWantToBeInPipModeNow()) {
        enterPictureInPictureMode();
    }
}

推荐:为用户提供完善的 PiP 转换体验

Android 12 对全屏和 PiP 窗口之间的动画转换进行了重大的视觉改进。我们强烈建议实施所有适用的更改;完成后,这些更改会自动扩展到可折叠设备和平板电脑等大屏幕,而无需任何其他工作。

如果您的应用不包含适用的更新,则 PiP 转换仍然有效,但动画效果不佳。例如,从全屏转换到 PiP 模式会导致 PiP 窗口在转换期间消失,然后在转换完成后重新出现。

这些更改涉及以下内容。

  • 从手势导航使向 PiP 模式的转换更流畅
  • 为进入和退出 PiP 模式设置正确的 sourceRectHint
  • 禁用非视频内容的无缝调整大小

请参阅 Android Kotlin 画中画示例 作为启用完善转换体验的参考。

从手势导航使向 PiP 模式的转换更流畅

从 Android 12 开始,setAutoEnterEnabled 标志为使用手势导航(例如,从全屏向上滑动到主屏幕)将视频内容转换到 PiP 模式提供了更流畅的动画。

完成以下步骤以进行此更改,并参考此 示例 以供参考

  1. 使用 setAutoEnterEnabled 构建 PictureInPictureParams.Builder

    Kotlin

    setPictureInPictureParams(PictureInPictureParams.Builder()
        .setAspectRatio(aspectRatio)
        .setSourceRectHint(sourceRectHint)
        .setAutoEnterEnabled(true)
        .build())
    

    Java

    setPictureInPictureParams(new PictureInPictureParams.Builder()
        .setAspectRatio(aspectRatio)
        .setSourceRectHint(sourceRectHint)
        .setAutoEnterEnabled(true)
        .build());
    
  2. 尽早使用更新的 PictureInPictureParams 调用 setPictureInPictureParams。应用不会等待 onUserLeaveHint 回调(因为它在 Android 11 中所做的那样)。

    例如,如果纵横比发生更改,您可能希望在第一次播放和任何后续播放时调用 setPictureInPictureParams

  3. 调用 setAutoEnterEnabled(false),但仅在必要时调用。例如,如果当前播放处于暂停状态,您可能不希望进入 PiP。

为进入和退出 PiP 模式设置正确的 sourceRectHint

从 Android 8.0 引入 PiP 开始,setSourceRectHint 指示了转换到画中画后可见的活动区域——例如,视频播放器中的视频视图边界。

使用 Android 12,系统使用 sourceRectHint 在进入和退出 PiP 模式时实现更流畅的动画。

要正确设置进入和退出 PiP 模式的 sourceRectHint

  1. 使用正确的边界作为 sourceRectHint 构建 PictureInPictureParams。我们建议还将布局更改侦听器附加到视频播放器

    Kotlin

    val mOnLayoutChangeListener =
    OnLayoutChangeListener { v: View?, oldLeft: Int,
            oldTop: Int, oldRight: Int, oldBottom: Int, newLeft: Int, newTop:
            Int, newRight: Int, newBottom: Int ->
        val sourceRectHint = Rect()
        mYourVideoView.getGlobalVisibleRect(sourceRectHint)
        val builder = PictureInPictureParams.Builder()
            .setSourceRectHint(sourceRectHint)
        setPictureInPictureParams(builder.build())
    }
    
    mYourVideoView.addOnLayoutChangeListener(mOnLayoutChangeListener)
    

    Java

    private final View.OnLayoutChangeListener mOnLayoutChangeListener =
            (v, oldLeft, oldTop, oldRight, oldBottom, newLeft, newTop, newRight,
            newBottom) -> {
        final Rect sourceRectHint = new Rect();
        mYourVideoView.getGlobalVisibleRect(sourceRectHint);
        final PictureInPictureParams.Builder builder =
            new PictureInPictureParams.Builder()
                .setSourceRectHint(sourceRectHint);
        setPictureInPictureParams(builder.build());
    };
    
    mYourVideoView.addOnLayoutChangeListener(mOnLayoutChangeListener);
    
  2. 如果需要,请在系统开始退出过渡动画之前更新sourceRectHint。当系统即将退出画中画模式时,活动的视图层次结构将布局到其目标配置(例如,全屏)。应用可以将其根视图或目标视图(例如视频播放器视图)附加布局更改监听器,以检测事件并在动画开始前更新sourceRectHint

    Kotlin

    // Listener is called immediately after the user exits PiP but before animating.
    playerView.addOnLayoutChangeListener { _, left, top, right, bottom,
                        oldLeft, oldTop, oldRight, oldBottom ->
        if (left != oldLeft
            || right != oldRight
            || top != oldTop
            || bottom != oldBottom) {
            // The playerView's bounds changed, update the source hint rect to
            // reflect its new bounds.
            val sourceRectHint = Rect()
            playerView.getGlobalVisibleRect(sourceRectHint)
            setPictureInPictureParams(
                PictureInPictureParams.Builder()
                    .setSourceRectHint(sourceRectHint)
                    .build()
            )
        }
    }
    
    

    Java

    // Listener is called right after the user exits PiP but before animating.
    playerView.addOnLayoutChangeListener((v, left, top, right, bottom,
                        oldLeft, oldTop, oldRight, oldBottom) -> {
        if (left != oldLeft
            || right != oldRight
            || top != oldTop
            || bottom != oldBottom) {
            // The playerView's bounds changed, update the source hint rect to
            // reflect its new bounds.
            final Rect sourceRectHint = new Rect();
            playerView.getGlobalVisibleRect(sourceRectHint);
            setPictureInPictureParams(
                new PictureInPictureParams.Builder()
                    .setSourceRectHint(sourceRectHint)
                    .build());
        }
    });
    
    

禁用非视频内容的无缝调整大小

Android 12 添加了 setSeamlessResizeEnabled 标志,在调整画中画窗口中非视频内容的大小时,它提供了更流畅的交叉淡入淡出动画。以前,调整画中画窗口中非视频内容的大小可能会产生令人不快的视觉伪像。

要禁用非视频内容的无缝调整大小

Kotlin

setPictureInPictureParams(PictureInPictureParams.Builder()
    .setSeamlessResizeEnabled(false)
    .build())

Java

setPictureInPictureParams(new PictureInPictureParams.Builder()
    .setSeamlessResizeEnabled(false)
    .build());

在画中画模式下处理 UI

当活动进入或退出画中画 (PiP) 模式时,系统会调用 Activity.onPictureInPictureModeChanged()Fragment.onPictureInPictureModeChanged()

Android 15 引入了确保在进入画中画模式时过渡更加流畅的更改。这对那些在主 UI(进入画中画)之上覆盖了 UI 元素的应用很有益。

开发人员使用 onPictureInPictureModeChanged() 回调来定义切换覆盖 UI 元素可见性的逻辑。此回调在画中画进入或退出动画完成后触发。从 Android 15 开始,PictureInPictureUiState 类包含了一个新状态。

使用此新的 UI 状态,面向 Android 15 的应用会在画中画动画开始时观察到 Activity#onPictureInPictureUiStateChanged() 回调被调用,并使用 isTransitioningToPip()。当应用处于画中画模式时,许多 UI 元素与应用无关,例如包含建议、即将播放的视频、评分和标题等信息的视图或布局。当应用进入画中画模式时,使用 onPictureInPictureUiStateChanged() 回调隐藏这些 UI 元素。当应用从画中画窗口进入全屏模式时,使用 onPictureInPictureModeChanged() 回调显示这些元素,如下例所示。

Kotlin

override fun onPictureInPictureUiStateChanged(pipState: PictureInPictureUiState) {
        if (pipState.isTransitioningToPip()) {
          // Hide UI elements.
        }
    }

Java

@Override
public void onPictureInPictureUiStateChanged(PictureInPictureUiState pipState) {
        if (pipState.isTransitioningToPip()) {
          // Hide UI elements.
        }
    }

Kotlin

override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
        if (isInPictureInPictureMode) {
          // Unhide UI elements.
        }
    }

Java

@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
        if (isInPictureInPictureMode) {
          // Unhide UI elements.
        }
    }

此无关 UI 元素(对于画中画窗口)的快速可见性切换有助于确保画中画进入动画更加流畅且无闪烁。

覆盖这些回调以重新绘制活动的 UI 元素。请记住,在画中画模式下,您的活动显示在一个小窗口中。当应用处于画中画模式时,用户无法与应用的 UI 元素交互,并且小型 UI 元素的细节可能难以查看。具有最少 UI 的视频播放活动可提供最佳的用户体验。

如果您的应用需要为画中画提供自定义操作,请参阅此页面上的 添加控件。在活动进入画中画之前移除其他 UI 元素,并在活动再次变为全屏时恢复它们。

添加控件

当用户打开窗口菜单(在移动设备上点击窗口,或从电视遥控器中选择菜单)时,画中画窗口可以显示控件。

如果应用具有 活动媒体会话,则会显示播放、暂停、下一首和上一首控件。

您还可以通过使用 PictureInPictureParams.Builder.setActions() 在进入画中画模式之前构建 PictureInPictureParams 来显式指定自定义操作,并在使用 enterPictureInPictureMode(android.app.PictureInPictureParams)setPictureInPictureParams(android.app.PictureInPictureParams) 进入画中画模式时传递参数。请注意,如果您尝试添加超过 getMaxNumPictureInPictureActions() 的数量,您只会获得最大数量。

在画中画模式下继续视频播放

当您的活动切换到画中画时,系统会将活动置于暂停状态并调用活动的 onPause() 方法。如果活动在过渡到画中画模式时处于暂停状态,则视频播放不应暂停,而应继续播放。

在 Android 7.0 及更高版本中,当系统调用活动的 onStop()onStart() 时,您应该暂停和恢复视频播放。通过这样做,您可以避免在 onPause() 中检查您的应用是否处于画中画模式并显式地继续播放。

如果您未将 setAutoEnterEnabled 标志设置为 true,并且您需要在 onPause() 实现中暂停播放,请通过调用 isInPictureInPictureMode() 检查画中画模式并适当地处理播放。例如

Kotlin

override fun onPause() {
    super.onPause()
    // If called while in PiP mode, do not pause playback.
    if (isInPictureInPictureMode) {
        // Continue playback.
    } else {
        // Use existing playback logic for paused activity behavior.
    }
}

Java

@Override
public void onPause() {
    // If called while in PiP mode, do not pause playback.
    if (isInPictureInPictureMode()) {
        // Continue playback.
        ...
    } else {
        // Use existing playback logic for paused activity behavior.
        ...
    }
}

当您的活动从画中画模式切换回全屏模式时,系统会恢复您的活动并调用您的 onResume() 方法。

为画中画使用单个播放活动

在您的应用中,用户可能在主屏幕上浏览内容时选择一个新视频,而视频播放活动处于画中画模式。在新视频中以全屏模式播放现有播放活动,而不是启动可能让用户感到困惑的新活动。

为了确保为视频播放请求使用单个活动,并根据需要切换到或退出画中画模式,请在清单中将活动的 android:launchMode 设置为 singleTask

<activity android:name="VideoActivity"
    ...
    android:supportsPictureInPicture="true"
    android:launchMode="singleTask"
    ...

在您的活动中,覆盖 onNewIntent() 并处理新视频,如果需要,停止任何现有的视频播放。

最佳实践

在 RAM 较低的设备上,画中画可能被禁用。在您的应用使用画中画之前,请通过调用 hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) 确保它可用。

画中画适用于播放全屏视频的活动。当将您的活动切换到画中画模式时,请避免显示除视频内容之外的任何内容。跟踪您的活动何时进入画中画模式并隐藏 UI 元素,如 在画中画模式下处理 UI 中所述。

当活动处于画中画模式时,默认情况下它不会获得输入焦点。要在画中画模式下接收输入事件,请使用 MediaSession.setCallback()。有关使用 setCallback() 的更多信息,请参阅 显示正在播放卡片

当您的应用处于画中画模式时,画中画窗口中的视频播放可能会导致与其他应用(例如音乐播放器应用或语音搜索应用)的音频干扰。为避免这种情况,请在开始播放视频时请求音频焦点,并处理音频焦点更改通知,如 管理音频焦点 中所述。如果您在画中画模式下收到音频焦点丢失的通知,请暂停或停止视频播放。

当您的应用即将进入画中画时,请注意只有顶层活动会进入画中画。在某些情况下(例如在多窗口设备上),下方活动可能会显示并再次可见,与画中画活动并排显示。您应该相应地处理这种情况,包括下方活动获得 onResume()onPause() 回调。用户也可能与该活动交互。例如,如果您显示了一个视频列表活动,并且视频播放活动处于画中画模式,则用户可能会从列表中选择一个新视频,画中画活动应相应更新。

其他示例代码

要下载用 Kotlin 编写的示例应用,请参阅 Android 画中画示例 (Kotlin)