画中画 (PiP) 是一种特殊的多分屏模式,主要用于视频播放。它允许用户在一个固定在屏幕角落的小窗口中观看视频,同时在主屏幕上浏览应用程序或内容。
PiP 利用 Android 7.0 中提供的多分屏 API 来提供固定的视频叠加窗口。要在您的应用程序中添加 PiP,您需要注册您的 Activity,根据需要将您的 Activity 切换到 PiP 模式,并确保在 Activity 处于 PiP 模式时隐藏 UI 元素并继续视频播放。
本指南介绍如何在 Compose 中使用 Compose 视频实现将 PiP 添加到您的应用程序中。请参阅 Socialite 应用程序,了解这些最佳实践的实际应用。
为 PiP 设置您的应用程序
在您的 AndroidManifest.xml
文件的 Activity 标签中,执行以下操作
- 添加
supportsPictureInPicture
并将其设置为true
,声明您将在应用程序中使用 PiP。 添加
configChanges
并将其设置为orientation|screenLayout|screenSize|smallestScreenSize
,以指定您的 Activity 处理布局配置更改。这样,当在 PiP 模式转换期间发生布局更改时,您的 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 之前退出应用程序时添加 PiP
要为 Android 12 之前的版本添加 PiP,请使用 addOnUserLeaveHintProvider
。请按照以下步骤为 Android 12 之前的版本添加 PiP
- 添加一个版本门,以便仅在版本 O 到 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 之后退出应用程序时添加 PiP
在 Android 12 之后,PictureInPictureParams.Builder
通过传递给应用程序视频播放器的修饰符添加。
- 创建一个
modifier
并调用其上的onGloballyPositioned
。布局坐标将在后续步骤中使用。 - 为
PictureInPictureParams.Builder()
创建一个变量。 - 添加一个
if
语句来检查 SDK 是否为 S 或更高版本。如果是,则向构建器添加setAutoEnterEnabled
并将其设置为true
,以便在滑动时进入 PiP 模式。与通过enterPictureInPictureMode
进入 PiP 模式相比,这提供了更流畅的动画。 - 使用
findActivity()
调用setPictureInPictureParams()
。在构建器上调用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)
通过按钮添加 PiP
要通过单击按钮进入 PiP 模式,请在 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!") }
在 PiP 模式下处理您的 UI
进入 PiP 模式后,除非您指定 UI 在 PiP 模式内外应如何显示,否则应用程序的整个 UI 将进入 PiP 窗口。
首先,您需要了解应用程序是否处于 PiP 模式。您可以使用 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()
来切换在应用程序进入 PiP 模式时显示哪些 UI 元素
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() }
确保应用程序在正确的时间进入 PiP 模式
您的应用程序不应在以下情况下进入 PiP 模式
- 如果视频已停止或暂停。
- 如果您处于应用程序中与视频播放器不同的页面上。
要控制应用程序何时进入 PiP 模式,请添加一个使用 mutableStateOf
跟踪视频播放器状态的变量。
根据视频是否正在播放来切换状态
要根据视频播放器是否正在播放来切换状态,请在视频播放器上添加一个监听器。根据播放器是否正在播放,切换状态变量的状态
player.addListener(object : Player.Listener { override fun onIsPlayingChanged(isPlaying: Boolean) { shouldEnterPipMode = isPlaying } })
根据播放器是否已释放来切换状态
当播放器被释放时,将状态变量设置为 false
fun releasePlayer() { shouldEnterPipMode = false }
使用状态来定义是否进入 PiP 模式(Android 12 之前)
- 由于在 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_TAG, "API does not support PiP") }
使用状态来定义是否进入 PiP 模式(Android 12 之后)
将状态变量传递到 setAutoEnterEnabled
中,以便应用程序仅在正确的时间进入 PiP 模式
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 为进入 PiP 模式创建更流畅的动画。在 Android 12+ 中,它还为退出 PiP 模式创建更流畅的动画。将此 API 添加到 PiP 构建器中,以指示在过渡到 PiP 之后可见的 Activity 区域。
- 仅当状态定义应用程序应该进入 PiP 模式时,才将
setSourceRectHint()
添加到构建器中。这避免了在应用程序不需要进入 PiP 时计算sourceRect
。 - 要设置
sourceRect
值,请使用layoutCoordinates
,这些坐标来自修饰符上的onGloballyPositioned
函数。 - 在构建器上调用
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 播放器中进行的操作。
添加远程操作
如果您想在 PiP 窗口中添加控件(播放、暂停等),请为要添加的每个控件创建一个 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)
后续步骤
在本指南中,您学习了在 Android 12 之前和 Android 12 之后在 Compose 中添加 PiP 的最佳实践。