从 Android 8.0(API 级别 26)开始,Android 允许 Activity 以画中画 (PiP) 模式启动。PiP 是一种特殊的多窗口模式,主要用于视频播放。它允许用户在屏幕角落的小窗口中观看视频,同时在应用之间导航或在主屏幕上浏览内容。
PiP 利用 Android 7.0 中提供的多窗口 API 来提供固定视频叠加窗口。要将 PiP 添加到您的应用中,您需要注册支持 PiP 的 Activity,根据需要将 Activity 切换到 PiP 模式,并确保在 Activity 处于 PiP 模式时隐藏 UI 元素并继续视频播放。
PiP 窗口显示在屏幕的最顶层,位于系统选择的角落。
PiP 也受 Android 14(API 级别 34)或更高版本上兼容的 Android TV OS 设备支持。虽然有很多相似之处,但在 TV 上使用 PiP 时还有其他注意事项。
用户如何与 PiP 窗口交互
用户可以将 PiP 窗口拖到另一个位置。从 Android 12 开始,用户还可以
单点触摸窗口可显示全屏切换按钮、关闭按钮、设置按钮以及您的应用提供的自定义操作(例如播放控件)。
双击窗口可在当前 PiP 大小与最大或最小 PiP 大小之间切换,例如,双击最大化的窗口会将其最小化,反之亦然。
通过将其拖到左边缘或右边缘来隐藏窗口。要取消隐藏窗口,可以点按隐藏窗口的可见部分或将其拖出。
使用双指张合手势调整 PiP 窗口大小。
您的应用控制当前 Activity 何时进入 PiP 模式。以下是一些示例
当用户点按主页按钮或向上滑动到主页时,Activity 可以进入 PiP 模式。Google 地图就是这样在用户同时运行另一个 Activity 时继续显示方向。
当用户从视频导航回浏览其他内容时,您的应用可以将视频移至 PiP 模式。
当用户观看内容剧集的结尾时,您的应用可以将视频切换到 PiP 模式。主屏幕显示有关该系列下一集的宣传或摘要信息。
您的应用可以为用户提供在观看视频时排队等待其他内容的方式。视频在 PiP 模式下继续播放,而主屏幕显示内容选择 Activity。
声明 PiP 支持
默认情况下,系统不会自动支持应用的 PiP。如果您想在应用中支持 PiP,请在清单中通过将 android:supportsPictureInPicture
设置为 true
来注册您的视频 Activity。此外,请指定您的 Activity 处理布局配置更改,以便您的 Activity 在 PiP 模式转换期间发生布局更改时不会重新启动。
<activity android:name="VideoActivity"
android:supportsPictureInPicture="true"
android:configChanges=
"screenSize|smallestScreenSize|screenLayout|orientation"
...
将 Activity 切换到 PiP
从 Android 12 开始,您可以通过将 setAutoEnterEnabled
标志设置为 true
来将 Activity 切换到 PiP 模式。通过此设置,Activity 会根据需要自动切换到 PiP 模式,而无需在 onUserLeaveHint
中明确调用 enterPictureInPictureMode()
。这还具有提供更流畅过渡的额外好处。有关详细信息,请参阅通过手势导航使到 PiP 模式的过渡更流畅。
如果您定位的是 Android 11 或更低版本,Activity 必须调用 enterPictureInPictureMode()
才能切换到 PiP 模式。例如,以下代码在用户点击应用 UI 中的专用按钮时将 Activity 切换到 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; } ... }
您可能希望包含将 Activity 切换到 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 PictureInPicture 示例,作为实现流畅过渡体验的参考。
通过手势导航使到 PiP 模式的过渡更流畅
从 Android 12 开始,setAutoEnterEnabled
标志为使用手势导航(例如,从全屏向上滑动到主页)过渡到 PiP 模式下的视频内容提供了更流畅的动画。
完成以下步骤进行此更改,并参考此示例
使用
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());
尽早使用最新的
PictureInPictureParams
调用setPictureInPictureParams
。应用不会等待onUserLeaveHint
回调(就像在 Android 11 中那样)。例如,如果宽高比发生变化,您可能希望在首次播放和任何后续播放时调用
setPictureInPictureParams
。调用
setAutoEnterEnabled(false)
,但仅在必要时才调用。例如,如果当前播放处于暂停状态,您可能不希望进入 PiP。
为进入和退出 PiP 模式设置适当的 sourceRectHint
自 Android 8.0 引入 PiP 以来,setSourceRectHint
指示了在过渡到画中画后 Activity 可见的区域,例如视频播放器中的视频视图边界。
在 Android 12 中,系统使用 sourceRectHint
来实现进入和退出 PiP 模式时更流畅的动画。
要为进入和退出 PiP 模式正确设置 sourceRectHint
使用适当的边界作为
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);
如有必要,在系统开始退出过渡之前更新
sourceRectHint
。当系统即将退出 PiP 模式时,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
标志,该标志在调整 PiP 窗口中非视频内容的大小时提供了更流畅的交叉渐变动画。以前,调整 PiP 窗口中非视频内容的大小可能会产生刺眼的视觉伪影。
为视频内容启用无缝调整大小
Kotlin
setPictureInPictureParams(PictureInPictureParams.Builder() .setSeamlessResizeEnabled(true) .build())
Java
setPictureInPictureParams(new PictureInPictureParams.Builder() .setSeamlessResizeEnabled(true) .build());
在 PiP 期间处理 UI
当 Activity 进入或退出画中画 (PiP) 模式时,系统会调用 Activity.onPictureInPictureModeChanged()
或 Fragment.onPictureInPictureModeChanged()
。
Android 15 引入了更改,以确保进入 PiP 模式时过渡更流畅。这对于在其主 UI 上叠加了 UI 元素并进入 PiP 的应用非常有利。
开发者使用 onPictureInPictureModeChanged()
回调来定义切换叠加 UI 元素可见性的逻辑。此回调在 PiP 进入或退出动画完成后触发。从 Android 15 开始,PictureInPictureUiState
类包含一个新状态。
有了这个新的 UI 状态,针对 Android 15 的应用会观察到 Activity#onPictureInPictureUiStateChanged()
回调在 PiP 动画开始时即被调用,其中 isTransitioningToPip()
为 true。有许多 UI 元素与处于 PiP 模式的应用无关,例如包含建议、即将播放的视频、评分和标题等信息的视图或布局。当应用进入 PiP 模式时,使用 onPictureInPictureUiStateChanged()
回调隐藏这些 UI 元素。当应用从 PiP 窗口进入全屏模式时,使用 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 元素(对于 PiP 窗口)的快速可见性切换有助于确保更流畅、无闪烁的 PiP 进入动画。
覆盖这些回调以重新绘制 Activity 的 UI 元素。请记住,在 PiP 模式下,您的 Activity 显示在一个小窗口中。当应用处于 PiP 模式时,用户无法与应用的 UI 元素交互,并且小 UI 元素的详细信息可能难以看清。具有最小 UI 的视频播放 Activity 可提供最佳用户体验。
如果您的应用需要为 PiP 提供自定义操作,请参阅本页上的添加控件。在您的 Activity 进入 PiP 之前删除其他 UI 元素,并在您的 Activity 再次全屏时恢复它们。
添加控件
当用户打开窗口菜单时(通过在移动设备上点击窗口,或从电视遥控器中选择菜单),PiP 窗口可以显示控件。
如果应用具有活动媒体会话,则会显示播放、暂停、下一曲和上一曲控件。
您还可以通过在进入 PiP 模式之前使用 PictureInPictureParams.Builder.setActions()
构建 PictureInPictureParams
来明确指定自定义操作,并在使用 enterPictureInPictureMode(android.app.PictureInPictureParams)
或 setPictureInPictureParams(android.app.PictureInPictureParams)
进入 PiP 模式时传递参数。请注意。如果您尝试添加超过 getMaxNumPictureInPictureActions()
的操作,您将只能获得最大数量。
在 PiP 中继续视频播放
当您的 Activity 切换到 PiP 时,系统会将 Activity 置于暂停状态,并调用 Activity 的 onPause()
方法。视频播放不应暂停,如果 Activity 在过渡到 PiP 模式时暂停,则应继续播放。
在 Android 7.0 及更高版本中,当系统调用 Activity 的 onStop()
和 onStart()
时,您应该暂停和恢复视频播放。通过这样做,您可以避免在 onPause()
中检查您的应用是否处于 PiP 模式并显式继续播放。
如果您尚未将 setAutoEnterEnabled
标志设置为 true
,并且您需要在 onPause()
实现中暂停播放,请通过调用 isInPictureInPictureMode()
检查 PiP 模式并适当地处理播放。例如
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 从 PiP 模式切换回全屏模式时,系统会恢复您的 Activity 并调用您的 onResume()
方法。
为 PiP 使用单个播放 Activity
在您的应用中,用户在主屏幕上浏览内容时可能会选择新的视频,而视频播放 Activity 处于 PiP 模式。在新视频在现有播放 Activity 中以全屏模式播放,而不是启动一个可能让用户感到困惑的新 Activity。
为了确保单个 Activity 用于视频播放请求并根据需要切换进出 PiP 模式,请在清单中将 Activity 的 android:launchMode
设置为 singleTask
<activity android:name="VideoActivity"
...
android:supportsPictureInPicture="true"
android:launchMode="singleTask"
...
在您的 Activity 中,覆盖 onNewIntent()
并处理新视频,如果需要,停止任何现有的视频播放。
最佳实践
在 RAM 较低的设备上可能会禁用 PiP。在您的应用使用 PiP 之前,请通过调用 hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
确保其可用。
PiP 旨在用于播放全屏视频的 Activity。将 Activity 切换到 PiP 模式时,避免显示视频内容以外的任何内容。跟踪 Activity 何时进入 PiP 模式并隐藏 UI 元素,如在 PiP 期间处理 UI 中所述。
当 Activity 处于 PiP 模式时,默认情况下它不会获得输入焦点。要在 PiP 模式下接收输入事件,请使用 MediaSession.setCallback()
。有关使用 setCallback()
的更多信息,请参阅显示正在播放卡。
当您的应用处于 PiP 模式时,PiP 窗口中的视频播放可能会干扰另一个应用(例如音乐播放器应用或语音搜索应用)的音频。为避免这种情况,请在开始播放视频时请求音频焦点,并处理音频焦点更改通知,如管理音频焦点中所述。如果您在 PiP 模式下收到音频焦点丢失的通知,请暂停或停止视频播放。
当您的应用即将进入 PiP 时,请注意只有最顶层的 Activity 会进入画中画。在某些情况下(例如多窗口设备),下面的 Activity 可能会显示并与 PiP Activity 一起再次可见。您应该相应地处理这种情况,包括下面的 Activity 获得 onResume()
或 onPause()
回调。用户也可能与 Activity 交互。例如,如果您显示了视频列表 Activity 并且播放视频 Activity 处于 PiP 模式,用户可能会从列表中选择一个新视频,并且 PiP Activity 应该相应地更新。
其他示例代码
要下载用 Kotlin 编写的示例应用,请参阅Android 画中画示例 (Kotlin)。