使用画中画 (PIP) 功能添加视频

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

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

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

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

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

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

用户可以将画中画窗口拖动到其他位置。从 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"
    ...

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

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

如果您的应用以 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();
    }
}

建议:为用户提供精致的 PiP 转换体验

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

如果您的应用未包含适用的更新,PiP 转场效果仍可正常使用,但动画效果会较差。例如,从全屏模式转换为画中画模式可能会导致画中画窗口在转换期间消失,然后在转换完成后重新显示。

这些变更涉及以下方面。

  • 更加顺畅地从手势导航模式过渡到画中画模式
  • 为进入和退出 PiP 模式设置适当的 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 以进入和退出 PiP 模式,请执行以下操作:

  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(false)
    .build())

Java

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

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

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

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

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

采用这种新的界面状态后,以 Android 15 为目标平台的应用会在画中画动画开始后立即观察使用 isTransitioningToPip() 调用的 Activity#onPictureInPictureUiStateChanged() 回调。当应用处于 PiP 模式时,许多界面元素与应用无关,例如包含建议、即将播放的视频、评分和标题等信息的视图或布局。当应用进入画中画模式时,请使用 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() 时,您应暂停视频播放;当系统调用 activity 的 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)