ZOFTINO.COM android and web dev tutorials

Android Picture in Picture Mode Example

Picture in picture support, introduced in Android 8.0 API level 26 and uses multi window APIs, allows you to develop a feature in your app which lets users to view an activity in a small window pinned to a corner while users can do something on the main screen. This feature is intended for playing videos in a small window while user is trying to browse files on the main screen or to perform other actions.

In this post, you can learn how to make an activity enter and exit picture-in-picture mode, hide and show UI controls on activity screen depending on picture-in-picture mode and implement picture-in-picture feature with an example.

Picture in Picture Mode Example

Our example app, which shows how to implement picture-in-picture mode, first displays a screen which allows user to select a video. On selecting a video, it will start video activity and play the video in VideoView with an option of allowing user to choose videos. When user click the button, video activity will be shown in PIP mode and user can select video from the previous activity. On selecting a video again, it will reuse the video activity which is running in PIP mode and start playing new video in full-screen mode.

Below screen shows video activity in fullscreen mode.

android picture in picture example

Below screen shows video activity in PIP mode.

android picture in picture example

Entering Picture in Picture Mode

First step to provide picture-in-picture support in your app is configuring the activity that you want to provide picture-in-picture support for. In AndroidManifest xml file, you need to set android:resizeableActivity and android:supportsPictureInPicture attributes of activity declaration to true.

In Android, any layout or configuration change restarts an activity. Since we don’t want this behavior when an activity transitions into picture-in-picture mode, we need to add android:configChanges to inform the system that activity handles the configuration changes.

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

Once activity declaration is complete, you need to provide a way for user to make the activity display in picture-in-picture mode. In our example, it is button, clicking it displays the activity in picture-in-picture mode and shows previous activity that allows user to choose a different video file. Other way of showing PIP mode is when user presses home button or recents button.

To display an activity in picture-in-picture mode, you need to call enterPictureInPictureMode method of activity passing PictureInPictureParams.Builder object. PictureInPictureParams.Builder allows you to set actions for the activity in picture-in-picture mode using setActions method and to set aspect ratio of activity using setAspectRatio method.

 Rational aspectRatio = new Rational(vv.getWidth(), vv.getHeight());
pictureInPictureParamsBuilder.setAspectRatio(aspectRatio).build();
enterPictureInPictureMode(pictureInPictureParamsBuilder.build()); 

Exiting Picture in Picture Mode

When the activity which is in picture-in-picture mode is touched or tapped, it will display full screen and close controls to allow user to go back to full screen mode or close the activity as shown in the below screen.

android picture in picture close and fullscreen controls

You can declare the activity as singleTask by setting android:launchMode attribute in AndroidManifest file to reuse the same activity in full-screen mode with new content that user selects. In our example, when video activity is in picture-in-picture mode, user can select new video file and app will play the selected video using same activity because its launchMode is set to singleTask.

Picture-in-Picture Mode Change Callback Method

System calls activity’s onPictureInPictureModeChanged callback method when activity changes to and from picture-in-picture mode. This callback methods gives an opportunity to remove UI components not required when activity is in pip mode and add them back when it is in full-screen mode.

@Override
public void onPictureInPictureModeChanged (boolean isInPictureInPictureMode,
                                           Configuration newConfig) {
    if (isInPictureInPictureMode) {
        pip.setVisibility(View.GONE);
        tb.setVisibility(View.GONE);
    } else {
        pip.setVisibility(View.VISIBLE);
        tb.setVisibility(View.VISIBLE);
    }
}

PIP Mode when Home or Recents Buttons Pressed

You can display your video activity in picture-in-picture mode when user presses home or recents button. When user presses home or recents button, current activity goes to background. In the activity life cycle, onUserLeaveHint callback method will be called when an activity is about to go to background due to user actions.

So, onUserLeaveHint callback method can be used to show the activity in PIP mode when user presses home or recents button as shown below. You can check whether the activity is already in PIP mode using isInPictureInPictureMode method.

android picture in picture when home or recents buttons pressed

 @Override
public void onUserLeaveHint(){
    if(!isInPictureInPictureMode()){
        Rational aspectRatio = new Rational(vv.getWidth(), vv.getHeight());
        pictureInPictureParamsBuilder.setAspectRatio(aspectRatio).build();
        enterPictureInPictureMode(pictureInPictureParamsBuilder.build());
    }
}
 

Videos Activity

public class BrowseFilesActivity extends AppCompatActivity {

    private static String songOne =
            "http://www.sample-videos.com/video/mp4/720/big_buck_bunny_720p_1mb.mp4";
    private static String songTwo =
            "http://mirrors.standaloneinstaller.com/video-sample/lion-sample.mp4";
    private static String songThree =
            "http://mirrors.standaloneinstaller.com/video-sample/Panasonic_HDC_TM_700_P_50i.mp4";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.files_list);

        Toolbar tb = findViewById(R.id.toolbar);
        setSupportActionBar(tb);
        tb.setSubtitle("Picture in Picture");

        findViewById(R.id.video_one).setOnClickListener(onClickListener);
        findViewById(R.id.video_two).setOnClickListener(onClickListener);
        findViewById(R.id.video_three).setOnClickListener(onClickListener);
    }

    private final View.OnClickListener onClickListener =
            new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    switch (view.getId()) {
                        case R.id.video_one: {
                            showSelectedVideo(songOne);
                            break;
                        }
                        case R.id.video_two: {
                            showSelectedVideo(songTwo);
                            break;
                        }
                        case R.id.video_three:
                            showSelectedVideo(songThree);
                            break;
                    }
                }
            };
    public void showSelectedVideo(String url){
        Intent i = new Intent();
        i.setClass(this, PictureInPictureActivity.class);
        i.putExtra("videoUrl", url);
        startActivity(i);
    }
}

Videos Activity Layout

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary" />
    <Button
        android:id="@+id/video_one"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        style="@style/Widget.AppCompat.Button.Colored"
        android:text="Video One"/>
    <Button
        android:id="@+id/video_two"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        style="@style/Widget.AppCompat.Button.Colored"
        android:text="Video Two"/>
    <Button
        android:id="@+id/video_three"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        style="@style/Widget.AppCompat.Button.Colored"
        android:text="Video Three"/>
</LinearLayout>

Video Activity

import android.app.PictureInPictureParams;
import android.content.Intent;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Rational;
import android.view.View;
import android.widget.Button;
import android.widget.VideoView;

public class PictureInPictureActivity extends AppCompatActivity {

    private VideoView vv;
    private Toolbar tb;
    private Button pip;
    private Uri videoUri;
    private final PictureInPictureParams.Builder pictureInPictureParamsBuilder =
            new PictureInPictureParams.Builder();

    private String defaultVideo =
            "http://mirrors.standaloneinstaller.com/video-sample/metaxas-keller-Bell.mp4";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.play_video);

        tb = findViewById(R.id.toolbar);
        setSupportActionBar(tb);
        tb.setSubtitle("Picture in Picture");

        vv = findViewById(R.id.videoView);
        setVideoView(getIntent());

        findViewById(R.id.play).setOnClickListener(onClickListener);
        findViewById(R.id.pause).setOnClickListener(onClickListener);
        pip =  findViewById(R.id.pip);
        pip.setOnClickListener(onClickListener);
    }
    private final View.OnClickListener onClickListener =
            new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    switch (view.getId()) {
                        case R.id.play: {
                            vv.start();
                            break;
                        }
                        case R.id.pause: {
                            vv.stopPlayback();
                            break;
                        }
                        case R.id.pip:
                            pictureInPictureMode();
                            break;
                    }
                }
            };
    private void setVideoView(Intent i){
        String vUrl = i.getStringExtra("videoUrl");
        if(vUrl != null && !vUrl.isEmpty()){
            videoUri =
                    Uri.parse(vUrl);
        }else{
            videoUri =
                    Uri.parse(defaultVideo);
        }
        vv.setVideoURI(videoUri);
    }
    private void pictureInPictureMode(){
        Rational aspectRatio = new Rational(vv.getWidth(), vv.getHeight());
        pictureInPictureParamsBuilder.setAspectRatio(aspectRatio).build();
        enterPictureInPictureMode(pictureInPictureParamsBuilder.build());
    }
    @Override
    public void onUserLeaveHint(){
        if(!isInPictureInPictureMode()){
            Rational aspectRatio = new Rational(vv.getWidth(), vv.getHeight());
            pictureInPictureParamsBuilder.setAspectRatio(aspectRatio).build();
            enterPictureInPictureMode(pictureInPictureParamsBuilder.build());
        }
    }
    @Override
    public void onPictureInPictureModeChanged (boolean isInPictureInPictureMode,
                                               Configuration newConfig) {
        if (isInPictureInPictureMode) {
            pip.setVisibility(View.GONE);
            tb.setVisibility(View.GONE);
        } else {
            pip.setVisibility(View.VISIBLE);
            tb.setVisibility(View.VISIBLE);
        }
    }
    @Override
    public void onNewIntent(Intent i){
        setVideoView(i);
    }
    @Override
    public void onStop() {
        if( vv.isPlaying()){
            vv.stopPlayback();
        }
        super.onStop();
    }
}

Video Activity Layout

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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="match_parent">
    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>
    <Button
        android:id="@+id/pip"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="4dp"
        style="@style/Widget.AppCompat.Button.Colored"
        android:text="PIP"
        app:layout_constraintLeft_toRightOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/toolbar"></Button>
    <VideoView
        android:id="@+id/videoView"
        android:layout_width="match_parent"
        android:layout_height="400dp"
        android:layout_margin="4dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/pip"></VideoView>
    <Button
        android:id="@+id/play"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        style="@style/Widget.AppCompat.Button.Colored"
        android:text="Play"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@+id/pause"
        app:layout_constraintTop_toBottomOf="@+id/videoView"/>
    <Button
        android:id="@+id/pause"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        style="@style/Widget.AppCompat.Button.Colored"
        android:text="Pause"
        app:layout_constraintLeft_toRightOf="@+id/play"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/videoView"/>
</android.support.constraint.ConstraintLayout>

AndroidManifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="zoftino.com.latest">
    <uses-permission android:name="android.permission.INTERNET"></uses-permission>
    <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/AppTheme">
        <activity android:name=".BrowseFilesActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".PictureInPictureActivity"
            android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
            android:resizeableActivity="true"
            android:supportsPictureInPicture="true"
            android:launchMode="singleTask"></activity>
    </application>
</manifest>