从 Android 8.0(API 级别 26)开始,Android 允许以画中画 (PiP) 模式启动 activity。画中画是一种特殊类型的多窗口模式,用于视频播放、视频通话和导航。使用该模式,用户可以将现有 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"
...
使用 Jetpack 实现 PiP
使用 Jetpack 画中画库来实现画中画体验,因为它可以简化集成并减少常见的应用内问题。不过,如果您希望使用平台 API 实现 PiP,请参阅以下文档。
将您的 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 过渡仍可正常运行,但动画效果不太流畅。例如,从全屏模式过渡到画中画模式可能会导致画中画窗口在过渡期间消失,并在过渡完成后重新显示。
这些变更涉及以下方面。
- 更加顺畅地从手势导航模式过渡到画中画模式
- 为进入和退出画中画模式设置适当的
sourceRectHint - 为非视频内容停用无缝大小调整
请参阅 Android Kotlin PictureInPicture 示例,了解如何实现流畅的过渡体验。
更加顺畅地从手势导航模式过渡到画中画模式
从 Android 12 开始,setAutoEnterEnabled 标志可提供更流畅的动画,以便在使用手势导航时过渡到画中画模式下的视频内容,例如从全屏模式向上滑动到主屏幕时。
如需进行此更改,请完成以下步骤,并参考此示例:
使用
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),但仅在必要时调用。例如,如果当前播放处于暂停状态,您可能不想进入画中画模式。
为进入和退出画中画模式设置了适当的 sourceRectHint
从 Android 8.0 中引入 PiP 开始,setSourceRectHint 用于指示在过渡到画中画模式后 activity 的可见区域,例如视频播放器中的视频视图边界。
在 Android 12 中,系统使用 sourceRectHint 在进入和退出画中画模式时实现更流畅的动画。
如需正确设置 sourceRectHint 以进入和退出 PiP 模式,请执行以下操作:
使用适当的边界作为
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。当系统即将退出画中画模式时,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 引入了多项变更,可确保在进入画中画模式时实现更顺畅的过渡。这对于界面元素叠加在进入 PiP 模式的主界面之上的应用很有用。
开发者使用 onPictureInPictureModeChanged() 回调来定义用于切换叠加界面元素可见性的逻辑。
当 PiP 进入或退出动画完成时,系统会触发此回调。
从 Android 15 开始,PictureInPictureUiState 类包含一个新状态。
借助此新的界面状态,以 Android 15 为目标平台的应用会在画中画动画开始时立即观察到 Activity#onPictureInPictureUiStateChanged() 回调被调用,并带有 isTransitioningToPip()。当应用处于 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. } }
此功能可快速切换不相关的界面元素(对于 PiP 窗口)的可见性,有助于确保 PiP 进入动画更流畅且无闪烁。
替换这些回调以重新绘制 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)。