画中画 (PiP) 是一种特殊类型的多窗口模式,主要用于视频播放。它允许用户在一个固定在屏幕角落的小窗口中观看视频,同时在应用程序之间导航或浏览主屏幕上的内容。
PiP 利用 Android 7.0 中提供的多窗口 API 提供固定的视频叠加窗口。要向您的应用添加 PiP,您需要注册您的活动,根据需要将您的活动切换到 PiP 模式,并确保在活动处于 PiP 模式时 UI 元素隐藏且视频播放继续。
本指南介绍如何在 Compose 中向您的应用添加 PiP,以及使用 Compose 视频实现。请参阅 Socialite 应用以查看这些最佳实践。
设置您的应用以使用 PiP
在您的 AndroidManifest.xml
文件的活动标签中,执行以下操作
- 添加
supportsPictureInPicture
并将其设置为true
以声明您将在应用中使用 PiP。 添加
configChanges
并将其设置为orientation|screenLayout|screenSize|smallestScreenSize
以指定您的活动处理布局配置更改。这样,当在 PiP 模式转换期间发生布局更改时,您的活动不会重新启动。<activity android:name=".SnippetsActivity" android:exported="true" android:supportsPictureInPicture="true" android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize" android:theme="@style/Theme.Snippets">
在您的 Compose 代码中,执行以下操作
- 在
Context
上添加此扩展。您将在本指南中多次使用此扩展来访问活动。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 info", "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 之前)
- 由于 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") }
使用状态定义是否进入 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 后可见的活动区域。
- 仅当状态定义应用应进入 PiP 模式时,才向
builder
添加setSourceRectHint()
。这避免了在应用不需要进入 PiP 时计算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 播放器,请在设置纵横比之前检查播放器是否不为空,以及播放器的视频尺寸是否不等于 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
- 为您的画中画窗口中的控件创建一个
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 之后。