Picture In Picture - Android Studio - Compose

How to add Picture In Picture Mode in the Android app?

Android 8.0 (API level 26) allows activities to launch in the picture-in-picture (PIP) mode. 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. The PIP window appears in the top layer of the screen in a corner chosen by the system.

Step 1: Create a new project OR Open your project

Step 2: Create another activity named PIPActivity

Step 3: Add the following properties to the PIPActivity in AndroidManifest

<activity android:name=".PIPActivity"
            android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
            android:launchMode="singleTask"
            android:resizeableActivity="true"
            android:supportsPictureInPicture="true"/>

Step 3: Code

AndroidMenifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.blogspot.atifsoftwares.pictureinpicture">

    <!-- internet permission -->
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.PictureInPicture">

        <activity android:name=".PIPActivity"
            android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
            android:launchMode="singleTask"
            android:resizeableActivity="true"
            android:supportsPictureInPicture="true"/>

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

MainActivity.kt

package com.technifysoft.myapplication

import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.technifysoft.myapplication.ui.theme.MyApplicationTheme

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            VideoSelectionScreen()
        }
    }
}

// video urls
private const val videoUrlOne = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
private const val videoUrlTwo = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/WeAreGoingOnBullrun.mp4"
private const val videoUrlThree = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/VolkswagenGTIReview.mp4"


@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VideoSelectionScreen() {
    val context = LocalContext.current

    Scaffold(
        topBar = {
            TopAppBar(title = { Text("Select a Video") })
        }
    ) { padding ->
        Column(
            modifier = Modifier
                .padding(padding)
                .padding(16.dp)
                .fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            VideoButton("Video One") {
                playVideo(context, videoUrlOne)
            }
            Spacer(modifier = Modifier.height(12.dp))
            VideoButton("Video Two") {
                playVideo(context, videoUrlTwo)
            }
            Spacer(modifier = Modifier.height(12.dp))
            VideoButton("Video Three") {
                playVideo(context, videoUrlThree)
            }
        }
    }
}

@Composable
fun VideoButton(text: String, onClick: () -> Unit) {
    Button(
        onClick = onClick,
        modifier = Modifier
            .fillMaxWidth(0.9f)
            .height(55.dp),
        shape = MaterialTheme.shapes.medium
    ) {
        Text(text)
    }
}

private fun playVideo(context: Context, videoUrl: String?) {
    //Intent to start activity, with video url
    val intent = Intent(context, PIPActivity::class.java)
        .apply {
            putExtra("videoURL", videoUrl)
        }
    context.startActivity(intent)
}


/**
 * GreetingPreview is a composable function for previewing the MainUI in Android Studio.
 * It is annotated with @Preview to enable live preview.
 *
 */
@Preview(showBackground = true)
@Composable
private fun GreetingPreview() {
    MyApplicationTheme {
        VideoSelectionScreen()
    }
}

PIPActivity.kt

package com.technifysoft.myapplication

import android.app.PictureInPictureParams
import android.content.Intent
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.util.Rational
import android.widget.MediaController
import android.widget.VideoView
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.net.toUri
import com.technifysoft.myapplication.ui.theme.MyApplicationTheme

class PIPActivity  : ComponentActivity() {

    private var videoUrl: String? = null
    private var videoView: VideoView? = null
    private var pipParams: PictureInPictureParams.Builder? = null

    private val TAG = "PIP_TAG"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        videoUrl = intent.getStringExtra("videoURL")

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            pipParams = PictureInPictureParams.Builder()
        }

        setContent {
            MaterialTheme {
                // ✅ Place Composable content inside a proper @Composable block
                VideoPlayerScreen(
                    videoUrl = videoUrl,
                    onEnterPip = { enterPipMode() },
                    onVideoViewReady = { videoView = it }
                )
            }
        }

    }

    private fun enterPipMode() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && videoView != null) {
            val aspectRatio = Rational(videoView!!.width, videoView!!.height)
            pipParams?.setAspectRatio(aspectRatio)
            enterPictureInPictureMode(pipParams!!.build())
            Log.d(TAG, "enterPipMode: Entered PIP mode")
        } else {
            Log.d(TAG, "enterPipMode: PIP not supported or VideoView null")
        }
    }

    override fun onUserLeaveHint() {
        super.onUserLeaveHint()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && !isInPictureInPictureMode) {
            Log.d(TAG, "onUserLeaveHint: Entering PIP mode")
            enterPipMode()
        }
    }

    override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
        super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
        Log.d(TAG, "PIP Mode Changed: $isInPictureInPictureMode")
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        videoUrl = intent.getStringExtra("videoURL")
        videoUrl?.let {
            videoView?.setVideoURI(it.toUri())
            videoView?.start()
        }
    }

    override fun onStop() {
        super.onStop()
        videoView?.stopPlayback()
    }
}


@Composable
fun VideoPlayerScreen(
    videoUrl: String?,
    onEnterPip: () -> Unit,
    onVideoViewReady: (VideoView) -> Unit
) {
    val context = LocalContext.current

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        // ✅ Wrap AndroidView inside Box or Column — not directly inside @UiComposable slots
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .aspectRatio(16 / 9f)
        ) {
            AndroidView(
                factory = { ctx ->
                    val videoView = VideoView(ctx)
                    val mediaController = MediaController(ctx)
                    mediaController.setAnchorView(videoView)
                    videoView.setMediaController(mediaController)

                    videoUrl?.let {
                        val uri = it.toUri()
                        videoView.setVideoURI(uri)
                        videoView.setOnPreparedListener { mp ->
                            Log.d("PIP_TAG", "Video Prepared, Playing...")
                            mp.start()
                        }
                    }
                    onVideoViewReady(videoView)
                    videoView
                },
                modifier = Modifier.fillMaxSize()
            )
        }

        Spacer(modifier = Modifier.height(24.dp))

        Button(
            onClick = onEnterPip,
            modifier = Modifier
                .fillMaxWidth(0.8f)
                .height(50.dp)
        ) {
            Text("Enter Picture-in-Picture Mode")
        }
    }

}

/**
 * GreetingPreview is a composable function for previewing the MainUI in Android Studio.
 * It is annotated with @Preview to enable live preview.
 *
 */
@Preview(showBackground = true)
@Composable
private fun GreetingPreview() {
    MyApplicationTheme {
        //VideoPlayerScreen()
    }
}

Screenshots



Comments

Popular posts from this blog

Picture In Picture - Android Studio - Kotlin

Manage External Storage Permission - Android Studio - Kotlin

How to add AIDL folder | Android Studio