Picture-in-picture (PiP) is a special type of multi-window mode mostly used for video playback. It lets the user watch a video in a small window pinned to a corner of the screen while navigating between apps or browsing content on the main screen.
PiP leverages the multi-window APIs made available in Android 7.0 to provide the pinned video overlay window. To add PiP to your app, you need to register your activity, switch your activity to PiP mode as needed, and make sure UI elements are hidden and video playback continues when the activity is in PiP mode.
This guide describes how to add PiP in Compose to your app with a Compose video implementation. See the Socialite app to see these best practices in action.
Set up your app for PiP
In the activity tag of your AndroidManifest.xml
file, do the following:
- Add
supportsPictureInPicture
and set it totrue
to declare you'll be using PiP in your app. Add
configChanges
and set it toorientation|screenLayout|screenSize|smallestScreenSize
to specify that your activity handles layout configuration changes. This way, your activity doesn't relaunch when layout changes occur during PiP mode transitions.<activity android:name=".SnippetsActivity" android:exported="true" android:supportsPictureInPicture="true" android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize" android:theme="@style/Theme.Snippets">
In your Compose code, do the following:
- Add this extension on
Context
. You'll use this extension multiple times throughout the guide to access the 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") }
Add PiP on leave app for pre-Android 12
To add PiP for pre-Android 12, use addOnUserLeaveHintProvider
. Follow
these steps to add PiP for pre-Android 12:
- Add a version gate so that this code is only accessed in versions O until R.
- Use a
DisposableEffect
withContext
as the key. - Inside the
DisposableEffect
, define the behavior for when theonUserLeaveHintProvider
is triggered using a lambda. In the lambda, callenterPictureInPictureMode()
onfindActivity()
and pass inPictureInPictureParams.Builder().build()
. - Add
addOnUserLeaveHintListener
usingfindActivity()
and pass in the lambda. - In
onDispose
, addremoveOnUserLeaveHintListener
usingfindActivity()
and pass in the 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") }
Add PiP on leave app for post-Android 12
Post-Android 12, the PictureInPictureParams.Builder
is added through a
modifier that is passed to the app's video player.
- Create a
modifier
and callonGloballyPositioned
on it. The layout coordinates will be used in a later step. - Create a variable for the
PictureInPictureParams.Builder()
. - Add an
if
statement to check if the SDK is S or above. If so, addsetAutoEnterEnabled
to the builder and set it totrue
to enter PiP mode upon swipe. This provides a smoother animation than going throughenterPictureInPictureMode
. - Use
findActivity()
to callsetPictureInPictureParams()
. Callbuild()
on thebuilder
and pass it in.
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)
Add PiP through a button
To enter PiP mode through a button click, call
enterPictureInPictureMode()
on findActivity()
.
The parameters are already set by previous calls to the
PictureInPictureParams.Builder
, so you do not need to set new parameters
on the builder. However, if you do want to change any parameters on button
click, you can set them here.
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!") }
Handle your UI in PiP mode
When you enter PiP mode, your app's entire UI enters the PiP window unless you specify how your UI should look in and out of PiP mode.
First, you need to know when your app is in PiP mode or not. You can use
OnPictureInPictureModeChangedProvider
to achieve this.
The code below tells you if your app is in PiP mode.
@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 } }
Now, you can use rememberIsInPipMode()
to toggle which UI elements to show
when the app enters PiP mode:
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() }
Make sure that your app enters PiP mode at the right times
Your app should not enter PiP mode in the following situations:
- If the video is stopped or paused.
- If you are on a different page of the app than the video player.
To control when your app enters PiP mode, add a variable that tracks the state
of the video player using a mutableStateOf
.
Toggle state based on if video is playing
To toggle the state based on if the video player is playing, add a listener on the video player. Toggle the state of your state variable based on if the player is playing or not:
player.addListener(object : Player.Listener { override fun onIsPlayingChanged(isPlaying: Boolean) { shouldEnterPipMode = isPlaying } })
Toggle state based on if player is released
When the player is released, set your state variable to false
:
fun releasePlayer() { shouldEnterPipMode = false }
Use state to define if PiP mode is entered (pre-Android 12)
- Since adding PiP pre-12 uses a
DisposableEffect
, you need to create a new variable byrememberUpdatedState
withnewValue
set as your state variable. This will ensure that the updated version is used within theDisposableEffect
. In the lambda that defines the behavior when the
OnUserLeaveHintListener
is triggered, add anif
statement with the state variable around the call toenterPictureInPictureMode()
: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") }
Use state to define if PiP mode is entered (post-Android 12)
Pass your state variable into setAutoEnterEnabled
so that your app only enters
PiP mode at the right time:
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)
Use setSourceRectHint
to implement a smooth animation
The setSourceRectHint
API creates a smoother animation for entering PiP
mode. In Android 12+, it also creates a smoother animation for exiting PiP mode.
Add this API to the PiP builder to indicate the area of the activity that is
visible following the transition into PiP.
- Only add
setSourceRectHint()
to thebuilder
if the state defines that the app should enter PiP mode. This avoids calculatingsourceRect
when the app does not need to enter PiP. - To set the
sourceRect
value, use thelayoutCoordinates
that are given from theonGloballyPositioned
function on the modifier. - Call
setSourceRectHint()
on thebuilder
and pass in thesourceRect
variable.
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)
Use setAspectRatio
to set PiP window's aspect ratio
To set the aspect ratio of the PiP window, you can either choose a specific
aspect ratio or use the width and height of the player's video size. If you are
using a media3 player, check that the player is not null and that the player's
video size is not equal to VideoSize.UNKNOWN
before setting the aspect
ratio.
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)
If you are using a custom player, set the aspect ratio on the player's height and width using the syntax specific to your player. Be aware that if your player resizes during initialization, if it falls outside of the valid bounds of what the aspect ratio can be, your app will crash. You may need to add checks around when the aspect ratio can be calculated, similar to how it is done for a media3 player.
Add remote actions
If you want to add controls (play, pause, etc.) to your PiP window, create a
RemoteAction
for each control you want to add.
- Add constants for your broadcast controls:
// 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
- Create a list of
RemoteActions
for the controls in your PiP window. - Next, add a
BroadcastReceiver
and overrideonReceive()
to set the actions of each button. Use aDisposableEffect
to register the receiver and the remote actions. When the player is disposed, unregister the receiver.@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) } } }
- Pass in a list of your remote actions to the
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)
Next steps
In this guide you learned the best practices of adding PiP in Compose both pre-Android 12 and post-Android 12.
- See the Socialite app to see the best practices of Compose PiP in action.
- See the PiP design guidance for more information.