使用画中画 (PiP) 功能

试用 Compose 方式
Jetpack Compose 是推荐用于 Android 的界面工具包。了解如何在 Compose 中支持画中画。

从 Android 8.0(API 级别 26)开始,Android 允许以画中画 (PiP) 模式启动 activity。画中画是一种特殊类型的多窗口模式,用于视频播放、视频通话和导航。使用该模式,用户可以将现有 activity 窗口固定到屏幕一角,同时在应用之间进行导航或浏览主屏幕上的内容。

画中画利用 Android 7.0 中提供的多窗口模式 API 来提供固定的视频叠加窗口。如要将画中画添加到您的应用中,您需要注册支持画中画的 activity,根据需要将 activity 切换为画中画模式,并确保当 activity 处于画中画模式时,界面元素处于隐藏状态且视频能够继续播放。

画中画窗口会显示在屏幕的最上层,位于系统选择的一角。

兼容的 Android TV OS 设备(搭载 Android 14 [API 级别 34] 或更高版本)也支持画中画。虽然有很多相似之处,但在电视上使用 画中画时, 还需要考虑其他因素。

用户如何与画中画窗口互动

用户可以将画中画窗口拖动到其他位置。从 Android 12 开始,用户还可以执行以下操作:

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

  • 点按两次该窗口可在当前画中画大小与最大或最小画中画大小之间切换。例如,点按两次最大化的窗口会将其最小化,反之亦然。

  • 将窗口拖到左侧或右侧边缘可隐藏该窗口。点按已隐藏窗口的可见部分或将其拖出可取消隐藏该窗口。

  • 使用双指张合即可缩放手势可调整画中画窗口的大小。

您的应用会控制当前 activity 在何时进入画中画模式。下面是一些示例:

  • 一个 activity 可以在用户点按主屏幕按钮或向上滑动到主屏幕时,进入画中画模式。Google 地图就是通过这种方式,在用户同时运行其他 activity 时继续显示路线。

  • 您的应用可以在用户从某个视频返回以浏览其他内容时,将该视频切换到画中画模式。

  • 您的应用可以在用户观看到某集内容结束时,将视频切换到画中画模式。主屏幕会显示有关这部电视剧下一集宣传信息或剧情摘要信息。

  • 您的应用可以提供一种方式,让用户可以在观看视频时将其他内容加入播放队列。当主屏幕显示内容选择 activity 时,视频会继续以画中画模式播放。

声明对画中画的支持

默认情况下,系统不会自动为应用提供画中画支持。如果您想在应用中支持画中画,可以通过将 android:supportsPictureInPicture 设置为 true,在清单中注册视频 activity。此外,指定您的 activity 会处理布局配置更改,这样一来,在画中画模式转换期间发生布局更改时,您的 activity 不会重新启动。

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

使用 Jetpack 实现 PiP

使用 Jetpack 画中画库来实现画中画体验,因为它可以简化集成并减少常见的应用内问题。如需查看其使用示例,请参阅我们的平台示例应用。不过,如果您希望使用平台 API 实现 PiP,请参阅以下文档。

将您的 activity 切换到画中画模式

从 Android 12 开始,您可以通过将 setAutoEnterEnabled 标志设置为 true,将您的 activity 切换到画中画模式。通过此设置,activity 会根据需要自动切换到画中画模式,而无需显式调用 enterPictureInPictureMode()onUserLeaveHint。此外,这样做还有助于实现更流畅的过渡。如需了解详情,请参阅更加顺畅地从手势导航模式过渡到画中画模式

如果您以 Android 11 或更低版本为目标平台,则 activity 必须调用 enterPictureInPictureMode() 才能切换到画中画模式。例如,以下代码会在用户点击应用界面中的专用按钮时,将 activity 切换到画中画模式:

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;
    }
    ...
}

您可能需要添加将 activity 切换到画中画模式(而不是进入后台)的逻辑。例如,如果用户在 Google 地图导航时按下主屏幕或最近使用的应用按钮,则该应用会切换到画中画模式。您可以通过替换 onUserLeaveHint() 来实现这一目的:

Kotlin

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

Java

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

建议:为用户提供流畅的画中画过渡体验

Android 12 对全屏窗口和画中画窗口之间的动画过渡进行了显著的视觉改进。我们强烈建议您实现所有适用的更改;完成此操作后,这些更改会自动缩放以适应可折叠设备和平板电脑等大屏幕,而无需进行任何其他工作。

如果您的应用不包含适用的更新,画中画过渡仍然有效,但动画效果不太流畅。例如,从全屏模式过渡到画中画模式可能会导致画中画窗口在过渡期间消失,然后在过渡完成后重新显示。

这些更改涉及以下内容。

  • 更加顺畅地从手势导航模式过渡到画中画模式
  • 为进入和退出画中画模式设置适当的 sourceRectHint
  • 为非视频内容停用无缝大小调整

如需了解如何启用流畅的过渡体验,请参阅 Android Kotlin PictureInPicture 示例

更加顺畅地从手势导航模式过渡到画中画模式

从 Android 12 开始,setAutoEnterEnabled 标志可为使用手势 导航过渡到画中画模式下的视频内容提供更 流畅的动画效果,例如从全屏模式向上滑动到主屏幕时。

如需进行此更改,请完成以下步骤:

  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),但仅在必要时调用。例如,如果当前播放处于暂停状态,您可能不希望进入画中画模式。

为进入和退出画中画模式设置适当的 sourceRectHint

从 Android 8.0 中引入画中画开始,setSourceRectHint 指示在过渡到画中画后可见的 Activity 区域,例如视频播放器中的视频视图边界。

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

如需为进入和退出画中画模式正确设置 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。当系统即将退出画中画模式时,activity 的视图层次结构会布局成它的目标配置(例如全屏)。应用可以将布局更改监听器附加到其根视图或目标视图(如视频播放器视图),以检测事件并在动画开始前更新 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(true)
    .build())

Java

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

在画中画模式下处理界面元素

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

Android 15 引入了更改,可确保在进入画中画模式时实现更流畅的过渡。这对于在主界面(进入画中画模式)上叠加了界面元素的应用很有用。

开发者使用 onPictureInPictureModeChanged() 回调来定义用于切换叠加界面元素可见性的逻辑。当画中画进入或退出动画完成时,系统会触发此回调。 从 Android 15 开始,PictureInPictureUiState 类包含一个新状态。

借助此新界面状态,以 Android 15 为目标平台的应用会在画中画动画开始后立即观察到使用 isTransitioningToPip() 调用 Activity#onPictureInPictureUiStateChanged() 回调。 当应用处于画中画模式时,许多界面元素与应用无关,例如包含建议、即将播放的视频、评分和标题等信息的视图或布局。当应用进入画中画模式时,请使用 onPictureInPictureUiStateChanged() 回调来隐藏这些界面元素。当应用从画中画窗口切换到全屏模式时,请使用 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.
        }
    }

快速切换与画中画窗口无关的界面元素的可见性有助于确保画中画进入动画更流畅且无闪烁。

替换这些回调以重新绘制 activity 的界面元素。请注意,在画中画模式下,您的 activity 会在一个小窗口中显示。在画中画模式下,用户无法与应用的界面元素互动,并且可能很难看清小界面元素的详细信息。界面极简的视频播放 activity 可提供出色的用户体验。

如果您的应用需要为画中画提供自定义操作,请参阅本页中的添加控件。在 activity 进入画中画模式之前移除其他界面元素,并在 activity 再次变为全屏时恢复这些元素。

添加控件

画中画窗口可以在用户打开窗口菜单(通过点按移动设备上的窗口或使用电视遥控器选择菜单)时显示控件。

如果应用有处于活跃状态的媒体会话,则窗口会显示“播放”“暂停”“前进”和“后退”控件。

您还可以通过在进入画中画模式之前构建 PictureInPictureParams (使用 PictureInPictureParams.Builder.setActions() )来明确指定自定义操作,并使用 enterPictureInPictureMode(android.app.PictureInPictureParams)setPictureInPictureParams(android.app.PictureInPictureParams)在进入画中画模式时传递这些参数。 请注意,如果您尝试添加的控件数量超过 getMaxNumPictureInPictureActions(),则系统只会添加上限数量的控件。

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

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

在 Android 7.0 及更高版本中,当 系统调用 activity 的 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.
        ...
    }
}

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

对画中画模式使用单个播放 activity

在您的应用中,可能会出现以下情况:有一个视频播放 Activity 正处于画中画模式,用户在主屏幕上浏览内容时选择了新的视频。应以全屏模式在现有的播放 activity 中播放新的视频,而不是启动可能会令用户感到困惑的新 activity。

如要确保将单个 activity 用于视频播放请求并根据需要进入或退出画中画模式,请在清单中将 activity 的 android:launchMode 设置为 singleTask

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

在您的 activity 中,替换 onNewIntent() 并处理新的视频,从而根据需要停止任何现有的视频播放。

最佳做法

低 RAM 设备可能无法使用画中画模式。在应用使用画中画之前,请务必通过调用 hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) 进行检查以确保可以使用画中画。

画中画旨在用于播放全屏视频的 activity。将 activity 切换到画中画模式时,请避免显示视频内容以外的任何内容。跟踪您的 activity 何时进入画中画模式并隐藏界面元素,如在画中画模式下处理界面元素中所述。

当 activity 进入画中画模式后,默认不会获得输入焦点。如需在画中画模式下接收输入事件,请使用 MediaSession.setCallback()。 如需详细了解如何使用 setCallback(),请参阅 显示“闻曲知音” 卡片

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

当您的应用即将进入画中画模式时,请注意,只有顶层 activity 才会进入画中画模式。在某些情况下(例如在多窗口模式设备上),此时系统可能会显示下层 Activity,在画中画 Activity 旁边,您可能会再次看到下层 Activity。您应相应地处理这种情况,包括以下 activity 获取 onResume()onPause() 回调。用户也有可能与该 activity 互动。例如,如果您的视频列表 activity 正在显示,视频播放 activity 处于画中画模式,用户可能会从列表中选择新视频,画中画 activity 应相应地进行更新。

更多示例代码

如需下载使用 Kotlin 编写的示例应用,请参阅 Android PictureInPicture 示例 (Kotlin)