Open In App

How to implement Floating Window (PIP) mode in Jetpack Compose?

Last Updated : 22 Jun, 2025
Comments
Improve
Suggest changes
Like Article
Like
Report

In a video player app, whenever the app is closed (paused) while the video is playing, the video player turns into a small window floating over the screen. This is called as a Floating Window, or Picture in Picture (PIP) mode. In this article, we will learn how to implement that PIP mode in Jetpack Compose.

Note: PiP mode is supported on Android 8.0 (API level 26) and higher.

What is PiP Mode?

Picture-in-Picture (PiP) mode is a special type of multi-window mode that allows users to continue watching video content in a small, resizable floating window while performing other tasks in the background. This feature is especially useful for multitasking such as replying to messages, browsing the web, or navigating apps without interrupting the video playback.

Features of PiP Mode

The PiP mode has several features which includes the following:

  1. Seamless transition between full screen and pip mode.
  2. Touch and drag functionality to reposition the window.
  3. Playback controls (play, pause, skip) optionally available in the pip interface.
  4. Responsive resizing based on screen space and orientation.
pip-in-compose


Step by Step Implementation

Step 1: Create a new project

To create a new project in the Android Studio, please refer to How to Create a new Project in Android Studio with Jetpack Compose.

Step 2: Create a Broadcast Receiver

First, let's create a Broadcast Receiver to receive UI events from action buttons while the screen is in PIP mode. Navigate to app > kotlin+java > {package-name}, right click on the folder and select, New > Kotlin Class/File and set the file name as PipReceiver.kt.

PipReceiver.kt:

Kotlin
package com.geeksforgeeks.demo

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent

/**
 * PipReceiver listens for broadcast actions triggered from Picture-in-Picture (PiP) mode.
 * Specifically, it responds to the custom play/pause action added to the PiP controls.
 */
class PipReceiver : BroadcastReceiver() {

    /**
     * Called when the broadcast associated with the PiP action is received.
     * This triggers playback to toggle (play or pause) via the VideoController.
     */
    override fun onReceive(context: Context, intent: Intent) {
        VideoController.togglePlayPause() // Toggle video playback state
    }
}


Step 3: Working with manifest file

Navigate to app > manifests > AndroidManifest.xml and make the following changes as follows. First, we need to register the broadcast receiver under the application tag similar to how we register an activity.

 <receiver android:name=".PipReceiver"/>

Finally, inside the activity scope of MainActivity or the activity you will be using, we need to allow support for Picture-In-Picture Mode and add config changes.

<activity
android:name=".MainActivity"
android:exported="true"
android:supportsPictureInPicture="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:theme="@style/Theme.Demo">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>


Step 4: Create a Video Playback Manager

Create a Kotlin class file VideoController.kt as similar to previous steps. This file provides centralized methods to start/pause the video.It maintains a WeakReference to the VideoView to avoid memory leaks. As a singleton object, it allows toggling video playback from outside the UI (e.g., PiP actions).

VideoController.kt:

Kotlin
package com.geeksforgeeks.demo

import android.widget.VideoView
import java.lang.ref.WeakReference

/**
 * VideoController is a singleton object that provides centralized control over video playback.
 * It allows external components (like the PiP Receiver) to toggle playback without direct access to the VideoView.
 */
object VideoController {

    // A weak reference to the VideoView to avoid memory leaks
    private var videoViewRef: WeakReference<VideoView>? = null

    /**
     * Registers the VideoView instance for external control.
     * Stores it as a weak reference to prevent memory leaks.
     */
    fun setVideoView(videoView: VideoView) {
        videoViewRef = WeakReference(videoView)
    }

    /**
     * Toggles the video playback state.
     * If the video is playing, it pauses it; otherwise, it starts playback.
     */
    fun togglePlayPause() {
        videoViewRef?.get()?.let { videoView ->
            if (videoView.isPlaying) {
                videoView.pause()
            } else {
                videoView.start()
            }
        }
    }
}


Step 5: Create a manager class for Picture-In-Picture Mode

This class can handle everything related to entering PiP mode, including setting bounds, aspect ratio, and actions. We will creating a custom RemoteAction (like a play/pause button) visible in the PiP window. Create a Kotlin Class named PipManager.kt and make the following changes as follows.

PipManager.kt
package com.geeksforgeeks.demo

import android.app.PendingIntent
import android.app.PictureInPictureParams
import android.app.RemoteAction
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Rect
import android.graphics.drawable.Icon
import android.net.Uri
import android.os.Build
import android.util.Rational
import androidx.annotation.RequiresApi

/**
 * PipManager handles all Picture-in-Picture (PiP) related functionality,
 * such as entering PiP mode and setting up custom actions.
 */
class PipManager(private val context: Context) {

    // Holds the bounds of the video on the screen, used to hint PiP position
    private var videoBounds: Rect = Rect()

    companion object {
        // Static video URL used by the player
        val VIDEO_URL: Uri =
            Uri.parse("https://www.sample-videos.com/video321/mp4/720/big_buck_bunny_720p_30mb.mp4")
    }

    // Check if PiP is supported on the current device
    private val isPipSupported: Boolean
        get() = context.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)

    /**
     * Updates the bounds of the video view. These bounds are used as a source hint
     * for positioning the PiP window when it appears.
     */
    fun updateBounds(bounds: Rect) {
        videoBounds = bounds
    }

    /**
     * Enters Picture-in-Picture mode if supported by the device and OS version.
     * Configures the PiP parameters such as aspect ratio, source rect hint,
     * and adds a custom action (play/pause button).
     */
    fun enterPipModeIfSupported() {
        // Exit early if PiP is not supported or the API level is below 26
        if (!isPipSupported || Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return

        // Cast context to Activity (required for PiP mode)
        val activity = context as? android.app.Activity ?: return

        // Build PiP parameters
        val pipParams = PictureInPictureParams.Builder()
            .setSourceRectHint(videoBounds) // Hint for initial PiP window placement
            .setAspectRatio(Rational(16, 9)) // Set fixed aspect ratio
            .setActions(listOf(buildRemoteAction())) // Add custom PiP action
            .build()

        // Request system to enter PiP mode
        activity.enterPictureInPictureMode(pipParams)
    }

    /**
     * Creates a custom RemoteAction (e.g. Play/Pause button) that appears in the PiP window.
     * The action sends a broadcast, which is received by PipReceiver to toggle playback.
     */
    @RequiresApi(Build.VERSION_CODES.O)
    private fun buildRemoteAction(): RemoteAction {
        // Create an Intent to broadcast when the PiP action is clicked
        val intent = Intent(context, PipReceiver::class.java)

        // Wrap the intent in a PendingIntent (required for RemoteAction)
        val pendingIntent = PendingIntent.getBroadcast(
            context,
            0,
            intent,
            PendingIntent.FLAG_IMMUTABLE
        )

        // Create and return the RemoteAction with an icon, title, description, and intent
        return RemoteAction(
            Icon.createWithResource(context, R.drawable.play_pause), // Icon shown in PiP
            "Play/Pause", // Title
            "Toggle playback", // Description
            pendingIntent // Action intent
        )
    }
}
play_pause.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="32dp"
    android:height="32dp"
    android:viewportWidth="960"
    android:viewportHeight="960">
  <path
      android:pathData="M200,648v-336l240,168 -240,168ZM520,640v-320h80v320h-80ZM680,640v-320h80v320h-80Z"
      android:fillColor="#e8eaed"/>
</vector>


Step 6: Create a composable for Video Player

Create a new Kotlin file with the name VideoPlayerScreen.kt and create a new composable inside the file which displays the video using VideoView inside a Compose layout via AndroidView. It starts playback automatically, registers the view with VideoController for external control, and reports its screen bounds to PipManager for proper Picture-in-Picture positioning. This composable bridges traditional views with modern Compose UI while enabling PiP support.


VideoPlayerScreen.kt:

Kotlin
package com.geeksforgeeks.demo

import android.net.Uri
import android.widget.VideoView
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toAndroidRect
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.viewinterop.AndroidView

/**
 * Composable function that displays a VideoView inside a Compose UI using AndroidView interop.
 *
 * @param videoUrl The URL of the video to be played.
 * @param onBoundsChanged Callback to report the video’s on-screen position for PiP purposes.
 */
@Composable
fun VideoPlayerScreen(
    videoUrl: Uri,
    onBoundsChanged: (android.graphics.Rect) -> Unit
) {
    AndroidView(
        factory = { context ->
            // Creates a VideoView and configures it to play the given video
            VideoView(context).apply {
                setVideoURI(videoUrl)  // Set the video source
                start()                // Start playback automatically
                VideoController.setVideoView(this)  // Register with VideoController for external control (e.g., play/pause from PiP)
            }
        },
        modifier = Modifier
            .fillMaxWidth() // Makes the video view fill the width of the parent
            .onGloballyPositioned {
                // Captures the position and size of the VideoView on the screen
                // Converts Compose Rect to Android Rect for compatibility with PiP APIs
                onBoundsChanged(it.boundsInWindow().toAndroidRect())
            }
    )
}


Step 7: Working with MainActivity.kt

In this file, we will define the VideoPlayerScreen composable with video url and layout bounds from the PipManager. We have overridden the onUserLeaveHint() to detect when the app is paused to turn on the PiP mode.

MainActivity.kt:

Kotlin
package com.geeksforgeeks.demo

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme

/**
 * MainActivity is the entry point of the app and hosts the Jetpack Compose UI.
 * It also integrates Picture-in-Picture (PiP) functionality using PipManager.
 */
class MainActivity : ComponentActivity() {

    // Manages Picture-in-Picture logic such as entering PiP mode and updating bounds
    private lateinit var pipManager: PipManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Initialize PipManager with the current Activity context
        pipManager = PipManager(this)

        // Set the Compose UI content
        setContent {
            MaterialTheme {
                // Display the video player and provide a callback to report its screen bounds
                VideoPlayerScreen(
                    videoUrl = PipManager.VIDEO_URL, // Static video URL
                    onBoundsChanged = { pipManager.updateBounds(it) } // Report video bounds to PipManager
                )
            }
        }
    }

    /**
     * Called when the user is about to leave the activity (e.g., pressing the Home button).
     * This is the trigger point for entering Picture-in-Picture mode.
     */
    override fun onUserLeaveHint() {
        super.onUserLeaveHint()
        pipManager.enterPipModeIfSupported() // Attempt to enter PiP mode if supported
    }
}

Output:


Refer to the following github repo to get the entire code: PIP_Mode_Jetpack_Compose


Next Article
Article Tags :

Similar Reads