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 to
true
to declare you'll be
using PiP in your app.
Add
configChanges
and set it to
orientation|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
with
Context
as the key.
- Inside the
DisposableEffect
, define the behavior for when the
onUserLeaveHintProvider
is triggered using a lambda. In the lambda, call
enterPictureInPictureMode()
on
findActivity()
and pass in
PictureInPictureParams.Builder().build()
.
- Add
addOnUserLeaveHintListener
using
findActivity()
and pass in the lambda.
- In
onDispose
, add
removeOnUserLeaveHintListener
using
findActivity()
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_TAG, "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 call
onGloballyPositioned
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, add
setAutoEnterEnabled
to the builder and set it to
true
to enter PiP
mode upon swipe. This provides a smoother animation than going through
enterPictureInPictureMode
.
- Use
findActivity()
to call
setPictureInPictureParams()
. Call
build()
on
the
builder
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)
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 by
rememberUpdatedState
with
newValue
set as your
state variable. This will ensure that the updated version is used within the
DisposableEffect
.
In the lambda that defines the behavior when the
OnUserLeaveHintListener
is triggered, add an
if
statement with the state variable around the call to
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")
}
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 the
builder
if the state defines that the
app should enter PiP mode. This avoids calculating
sourceRect
when the app
does not need to enter PiP.
- To set the
sourceRect
value, use the
layoutCoordinates
that are given
from the
onGloballyPositioned
function on the modifier.
- Call
setSourceRectHint()
on the
builder
and pass in the
sourceRect
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 override
onReceive()
to set the
actions of each button. Use a
DisposableEffect
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.