画中画 (PiP) 是一种特殊的多窗口模式,主要用于视频播放。通过该模式,用户可在固定到屏幕一角的小窗口中观看视频,同时在应用之间进行导航或浏览主屏幕上的内容。
画中画利用 Android 7.0 中提供的多窗口模式 API 来提供固定的视频叠加窗口。如需向应用添加画中画功能,您需要注册 activity,根据需要将 activity 切换到画中画模式,并确保当 activity 处于画中画模式时,界面元素处于隐藏状态且视频播放继续。
本指南介绍了如何通过 Compose 视频实现将 Compose 中的画中画功能添加到应用中。如需了解这些最佳实践的实际运用,请参阅 Socialite 应用。
设置您的应用以使用画中画功能
在 AndroidManifest.xml
文件的 activity 代码中,执行以下操作:
- 添加
supportsPictureInPicture
并将其设置为true
,以声明您将在应用中使用画中画功能。 添加
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 中访问到 Android R。
- 使用
DisposableEffect
并将Context
作为键。 - 在
DisposableEffect
中,定义使用 lambda 触发onUserLeaveHintProvider
时的行为。在 lambda 中,对findActivity()
调用enterPictureInPictureMode()
并传入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_TAG, "API does not support PiP") }
在 Android 12 之后的版本中,为退出应用添加了画中画功能
在 Android 12 之后,PictureInPictureParams.Builder
通过传递到应用视频播放器的修饰符添加。
- 创建一个
modifier
并对其调用onGloballyPositioned
。布局坐标将在后续步骤中使用。 - 为
PictureInPictureParams.Builder()
创建一个变量。 - 添加
if
语句以检查 SDK 是否为 S 或更高版本。如果是这样,请将setAutoEnterEnabled
添加到构建器中,并将其设置为true
,以便在滑动时进入画中画模式。与执行enterPictureInPictureMode
相比,这会使动画更流畅。 - 使用
findActivity()
调用setPictureInPictureParams()
。对builder
调用build()
并传入它。
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
来实现此目的。以下代码会告知您应用是否处于画中画模式。
@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 之前)
- 由于在 12 之前版本中添加画中画需要使用
DisposableEffect
,因此您需要通过rememberUpdatedState
创建新变量,并将newValue
设置为状态变量。这将确保在DisposableEffect
中使用更新后的版本。 在用于定义触发
OnUserLeaveHintListener
时行为的 lambda 中,添加一个if
语句,并在对enterPictureInPictureMode()
的调用前后添加状态变量: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_TAG, "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 添加到画中画构建器中,以指示过渡到画中画后可见的 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
设置画中画窗口的宽高比
如需设置画中画窗口的宽高比,您可以选择特定的宽高比,也可以使用播放器的视频尺寸的宽度和高度。如果您使用的是 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
- 为画中画窗口中的控件创建
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)
后续步骤
在本指南中,您学习了在 Android 12 之前和 Android 12 之后的版本中添加画中画的最佳实践。