Android-Picture in Picture Support.

— As in android developers document, “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”.

Let’s see what exactly Picture-in-picture mode is with an example.

Say we make a whatsapp video call and press back button in phone. The video screen becomes small and that small window will be pinned in the corner of the screen (Corner will be choosen by the system). This will be our PIP window. Same goes with Google maps, maps continues to show directions even when we are in different app or when we presss home button.

With this in mind let’s start implementing picture-in-picture support. To support PIP we have to add supportsPictureInPicture and configChanges in our manifest file. configChanges handles layout configuartion changes so that our activity doesn’t relaunch whenever layout changes occur during PIP mode transitions.

Dont forget to add Internet Permission in manifest file and enable Data binding in gradle as we will be using databinding in this project.

Step 1 : Adding support for Picture in picture

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

<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=".PictureInPictureActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:launchMode="singleTask"
android:resizeableActivity="true"
android:supportsPictureInPicture="true"
tools:targetApi="n" />
<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>
build.gradle (Module level)dataBinding{
enabled = true
}

Step 2 : Designing MainActivity with Recyclerview and Navigation to PIPActivity

Before starting with coding part for PIP support, let’s add the MainActivity with recyclerview (i.e background activity when app enters into PIP mode).

MainActivity

import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import androidx.recyclerview.widget.LinearLayoutManager;
import android.content.Intent;
import android.os.Bundle;
import com.example.pictureinpicture.databinding.ActivityMainBinding;
import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity implements VideoAdapter.onNameClick{

private ActivityMainBinding binding;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this,R.layout.activity_main);

setAdapterAndPopulateList();
}
private void setAdapterAndPopulateList() {
List<VideoModel> videoList = new ArrayList<>();

videoList.add(new VideoModel("The Smoking Tire Video", "https://www.dropbox.com/s/0x2ke57h7wv49ll/Sample_512x288.mp4"));
videoList.add(new VideoModel("The Curling window", "https://www.dropbox.com/s/0x2ke57h7wv49ll/Sample_512x288.mp4"));
videoList.add(new VideoModel("The Joy", "https://www.dropbox.com/s/0x2ke57h7wv49ll/Sample_512x288.mp4"));

binding.recyclerView.setLayoutManager(new LinearLayoutManager(this));
VideoAdapter adapter = new VideoAdapter(videoList, this,MainActivity.this);
binding.recyclerView.setAdapter(adapter);
}

@Override
public void onUrlClick(String url) {
startActivity(new Intent(this, PictureInPictureActivity.class).putExtra("url", url));
}
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<layout>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<androidx.recyclerview.widget.RecyclerView
tools:listitem="@layout/item_video"
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

VideoAdapter.java

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.databinding.DataBindingUtil;
import androidx.recyclerview.widget.RecyclerView;
import com.example.pictureinpicture.databinding.ItemVideoBinding;
import java.util.List;

public class VideoAdapter extends RecyclerView.Adapter<VideoAdapter.VideoViewHolder> {

private final List<VideoModel> videoList;
private final Context context;
private final onNameClick onNameClick;

public VideoAdapter(List<VideoModel> videoList, Context context, onNameClick onNameClick) {
this.videoList = videoList;
this.context = context;
this.onNameClick= onNameClick;
}

@NonNull
@Override
public VideoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ItemVideoBinding videoBinding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.item_video, parent, false);
return new VideoViewHolder(videoBinding);
}

@Override
public void onBindViewHolder(@NonNull VideoViewHolder holder, int position) {
VideoModel videoModel = videoList.get(position);
holder.videoBinding.setModel(videoModel);
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
onNameClick.onUrlClick(videoList.get(position).getUrl());
}
});
}

@Override
public int getItemCount() {
return videoList.size();
}

public static class VideoViewHolder extends RecyclerView.ViewHolder {
ItemVideoBinding videoBinding;
public VideoViewHolder(@NonNull ItemVideoBinding itemView) {
super(itemView.getRoot());
videoBinding = itemView;
}
}

public interface onNameClick{
void onUrlClick(String url);
}
}

item_video.xml

<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="model"
type="com.example.pictureinpicture.VideoModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TextView
android:text="@{model.name}"
android:id="@+id/video_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="5dp"
android:textColor="@color/purple_200"
android:textSize="20sp"
android:textStyle="bold"
android:fontFamily="monospace"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />


</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

VideoModel.java

package com.example.pictureinpicture;

public class VideoModel {
private String name;
private String url;

public VideoModel(String name, String url) {
this.name = name;
this.url = url;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getUrl() {
return url;
}

public void setUrl(String url) {
this.url = url;
}
}

Step 3 : Switching activity to PIP, Hanlding UI during PIP and Continuing video playback while in PIP mode

We can enter into PIP mode by calling enterPictureInPictureMode() method.

/*sample code*/
public void enterPipMode(View view) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
/* If you want aspect ratios other than default */
Rational aspectRatio = new Rational(binding.videoView.getWidth(), binding.videoView.getHeight());
pipBuilder = new PictureInPictureParams.Builder();
pipBuilder.setAspectRatio(aspectRatio).build();
enterPictureInPictureMode(pipBuilder.build());
}
}

While entering into or exiting from the PIP mode the system calls onPictureInPictureModeChanged() method.

Note : After entering into PIP mode our activity screen will be very small. So, here we need to keep very minimal UI elements in order to provide best user experience.

/*sample code*/
@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
binding.setInPictureMode(isInPictureInPictureMode);
}

Suppose if user presses home or recent button the activity has to enter into PIP mode in some applications. This condition can be handled by overiding onUserLeaveHint() method.

/*sample code*/
@Override
public void onUserLeaveHint() {
if (!isInPictureInPictureMode()) {
/*Default height and width for screen*/
pipBuilder.build();
enterPictureInPictureMode(pipBuilder.build());
}
}

On click of “Enter Pip mode” button, we are entering into PIP mode (PictureInPictureActivity) along with MainActivity screen (whole screen) and also we are hiding “enter pip mode” button for better user experience as discussed above.

import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import android.app.PictureInPictureParams;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.util.Rational;
import android.view.View;
import com.example.pictureinpicture.databinding.ActivityPictureInPictureBinding;

@RequiresApi(api = Build.VERSION_CODES.O)
public class PictureInPictureActivity extends AppCompatActivity {

private ActivityPictureInPictureBinding binding;

private String url;

private PictureInPictureParams.Builder pipBuilder;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this,R.layout.activity_picture_in_picture);

binding.setHandler(this);

url = getIntent().getStringExtra("url");

binding.videoView.setVideoPath((url));
binding.videoView.requestFocus();
binding.videoView.start();
}
/* Switching activity to PIP*/
public void enterPipMode(View view) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
/* If you want aspect ratios other than default */
Rational aspectRatio = new Rational(binding.videoView.getWidth(), binding.videoView.getHeight());
pipBuilder = new PictureInPictureParams.Builder();
pipBuilder.setAspectRatio(aspectRatio).build();
enterPictureInPictureMode(pipBuilder.build());
}
}

@Override
public void onUserLeaveHint() {
if (!isInPictureInPictureMode()) {
/*Default height and width for screen*/
pipBuilder.build();
enterPictureInPictureMode(pipBuilder.build());
}
}
/*Hanlding UI during PIP*/
@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
binding.setInPictureMode(isInPictureInPictureMode);
}

@Override
public void onNewIntent(Intent i) {
super.onNewIntent(i);
updatePipMode();
}

private void updatePipMode() {
binding.videoView.setVideoPath((url));
binding.videoView.requestFocus();
}
}
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View"/>
<variable
name="handler"
type="com.example.pictureinpicture.PictureInPictureActivity" />
<variable
name="inPictureMode"
type="java.lang.Boolean" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".PictureInPictureActivity">
<VideoView
android:id="@+id/videoView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<androidx.appcompat.widget.AppCompatButton
android:textColor="@color/white"
android:text=" Enter PIP Mode "
android:textAllCaps="false"
android:background="@color/purple_500"
android:visibility="@{inPictureMode ? View.GONE : View.VISIBLE}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="30dp"
android:layout_marginRight="30dp"
android:layout_marginBottom="30dp"
android:onClick="@{handler :: enterPipMode}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Inside our manifest file we have added “launchMode” and set it to “singleTask” as to ensure a single activity is used for video playback requests and switched into or out of PIP mode.

Just double tap the videoview to enter back to normal mode and that’s all!

Here is the github link to download the project. :-)

Happy Coding!