画中画 (PiP) 是一种特殊的多窗口模式,主要用于 视频播放。它可让用户在固定到 浏览各个应用或浏览内容时 主屏幕。
画中画利用 Android 7.0 中提供的多窗口模式 API 来提供固定的视频叠加窗口。如需将画中画添加到您的应用中,您需要注册 activity,根据需要将 activity 切换为画中画模式,并确保当 activity 处于画中画模式时,界面元素处于隐藏状态且视频能够继续播放。
本指南介绍了如何通过 Compose 视频向应用添加 Compose 中的画中画功能 实施。前往 Socialite 应用,查看这些最佳照片 做法。
设置应用以使用画中画功能
在 AndroidManifest.xml
文件的 activity 标记中,执行以下操作:
- 添加
supportsPictureInPicture
并将其设置为true
,以声明您将在应用中使用 PiP。 添加
configChanges
并将其设置为orientation|screenLayout|screenSize|smallestScreenSize
来指定 您的 activity 会处理布局配置变更。这样一来,如果在画中画模式转换期间出现布局更改,您的 activity 就不会重新启动。<activity android:name=".SnippetsActivity" android:exported="true" android:supportsPictureInPicture="true" android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize" android:theme="@style/Theme.Snippets">
在 Compose 代码中,执行以下操作:
- 在
Context
上添加此扩展程序。您将多次使用此附加信息 访问该 activity。internal fun Context.findActivity(): ComponentActivity { var context = this while (context is ContextWrapper) { if (context is ComponentActivity) return context context = context.baseContext } throw IllegalStateException("Picture in picture should be called in the context of an Activity") }
为 Android 12 之前的版本添加了“离开时画中画”应用
如需为 Android 12 之前的版本添加画中画,请使用 addOnUserLeaveHintProvider
。关注
如需为 Android 12 之前的版本添加画中画,请按以下步骤操作:
- 添加版本门控,以便在版本 O 之前仅在版本 R 中访问此代码。
- 使用
DisposableEffect
并将Context
作为键。 - 在
DisposableEffect
内,定义在onUserLeaveHintProvider
是使用 lambda 触发的。在 lambda 中,调用enterPictureInPictureMode()
调用findActivity()
并传入PictureInPictureParams.Builder().build()
。 - 使用
findActivity()
添加addOnUserLeaveHintListener
并传入 lambda。 - 在
onDispose
中,使用findActivity()
添加removeOnUserLeaveHintListener
并传入 lambda。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && Build.VERSION.SDK_INT < Build.VERSION_CODES.S ) { val context = LocalContext.current DisposableEffect(context) { val onUserLeaveBehavior: () -> Unit = { context.findActivity() .enterPictureInPictureMode(PictureInPictureParams.Builder().build()) } context.findActivity().addOnUserLeaveHintListener( onUserLeaveBehavior ) onDispose { context.findActivity().removeOnUserLeaveHintListener( onUserLeaveBehavior ) } } } else { Log.i("PiP info", "API does not support PiP") }
为 Android 12 之后的版本添加了“离开时画中画”应用
在 Android 12 之后,PictureInPictureParams.Builder
通过
修饰符,传递给应用视频播放器。
- 创建一个
modifier
并对其调用onGloballyPositioned
。布局坐标将在后面的步骤中使用。 - 为
PictureInPictureParams.Builder()
创建一个变量。 - 添加
if
语句以检查 SDK 是否为 S 或更高版本。如果是,请将setAutoEnterEnabled
转到构建器,并将其设置为true
以进入画中画模式 模式。这会提供比完整播放的动画更流畅enterPictureInPictureMode
。 - 使用
findActivity()
调用setPictureInPictureParams()
。拨打build()
并传入builder
。
val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> val builder = PictureInPictureParams.Builder() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { builder.setAutoEnterEnabled(true) } context.findActivity().setPictureInPictureParams(builder.build()) } VideoPlayer(pipModifier)
通过按钮添加画中画
如需通过点击按钮进入画中画模式,请对 findActivity()
调用 enterPictureInPictureMode()
。
这些参数已由之前对
PictureInPictureParams.Builder
,因此您无需设置新参数
代码。不过,如果您想更改“按钮”上的任何参数,
您可在此处进行设置
val context = LocalContext.current Button(onClick = { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.findActivity().enterPictureInPictureMode( PictureInPictureParams.Builder().build() ) } else { Log.i(PIP_TAG, "API does not support PiP") } }) { Text(text = "Enter PiP mode!") }
在画中画模式下处理界面
当您进入画中画模式时,应用的整个界面都会进入画中画窗口,除非您 指定界面在进入和退出画中画模式时的外观。
首先,您需要了解应用何时处于画中画模式。您可以使用 OnPictureInPictureModeChangedProvider
来实现此目的。以下代码会告知您应用是否处于 PiP 模式。
@Composable fun rememberIsInPipMode(): Boolean { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val activity = LocalContext.current.findActivity() var pipMode by remember { mutableStateOf(activity.isInPictureInPictureMode) } DisposableEffect(activity) { val observer = Consumer<PictureInPictureModeChangedInfo> { info -> pipMode = info.isInPictureInPictureMode } activity.addOnPictureInPictureModeChangedListener( observer ) onDispose { activity.removeOnPictureInPictureModeChangedListener(observer) } } return pipMode } else { return false } }
现在,您可以使用 rememberIsInPipMode()
来切换显示哪些界面元素
当应用进入画中画模式时:
val inPipMode = rememberIsInPipMode() Column(modifier = modifier) { // This text will only show up when the app is not in PiP mode if (!inPipMode) { Text( text = "Picture in Picture", ) } VideoPlayer() }
确保您的应用在适当的时间进入画中画模式
在以下情况下,您的应用不应进入画中画模式:
- 视频停止或暂停播放。
- 您当前打开的应用页面与视频播放器的页面不同。
如需控制应用何时进入画中画模式,请添加一个跟踪应用状态的变量
使用 mutableStateOf
。
根据视频是否正在播放切换状态
如需根据视频播放器是否正在播放切换状态,请添加监听器 视频播放器。根据玩家是否正在玩游戏,切换状态变量的状态:
player.addListener(object : Player.Listener { override fun onIsPlayingChanged(isPlaying: Boolean) { shouldEnterPipMode = isPlaying } })
根据播放器是否已释放来切换状态
播放器释放后,将状态变量设置为 false
:
fun releasePlayer() { shouldEnterPipMode = false }
使用状态来定义是否进入画中画模式(Android 12 之前的版本)
- 由于在 Android 12 之前添加 PiP 时使用的是
DisposableEffect
,因此您需要通过rememberUpdatedState
创建一个新变量,并将newValue
设置为状态变量。这可确保在DisposableEffect
中使用更新后的版本。 在用于定义
OnUserLeaveHintListener
触发时的行为的 lambda 中,在对enterPictureInPictureMode()
的调用周围添加包含状态变量的if
语句:val currentShouldEnterPipMode by rememberUpdatedState(newValue = shouldEnterPipMode) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && Build.VERSION.SDK_INT < Build.VERSION_CODES.S ) { val context = LocalContext.current DisposableEffect(context) { val onUserLeaveBehavior: () -> Unit = { if (currentShouldEnterPipMode) { context.findActivity() .enterPictureInPictureMode(PictureInPictureParams.Builder().build()) } } context.findActivity().addOnUserLeaveHintListener( onUserLeaveBehavior ) onDispose { context.findActivity().removeOnUserLeaveHintListener( onUserLeaveBehavior ) } } } else { Log.i("PiP info", "API does not support PiP") }
使用状态来定义是否进入画中画模式(在 Android 12 之后的版本)
将状态变量传递到 setAutoEnterEnabled
,以便应用仅进入
开启画中画模式:
val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> val builder = PictureInPictureParams.Builder() // Add autoEnterEnabled for versions S and up if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { builder.setAutoEnterEnabled(shouldEnterPipMode) } context.findActivity().setPictureInPictureParams(builder.build()) } VideoPlayer(pipModifier)
使用 setSourceRectHint
实现平滑动画
setSourceRectHint
API 可创建更流畅的进入画中画模式的动画。在 Android 12 及更高版本中,它还针对退出画中画模式创建了更流畅的动画。
将此 API 添加到 PiP 构建器,以指明在转换为画中画后可见的 activity 区域。
- 仅当状态定义了
setSourceRectHint()
builder
应用应进入画中画模式。这样可以避免在应用运行时计算sourceRect
无需输入画中画。 - 如需设置
sourceRect
值,请使用修饰符上通过onGloballyPositioned
函数给出的layoutCoordinates
。 - 对
builder
调用setSourceRectHint()
并传入sourceRect
变量。
val context = LocalContext.current val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> val builder = PictureInPictureParams.Builder() if (shouldEnterPipMode) { val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect() builder.setSourceRectHint(sourceRect) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { builder.setAutoEnterEnabled(shouldEnterPipMode) } context.findActivity().setPictureInPictureParams(builder.build()) } VideoPlayer(pipModifier)
使用 setAspectRatio
设置 PiP 窗口的宽高比
如需设置 PIP 窗口的宽高比,您可以选择特定的宽高比,也可以使用播放器视频尺寸的宽度和高度。如果您
使用 media3 播放器时,检查该播放器是否不为 null,以及该播放器的
在设置宽高比之前,视频大小不等于 VideoSize.UNKNOWN
宽高比。
val context = LocalContext.current val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> val builder = PictureInPictureParams.Builder() if (shouldEnterPipMode && player != null && player.videoSize != VideoSize.UNKNOWN) { val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect() builder.setSourceRectHint(sourceRect) builder.setAspectRatio( Rational(player.videoSize.width, player.videoSize.height) ) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { builder.setAutoEnterEnabled(shouldEnterPipMode) } context.findActivity().setPictureInPictureParams(builder.build()) } VideoPlayer(pipModifier)
如果你使用的是自定义播放器,请根据播放器的高度设置宽高比 和宽度。请注意,如果您的播放器在初始化期间调整大小,并且超出宽高比的有效边界,您的应用将崩溃。您可能需要添加 何时可以计算宽高比,类似于针对 media3 的计算宽高比 。
添加远程操作
如果您想要向画中画窗口添加控件(播放、暂停等),请创建一个
RemoteAction
。
- 为广播控件添加常量:
// Constant for broadcast receiver const val ACTION_BROADCAST_CONTROL = "broadcast_control" // Intent extras for broadcast controls from Picture-in-Picture mode. const val EXTRA_CONTROL_TYPE = "control_type" const val EXTRA_CONTROL_PLAY = 1 const val EXTRA_CONTROL_PAUSE = 2
- 为 PiP 窗口中的控件创建
RemoteActions
列表。 - 接下来,添加一个
BroadcastReceiver
并替换onReceive()
,以设置每个按钮的操作。使用DisposableEffect
注册 接收者和远程操作处理好播放器后,取消注册 接收器。@RequiresApi(Build.VERSION_CODES.O) @Composable fun PlayerBroadcastReceiver(player: Player?) { val isInPipMode = rememberIsInPipMode() if (!isInPipMode || player == null) { // Broadcast receiver is only used if app is in PiP mode and player is non null return } val context = LocalContext.current DisposableEffect(player) { val broadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { if ((intent == null) || (intent.action != ACTION_BROADCAST_CONTROL)) { return } when (intent.getIntExtra(EXTRA_CONTROL_TYPE, 0)) { EXTRA_CONTROL_PAUSE -> player.pause() EXTRA_CONTROL_PLAY -> player.play() } } } ContextCompat.registerReceiver( context, broadcastReceiver, IntentFilter(ACTION_BROADCAST_CONTROL), ContextCompat.RECEIVER_NOT_EXPORTED ) onDispose { context.unregisterReceiver(broadcastReceiver) } } }
- 将远程操作列表传递到
PictureInPictureParams.Builder
:val context = LocalContext.current val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> val builder = PictureInPictureParams.Builder() builder.setActions( listOfRemoteActions() ) if (shouldEnterPipMode && player != null && player.videoSize != VideoSize.UNKNOWN) { val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect() builder.setSourceRectHint(sourceRect) builder.setAspectRatio( Rational(player.videoSize.width, player.videoSize.height) ) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { builder.setAutoEnterEnabled(shouldEnterPipMode) } context.findActivity().setPictureInPictureParams(builder.build()) } VideoPlayer(modifier = pipModifier)
后续步骤
在本指南中,您学习了在 Compose 中添加画中画的最佳实践 Android 12 之前和 Android 12 之后的版本。