Android picture-in-picture scale view
Asked Answered
N

4

11

Is there a way to use picture-in-picture feature on an activity that is not a video to show it scaled down ?

I have an activity with a giant progress bar and some text that I would like to display on PiP window while the user did some web browsing.

I already have

android:supportsPictureInPicture="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout"

set for the activity in the manifest.

and to start PiP

@Override
protected void onUserLeaveHint() {


    PictureInPictureParams params = new PictureInPictureParams.Builder()
            .build();
    enterPictureInPictureMode(params);

}

This is what my sample app looks like

enter image description here

I press home and it briefly animates to

enter image description here

and then quickly redraws to become

enter image description here

I am hoping to show PiP as it appears scaled down in picture #2 but after a quick animation it redraws to what it looks like in picture #3.

Is there anyway to achieve a scaled down view?

Please keep in mind this is not a going to be an app store app. It is a very targeted app on a dedicated tablet.

Nympholepsy answered 19/2, 2019 at 21:15 Comment(0)
S
6

Maybe a bit hacky, but you could change the DPI at runtime.

Following code uses onPictureInPictureModeChanged() to listen for a mode change and changes the DPI at the next restart.

public class Activity extends AppCompatActivity {

    private MyApplication mApplication;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mApplication = (MyApplication) getApplicationContext();

        if (mApplication.mode == MyApplication.MODE_NONE) {
            saveDpi();
        } else {
            setDpi();
        }

        setContentView(R.layout.activity);

        ...
    }

    private void saveDpi() {
        Configuration configuration = getResources().getConfiguration();
        mApplication.orgDensityDpi = configuration.densityDpi;
    }

    private void setDpi() {
        Configuration configuration = getResources().getConfiguration();
        DisplayMetrics metrics = getResources().getDisplayMetrics();
        if (mApplication.mode == MyApplication.MODE_PIP) {
            configuration.densityDpi = mApplication.orgDensityDpi / 3;
        } else {
            configuration.densityDpi = mApplication.orgDensityDpi;
        }
        getBaseContext().getResources().updateConfiguration(configuration, metrics);
    }

    @Override
    protected void onUserLeaveHint() {
        PictureInPictureParams params = new PictureInPictureParams.Builder().build();
        enterPictureInPictureMode(params);
    }

    @Override
    public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
        if (isInPictureInPictureMode) {
            mApplication.mode = MyApplication.MODE_PIP;
        } else {
            mApplication.mode = MyApplication.MODE_FULL;
        }
    }

}

Because onUserLeaveHint() - which starts the PIP mode - is called after onSaveInstanceState() the current mode can't be stored in a field of the activity class. It must be stored somewhere else where it survives a configuration change. A field in the application class is used here.

public class MyApplication extends Application {

    public static final int MODE_NONE = 0;
    public static final int MODE_FULL = 1;
    public static final int MODE_PIP = 2;

    public int mode = MODE_NONE;
    public int orgDensityDpi = 0;

}

(There's no need to prevent configuration changes with android:configChanges.)

Result:

result

Solder answered 10/3, 2019 at 17:37 Comment(2)
this answer works great but only at the cost of recreating the activity. it would be nice to achieve this without recreation. we would need to redraw/scale the layout without calling set contentviewPhotography
I was trying to implement something similar to this in koitlin but I can't figure out how to get static fields on the main application class. Can anybody help with this?Marlin
S
4

Here is another solution which uses fragments to show a scaled UI. Unlike my previous solution, this solution has the advantage to be able to show a UI which is optimized for the PIP mode. (E.g. some views can be hidden in the PIP mode.)

Following code uses onPictureInPictureModeChanged() to listen for a mode change and changes the UI at the next restart. (Because the toolbar is not needed in PIP mode, it is hidden before the PIP mode is entered.)

public class Activity extends AppCompatActivity {

    private static final String FRAGMENT_TAG_FULL = "fragment_full";
    private static final String FRAGMENT_TAG_PIP = "fragment_pip";

    private MyApplication mApplication;

    private Toolbar mToolbar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mApplication = (MyApplication) getApplicationContext();

        setContentView(R.layout.activity);

        mToolbar = findViewById(R.id.toolbar);
        setSupportActionBar(mToolbar);

        if (!mApplication.inPipMode) {
            showFullFragment();
        } else {
            showPipFragment();
        }
    }

    @Override
    protected void onUserLeaveHint() {
        mToolbar.setVisibility(View.GONE);
        PictureInPictureParams params = new PictureInPictureParams.Builder().build();
        enterPictureInPictureMode(params);
    }

    @Override
    public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
        if (isInPictureInPictureMode) {
            mApplication.inPipMode = true;
        } else {
            mApplication.inPipMode = false;
        }
    }

    private void showFullFragment() {
        Fragment fragment = new FragmentFull();
        getSupportFragmentManager().beginTransaction()
                .replace(R.id.container_content, fragment, FRAGMENT_TAG_FULL)
                .commit();
        mToolbar.setVisibility(View.VISIBLE);
    }

    private void showPipFragment() {
        Fragment fragment = new FragmentPip();
        getSupportFragmentManager().beginTransaction()
                .replace(R.id.container_content, fragment, FRAGMENT_TAG_PIP)
                .commit();
        mToolbar.setVisibility(View.GONE);
    }

}

Because onUserLeaveHint() - which starts the PIP mode - is called after onSaveInstanceState() the current mode can't be stored in a field of the activity class. It must be stored somewhere else where it survives a configuration change. A field in the application class is used here.

public class MyApplication extends Application {

    public boolean inPipMode = false;

}

Fragment layout for full screen mode:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:text="Hello World!"
        android:textSize="36sp" />

    <TextView
        android:id="@+id/text_detail"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/text"
        android:layout_centerHorizontal="true"
        android:text="&#x1F642;"
        android:textSize="28sp" />

</RelativeLayout>

Fragment layout for PIP mode:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:text="Hello World!"
        android:textSize="10sp"/>

</RelativeLayout>

Result:

result

Solder answered 14/3, 2019 at 0:20 Comment(3)
onCreate is not called when entering PIP mode... am I wrong?Roti
Entering PIP mode causes a configuration change. By default, the application is recreated an "onCreate" is called. However, this can be suppressed in the app manifest file. See: developer.android.com/guide/topics/ui/…Solder
Ok got it, so if I write in the manifest that I "handles" configuration change it will not call to onCreate againRoti
E
3

No need to include the configChanges in the manifest.

public class PictureIPActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);       
    setContentView(R.layout.activity_picture_ip);
    findViewById(R.id.tv_hello_world).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Toast.makeText(mApplication, "enabling PiP", Toast.LENGTH_SHORT).show();
            enterPiPMode();
        }
    });
}

@Override
protected void onPause() {
    super.onPause();
    enterPiPMode();
}

private void enterPiPMode() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        PictureInPictureParams params = new PictureInPictureParams.Builder()
                .setAspectRatio(getPipRatio()).build();
        enterPictureInPictureMode(params);
    }
}

public Rational getPipRatio() {
 //   anything with aspect ration below 0.5 and above 2.5 (roughly) will be 
 unpractical or unpleasant to use
    DisplayMetrics metrics = getResources().getDisplayMetrics();
    return new Rational(Math.round(metrics.xdpi), Math.round(metrics.ydpi));
}
}

The key for the animation to resize is the settings in the AndroidManifest.xml

  <activity android:name=".PictureIPActivity"
              android:resizeableActivity="true"
              android:launchMode="singleTask"
              android:supportsPictureInPicture="true">
Embattle answered 12/3, 2019 at 7:10 Comment(0)
C
1

Whenever the Activity either enters or exits the PIP mode it is destroyed and recreated (this is the behavior I've noted). The difference between the animation and end result is because when entering PIP mode the system animates by scaling down the activity and its UI components.

When the activity is recreated it is using the same layout you provided during the initial creation of the activity with the same dimens, the problem is that the Activity's configuration has changed and the device has entered into a smaller size configuration i.e. in your case from xlarge to either small or normal.

So now that we know the Activity is destroyed, you can handle screen size changes as you typically do.

Here's what you can do:

  1. Provide a new layout for the new configuration.
  2. Provide a new text size for the new configurations.
  3. Provide a new text size at run time on onPictureInPictureModeChanged() callback.

I achieved the desired result by adding a new dimens-small folder. You can choose one for yourself. This dimens.xml will contain the android:textSize="@dimen/textSize" for the small screen.


Now that that's done here is the reason you probably didn't look for recreation: according to the PIP Docs

specify that your activity handles layout configuration changes so that your activity doesn't relaunch when layout changes occur during PIP mode transitions.

And even though I added

android:resizeableActivity="true"
android:supportsPictureInPicture="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout"

in my <activity> tag in the manifest my Activity was still recreated at the end of each mode change.

Which is either a bug or something missing from either docs or the code. Or perhaps an unclear statement for transitions/animations only and not the actual end result.

Catha answered 12/3, 2019 at 10:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.