从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 模式中的视频内容提供了更加流畅的动画。
完成以下步骤以进行此更改,并参考此示例。
使用
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
指示了转换到画中画后可见的活动区域——例如,视频播放器中的视频视图边界。
在 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 模式时,活动的视图层次结构将布局到其目标配置(例如,全屏)。应用可以将其根视图或目标视图(例如视频播放器视图)附加一个布局更改侦听器来检测事件,并在动画开始前更新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(false) .build())
Java
setPictureInPictureParams(new PictureInPictureParams.Builder() .setSeamlessResizeEnabled(false) .build());
处理 PiP 期间的 UI
当活动进入或退出画中画 (PiP) 模式时,系统会调用Activity.onPictureInPictureModeChanged()
或Fragment.onPictureInPictureModeChanged()
。
Android 15 引入了更改,以确保在进入 PiP 模式时转换更加流畅。这对于在其主 UI 上叠加了 UI 元素的应用非常有益,这些 UI 元素会进入 PiP。
开发人员使用onPictureInPictureModeChanged()
回调来定义切换叠加 UI 元素可见性的逻辑。当 PiP 进入或退出动画完成时,会触发此回调。从 Android 15 开始,PictureInPictureUiState
类包含一个新的状态。
使用此新的 UI 状态,针对 Android 15 的应用会在 PiP 动画启动后立即观察到Activity#onPictureInPictureUiStateChanged()
回调被调用,其中isTransitioningToPip()
为真。许多 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 进入动画更流畅且无闪烁。
重写这些回调以重绘活动的 UI 元素。请记住,在 PiP 模式下,您的活动显示在一个小窗口中。当应用处于 PiP 模式时,用户无法与应用的 UI 元素交互,并且小型 UI 元素的细节可能难以查看。具有最少 UI 的视频播放活动可提供最佳用户体验。
如果您的应用需要为 PiP 提供自定义操作,请参阅本页面的添加控件。在活动进入 PiP 之前删除其他 UI 元素,并在活动再次全屏显示时恢复它们。
添加控件
当用户打开窗口菜单(通过在移动设备上点击窗口或从电视遥控器选择菜单)时,PiP 窗口可以显示控件。
如果应用具有活动的媒体会话,则会显示播放、暂停、下一首和上一首控件。
您还可以通过使用PictureInPictureParams.Builder.setActions()
在进入 PiP 模式之前构建具有PictureInPictureParams
来显式指定自定义操作,并在使用enterPictureInPictureMode(android.app.PictureInPictureParams)
或setPictureInPictureParams(android.app.PictureInPictureParams)
进入 PiP 模式时传递参数。注意,如果您尝试添加超过getMaxNumPictureInPictureActions()
数量的操作,您只会获得最大数量的操作。
在 PiP 中继续视频播放
当您的活动切换到 PiP 时,系统会将活动置于暂停状态并调用活动的onPause()
方法。如果活动在转换到 PiP 模式时暂停,则视频播放不应暂停,而应继续播放。
在 Android 7.0 及更高版本中,当系统调用您的活动的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. ... } }
当您的活动从 PiP 模式切换回全屏模式时,系统会恢复您的活动并调用您的onResume()
方法。
为 PiP 使用单个播放活动
在您的应用中,用户可能在主屏幕上浏览内容时选择一个新视频,而视频播放活动处于 PiP 模式。在新活动中播放全屏模式下的新视频,而不是启动可能使用户感到困惑的新活动。
为了确保为视频播放请求使用单个活动,并根据需要切换到或退出 PiP 模式,请在清单中将活动的android:launchMode
设置为singleTask
。
<activity android:name="VideoActivity"
...
android:supportsPictureInPicture="true"
android:launchMode="singleTask"
...
在您的活动中,重写onNewIntent()
并处理新视频,如有必要,停止任何现有的视频播放。
最佳实践
在内存不足的设备上,PiP 可能被禁用。在您的应用使用 PiP 之前,请通过调用hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
来确保它可用。
画中画 (PiP) 模式适用于播放全屏视频的活动。将活动切换到 PiP 模式时,应避免显示除视频内容以外的任何内容。请跟踪您的活动何时进入 PiP 模式并隐藏 UI 元素,如处理 PiP 期间的 UI中所述。
当活动处于 PiP 模式时,默认情况下它不会获得输入焦点。要在 PiP 模式下接收输入事件,请使用MediaSession.setCallback()
。有关使用setCallback()
的更多信息,请参阅显示正在播放卡片。
当您的应用处于 PiP 模式时,PiP 窗口中的视频播放可能会导致与其他应用(例如音乐播放器应用或语音搜索应用)的音频干扰。为避免这种情况,请在开始播放视频时请求音频焦点,并处理音频焦点更改通知,如管理音频焦点中所述。如果您在 PiP 模式下收到音频焦点丢失的通知,请暂停或停止视频播放。
当您的应用即将进入 PiP 时,请注意只有顶级活动才能进入画中画。在某些情况下(例如在多窗口设备上),下面的活动可能会再次显示并可见,同时 PiP 活动也在运行。您应该相应地处理这种情况,包括下面的活动获得onResume()
或onPause()
回调。用户也可能与该活动进行交互。例如,如果您显示了一个视频列表活动,并且正在播放的视频活动处于 PiP 模式,用户可能会从列表中选择一个新视频,PiP 活动也应该相应地更新。
更多示例代码
要下载用 Kotlin 编写的示例应用,请参阅Android 画中画示例 (Kotlin)。