Move TV PiP away from keep clear areas
The PiP is placed in a way to avoid keep clear areas. If no areas overlapping the default PiP anchor position are defined, the PiP will stay at its anchor position. Otherwise, it will try to move to a free location closest to its anchor position. Unrestricted areas can cause the PiP to move across the entire screen, but restricted areas have limited influence and can only move the PiP a short distance. If no free position is found, the PiP will be stashed for some time. To find the position to move to once the PiP unstashes, a relaxed search is performed, excluding a restricted keep clear area. If this still does not yield a position to move to, only unrestricted areas will be taken into account. The PiP is stashed along the edge closest to the unstash position. Bug: 218416347 Bug: 218494300 Test: atest TvPipKeepClearAlgorithmTest Change-Id: I76c6527320fc403bb604b188f38eed505eeaab89
This commit is contained in:
@@ -33,4 +33,10 @@
|
||||
<!-- The default gravity for the picture-in-picture window.
|
||||
Currently, this maps to Gravity.BOTTOM | Gravity.RIGHT -->
|
||||
<integer name="config_defaultPictureInPictureGravity">0x55</integer>
|
||||
|
||||
<!-- Fraction of screen width/height restricted keep clear areas can move the PiP. -->
|
||||
<fraction name="config_pipMaxRestrictedMoveDistance">15%</fraction>
|
||||
|
||||
<!-- Duration (in milliseconds) the PiP stays stashed before automatically unstashing. -->
|
||||
<integer name="config_pipStashDuration">5000</integer>
|
||||
</resources>
|
||||
|
||||
24
libs/WindowManager/Shell/res/values-television/dimen.xml
Normal file
24
libs/WindowManager/Shell/res/values-television/dimen.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2022 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<!-- These resources are around just to allow their values to be customized
|
||||
for TV products. Do not translate. -->
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
|
||||
<!-- Padding between PIP and keep clear areas that caused it to move. -->
|
||||
<dimen name="pip_keep_clear_area_padding">16dp</dimen>
|
||||
</resources>
|
||||
@@ -70,7 +70,8 @@ public abstract class TvPipModule {
|
||||
TaskStackListenerImpl taskStackListener,
|
||||
DisplayController displayController,
|
||||
WindowManagerShellWrapper windowManagerShellWrapper,
|
||||
@ShellMainThread ShellExecutor mainExecutor) {
|
||||
@ShellMainThread ShellExecutor mainExecutor,
|
||||
@ShellMainThread Handler mainHandler) {
|
||||
return Optional.of(
|
||||
TvPipController.create(
|
||||
context,
|
||||
@@ -84,7 +85,8 @@ public abstract class TvPipModule {
|
||||
taskStackListener,
|
||||
displayController,
|
||||
windowManagerShellWrapper,
|
||||
mainExecutor));
|
||||
mainExecutor,
|
||||
mainHandler));
|
||||
}
|
||||
|
||||
@WMSingleton
|
||||
|
||||
@@ -74,7 +74,7 @@ public class PipBoundsAlgorithm {
|
||||
/**
|
||||
* TODO: move the resources to SysUI package.
|
||||
*/
|
||||
protected void reloadResources(Context context) {
|
||||
private void reloadResources(Context context) {
|
||||
final Resources res = context.getResources();
|
||||
mDefaultAspectRatio = res.getFloat(
|
||||
R.dimen.config_pictureInPictureDefaultAspectRatio);
|
||||
|
||||
@@ -54,11 +54,15 @@ public class PipBoundsState {
|
||||
public static final int STASH_TYPE_NONE = 0;
|
||||
public static final int STASH_TYPE_LEFT = 1;
|
||||
public static final int STASH_TYPE_RIGHT = 2;
|
||||
public static final int STASH_TYPE_BOTTOM = 3;
|
||||
public static final int STASH_TYPE_TOP = 4;
|
||||
|
||||
@IntDef(prefix = { "STASH_TYPE_" }, value = {
|
||||
STASH_TYPE_NONE,
|
||||
STASH_TYPE_LEFT,
|
||||
STASH_TYPE_RIGHT
|
||||
STASH_TYPE_RIGHT,
|
||||
STASH_TYPE_BOTTOM,
|
||||
STASH_TYPE_TOP
|
||||
})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface StashType {}
|
||||
|
||||
@@ -28,15 +28,21 @@ import static com.android.wm.shell.pip.tv.TvPipBoundsState.ORIENTATION_VERTICAL;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Rect;
|
||||
import android.os.SystemClock;
|
||||
import android.util.ArraySet;
|
||||
import android.util.Log;
|
||||
import android.util.Size;
|
||||
import android.view.Gravity;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.wm.shell.R;
|
||||
import com.android.wm.shell.common.DisplayLayout;
|
||||
import com.android.wm.shell.pip.PipBoundsAlgorithm;
|
||||
import com.android.wm.shell.pip.PipSnapAlgorithm;
|
||||
import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Contains pip bounds calculations that are specific to TV.
|
||||
@@ -46,91 +52,129 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
|
||||
private static final String TAG = TvPipBoundsAlgorithm.class.getSimpleName();
|
||||
private static final boolean DEBUG = TvPipController.DEBUG;
|
||||
|
||||
private final @android.annotation.NonNull TvPipBoundsState mTvPipBoundsState;
|
||||
private final @NonNull TvPipBoundsState mTvPipBoundsState;
|
||||
|
||||
private int mFixedExpandedHeightInPx;
|
||||
private int mFixedExpandedWidthInPx;
|
||||
|
||||
private final TvPipKeepClearAlgorithm mKeepClearAlgorithm;
|
||||
|
||||
public TvPipBoundsAlgorithm(Context context,
|
||||
@NonNull TvPipBoundsState tvPipBoundsState,
|
||||
@NonNull PipSnapAlgorithm pipSnapAlgorithm) {
|
||||
super(context, tvPipBoundsState, pipSnapAlgorithm);
|
||||
this.mTvPipBoundsState = tvPipBoundsState;
|
||||
this.mKeepClearAlgorithm = new TvPipKeepClearAlgorithm(SystemClock::uptimeMillis);
|
||||
reloadResources(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reloadResources(Context context) {
|
||||
super.reloadResources(context);
|
||||
private void reloadResources(Context context) {
|
||||
final Resources res = context.getResources();
|
||||
mFixedExpandedHeightInPx = res.getDimensionPixelSize(
|
||||
com.android.internal.R.dimen.config_pictureInPictureExpandedHorizontalHeight);
|
||||
mFixedExpandedWidthInPx = res.getDimensionPixelSize(
|
||||
com.android.internal.R.dimen.config_pictureInPictureExpandedVerticalWidth);
|
||||
mKeepClearAlgorithm.setPipAreaPadding(
|
||||
res.getDimensionPixelSize(R.dimen.pip_keep_clear_area_padding));
|
||||
mKeepClearAlgorithm.setMaxRestrictedDistanceFraction(
|
||||
res.getFraction(R.fraction.config_pipMaxRestrictedMoveDistance, 1, 1));
|
||||
mKeepClearAlgorithm.setStashDuration(res.getInteger(R.integer.config_pipStashDuration));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Context context) {
|
||||
super.onConfigurationChanged(context);
|
||||
reloadResources(context);
|
||||
}
|
||||
|
||||
/** Returns the destination bounds to place the PIP window on entry. */
|
||||
@Override
|
||||
public Rect getEntryDestinationBounds() {
|
||||
if (DEBUG) Log.d(TAG, "getEntryDestinationBounds()");
|
||||
if (mTvPipBoundsState.getTvExpandedAspectRatio() != 0
|
||||
if (mTvPipBoundsState.isTvExpandedPipSupported()
|
||||
&& mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0
|
||||
&& !mTvPipBoundsState.isTvPipManuallyCollapsed()) {
|
||||
updatePositionOnExpandToggled(Gravity.NO_GRAVITY, true);
|
||||
updateExpandedPipSize();
|
||||
updateGravityOnExpandToggled(Gravity.NO_GRAVITY, true);
|
||||
mTvPipBoundsState.setTvPipExpanded(true);
|
||||
}
|
||||
return getTvPipBounds(true);
|
||||
return getTvPipBounds().getBounds();
|
||||
}
|
||||
|
||||
/** Returns the current bounds adjusted to the new aspect ratio, if valid. */
|
||||
@Override
|
||||
public Rect getAdjustedDestinationBounds(Rect currentBounds, float newAspectRatio) {
|
||||
if (DEBUG) Log.d(TAG, "getAdjustedDestinationBounds: " + newAspectRatio);
|
||||
return getTvPipBounds(mTvPipBoundsState.isTvPipExpanded());
|
||||
return getTvPipBounds().getBounds();
|
||||
}
|
||||
|
||||
/**
|
||||
* The normal bounds at a different position on the screen.
|
||||
* Calculates the PiP bounds.
|
||||
*/
|
||||
public Rect getTvNormalBounds() {
|
||||
Rect normalBounds = getNormalBounds();
|
||||
Rect insetBounds = new Rect();
|
||||
public Placement getTvPipBounds() {
|
||||
final Size pipSize = getPipSize();
|
||||
final Rect displayBounds = mTvPipBoundsState.getDisplayBounds();
|
||||
final Size screenSize = new Size(displayBounds.width(), displayBounds.height());
|
||||
final Rect insetBounds = new Rect();
|
||||
getInsetBounds(insetBounds);
|
||||
|
||||
Set<Rect> restrictedKeepClearAreas = mTvPipBoundsState.getRestrictedKeepClearAreas();
|
||||
Set<Rect> unrestrictedKeepClearAreas = mTvPipBoundsState.getUnrestrictedKeepClearAreas();
|
||||
|
||||
if (mTvPipBoundsState.isImeShowing()) {
|
||||
if (DEBUG) Log.d(TAG, "IME showing, height: " + mTvPipBoundsState.getImeHeight());
|
||||
insetBounds.bottom -= mTvPipBoundsState.getImeHeight();
|
||||
|
||||
final Rect imeBounds = new Rect(
|
||||
0,
|
||||
insetBounds.bottom - mTvPipBoundsState.getImeHeight(),
|
||||
insetBounds.right,
|
||||
insetBounds.bottom);
|
||||
|
||||
unrestrictedKeepClearAreas = new ArraySet<>(unrestrictedKeepClearAreas);
|
||||
unrestrictedKeepClearAreas.add(imeBounds);
|
||||
}
|
||||
|
||||
Rect result = new Rect();
|
||||
Gravity.apply(mTvPipBoundsState.getTvPipGravity(), normalBounds.width(),
|
||||
normalBounds.height(), insetBounds, result);
|
||||
mKeepClearAlgorithm.setGravity(mTvPipBoundsState.getTvPipGravity());
|
||||
mKeepClearAlgorithm.setScreenSize(screenSize);
|
||||
mKeepClearAlgorithm.setMovementBounds(insetBounds);
|
||||
mKeepClearAlgorithm.setStashOffset(mTvPipBoundsState.getStashOffset());
|
||||
|
||||
final Placement placement = mKeepClearAlgorithm.calculatePipPosition(
|
||||
pipSize,
|
||||
restrictedKeepClearAreas,
|
||||
unrestrictedKeepClearAreas);
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "normalBounds: " + normalBounds.toShortString());
|
||||
Log.d(TAG, "pipSize: " + pipSize);
|
||||
Log.d(TAG, "screenSize: " + screenSize);
|
||||
Log.d(TAG, "stashOffset: " + mTvPipBoundsState.getStashOffset());
|
||||
Log.d(TAG, "insetBounds: " + insetBounds.toShortString());
|
||||
Log.d(TAG, "pipSize: " + pipSize);
|
||||
Log.d(TAG, "gravity: " + Gravity.toString(mTvPipBoundsState.getTvPipGravity()));
|
||||
Log.d(TAG, "resultBounds: " + result.toShortString());
|
||||
Log.d(TAG, "restrictedKeepClearAreas: " + restrictedKeepClearAreas);
|
||||
Log.d(TAG, "unrestrictedKeepClearAreas: " + unrestrictedKeepClearAreas);
|
||||
Log.d(TAG, "placement: " + placement);
|
||||
}
|
||||
|
||||
mTvPipBoundsState.setTvPipExpanded(false);
|
||||
|
||||
return result;
|
||||
return placement;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return previous gravity if it is to be saved, or Gravity.NO_GRAVITY if not.
|
||||
* @return previous gravity if it is to be saved, or {@link Gravity#NO_GRAVITY} if not.
|
||||
*/
|
||||
int updatePositionOnExpandToggled(int previousGravity, boolean expanding) {
|
||||
int updateGravityOnExpandToggled(int previousGravity, boolean expanding) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "updatePositionOnExpandToggle(), expanding: " + expanding
|
||||
Log.d(TAG, "updateGravityOnExpandToggled(), expanding: " + expanding
|
||||
+ ", mOrientation: " + mTvPipBoundsState.getTvFixedPipOrientation()
|
||||
+ ", previous gravity: " + Gravity.toString(previousGravity));
|
||||
}
|
||||
|
||||
if (!mTvPipBoundsState.isTvExpandedPipEnabled()) {
|
||||
if (!mTvPipBoundsState.isTvExpandedPipSupported()) {
|
||||
return Gravity.NO_GRAVITY;
|
||||
}
|
||||
|
||||
if (expanding && mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_UNDETERMINED) {
|
||||
float expandedRatio = mTvPipBoundsState.getTvExpandedAspectRatio();
|
||||
float expandedRatio = mTvPipBoundsState.getDesiredTvExpandedAspectRatio();
|
||||
if (expandedRatio == 0) {
|
||||
return Gravity.NO_GRAVITY;
|
||||
}
|
||||
@@ -139,7 +183,6 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
|
||||
} else {
|
||||
mTvPipBoundsState.setTvFixedPipOrientation(ORIENTATION_HORIZONTAL);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int gravityToSave = Gravity.NO_GRAVITY;
|
||||
@@ -181,10 +224,10 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if position changed
|
||||
* @return true if gravity changed
|
||||
*/
|
||||
boolean updatePosition(int keycode) {
|
||||
if (DEBUG) Log.d(TAG, "updatePosition, keycode: " + keycode);
|
||||
boolean updateGravity(int keycode) {
|
||||
if (DEBUG) Log.d(TAG, "updateGravity, keycode: " + keycode);
|
||||
|
||||
// Check if position change is valid
|
||||
if (mTvPipBoundsState.isTvPipExpanded()) {
|
||||
@@ -247,26 +290,31 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
|
||||
return false;
|
||||
}
|
||||
|
||||
private Size getPipSize() {
|
||||
final boolean isExpanded =
|
||||
mTvPipBoundsState.isTvExpandedPipSupported() && mTvPipBoundsState.isTvPipExpanded()
|
||||
&& mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0;
|
||||
if (isExpanded) {
|
||||
return mTvPipBoundsState.getTvExpandedSize();
|
||||
} else {
|
||||
final Rect normalBounds = getNormalBounds();
|
||||
return new Size(normalBounds.width(), normalBounds.height());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the PiP bounds.
|
||||
* Updates {@link TvPipBoundsState#getTvExpandedSize()} based on
|
||||
* {@link TvPipBoundsState#getDesiredTvExpandedAspectRatio()}, the screen size.
|
||||
*/
|
||||
public Rect getTvPipBounds(boolean expandedIfPossible) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "getExpandedBoundsIfPossible with gravity "
|
||||
+ Gravity.toString(mTvPipBoundsState.getTvPipGravity())
|
||||
+ ", fixed orientation: " + mTvPipBoundsState.getTvFixedPipOrientation());
|
||||
}
|
||||
void updateExpandedPipSize() {
|
||||
final DisplayLayout displayLayout = mTvPipBoundsState.getDisplayLayout();
|
||||
final float expandedRatio =
|
||||
mTvPipBoundsState.getDesiredTvExpandedAspectRatio(); // width / height
|
||||
|
||||
if (!mTvPipBoundsState.isTvExpandedPipEnabled() || !expandedIfPossible) {
|
||||
return getTvNormalBounds();
|
||||
}
|
||||
|
||||
DisplayLayout displayLayout = mTvPipBoundsState.getDisplayLayout();
|
||||
float expandedRatio = mTvPipBoundsState.getTvExpandedAspectRatio(); // width / height
|
||||
Size expandedSize;
|
||||
final Size expandedSize;
|
||||
if (expandedRatio == 0) {
|
||||
Log.d(TAG, "Expanded mode not supported");
|
||||
return getTvNormalBounds();
|
||||
Log.d(TAG, "updateExpandedPipSize(): Expanded mode aspect ratio of 0 not supported");
|
||||
return;
|
||||
} else if (expandedRatio < 1) {
|
||||
// vertical
|
||||
if (mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_HORIZONTAL) {
|
||||
@@ -300,26 +348,14 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
|
||||
}
|
||||
}
|
||||
|
||||
if (expandedSize == null) {
|
||||
return getTvNormalBounds();
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "expanded size, width: " + expandedSize.getWidth()
|
||||
+ ", height: " + expandedSize.getHeight());
|
||||
}
|
||||
|
||||
Rect insetBounds = new Rect();
|
||||
getInsetBounds(insetBounds);
|
||||
|
||||
Rect expandedBounds = new Rect();
|
||||
Gravity.apply(mTvPipBoundsState.getTvPipGravity(), expandedSize.getWidth(),
|
||||
expandedSize.getHeight(), insetBounds, expandedBounds);
|
||||
if (DEBUG) Log.d(TAG, "expanded bounds: " + expandedBounds.toShortString());
|
||||
|
||||
mTvPipBoundsState.setTvExpandedSize(expandedSize);
|
||||
mTvPipBoundsState.setTvPipExpanded(true);
|
||||
return expandedBounds;
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "updateExpandedPipSize(): expanded size, width=" + expandedSize.getWidth()
|
||||
+ ", height=" + expandedSize.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
void keepUnstashedForCurrentKeepClearAreas() {
|
||||
mKeepClearAlgorithm.keepUnstashedForCurrentKeepClearAreas();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,10 +53,10 @@ public class TvPipBoundsState extends PipBoundsState {
|
||||
|
||||
public static final int DEFAULT_TV_GRAVITY = Gravity.BOTTOM | Gravity.RIGHT;
|
||||
|
||||
private boolean mIsTvExpandedPipEnabled;
|
||||
private final boolean mIsTvExpandedPipSupported;
|
||||
private boolean mIsTvPipExpanded;
|
||||
private boolean mTvPipManuallyCollapsed;
|
||||
private float mTvExpandedAspectRatio;
|
||||
private float mDesiredTvExpandedAspectRatio;
|
||||
private @Orientation int mTvFixedPipOrientation;
|
||||
private int mTvPipGravity;
|
||||
private @Nullable Size mTvExpandedSize;
|
||||
@@ -64,8 +64,8 @@ public class TvPipBoundsState extends PipBoundsState {
|
||||
|
||||
public TvPipBoundsState(@NonNull Context context) {
|
||||
super(context);
|
||||
setIsTvExpandedPipEnabled(context.getPackageManager().hasSystemFeature(
|
||||
PackageManager.FEATURE_EXPANDED_PICTURE_IN_PICTURE));
|
||||
mIsTvExpandedPipSupported = context.getPackageManager().hasSystemFeature(
|
||||
PackageManager.FEATURE_EXPANDED_PICTURE_IN_PICTURE);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,7 +75,7 @@ public class TvPipBoundsState extends PipBoundsState {
|
||||
public void setBoundsStateForEntry(ComponentName componentName, ActivityInfo activityInfo,
|
||||
PictureInPictureParams params, PipBoundsAlgorithm pipBoundsAlgorithm) {
|
||||
super.setBoundsStateForEntry(componentName, activityInfo, params, pipBoundsAlgorithm);
|
||||
setTvExpandedAspectRatio(params.getExpandedAspectRatio(), true);
|
||||
setDesiredTvExpandedAspectRatio(params.getExpandedAspectRatio(), true);
|
||||
}
|
||||
|
||||
/** Resets the TV PiP state for a new activity. */
|
||||
@@ -85,32 +85,32 @@ public class TvPipBoundsState extends PipBoundsState {
|
||||
}
|
||||
|
||||
/** Set the tv expanded bounds of PIP */
|
||||
public void setTvExpandedSize(@Nullable Size bounds) {
|
||||
mTvExpandedSize = bounds;
|
||||
public void setTvExpandedSize(@Nullable Size size) {
|
||||
mTvExpandedSize = size;
|
||||
}
|
||||
|
||||
/** Get the PIP tv expanded bounds. */
|
||||
/** Get the expanded size of the PiP. */
|
||||
@Nullable
|
||||
public Size getTvExpandedSize() {
|
||||
return mTvExpandedSize;
|
||||
}
|
||||
|
||||
/** Set the PIP aspect ratio for the expanded PIP (TV) that is desired by the app. */
|
||||
public void setTvExpandedAspectRatio(float aspectRatio, boolean override) {
|
||||
public void setDesiredTvExpandedAspectRatio(float aspectRatio, boolean override) {
|
||||
if (override || mTvFixedPipOrientation == ORIENTATION_UNDETERMINED || aspectRatio == 0) {
|
||||
mTvExpandedAspectRatio = aspectRatio;
|
||||
mDesiredTvExpandedAspectRatio = aspectRatio;
|
||||
resetTvPipState();
|
||||
return;
|
||||
}
|
||||
if ((aspectRatio > 1 && mTvFixedPipOrientation == ORIENTATION_HORIZONTAL)
|
||||
|| (aspectRatio <= 1 && mTvFixedPipOrientation == ORIENTATION_VERTICAL)) {
|
||||
mTvExpandedAspectRatio = aspectRatio;
|
||||
mDesiredTvExpandedAspectRatio = aspectRatio;
|
||||
}
|
||||
}
|
||||
|
||||
/** Get the PIP aspect ratio for the expanded PIP (TV) that is desired by the app. */
|
||||
public float getTvExpandedAspectRatio() {
|
||||
return mTvExpandedAspectRatio;
|
||||
public float getDesiredTvExpandedAspectRatio() {
|
||||
return mDesiredTvExpandedAspectRatio;
|
||||
}
|
||||
|
||||
/** Sets the orientation the expanded TV PiP activity has been fixed to. */
|
||||
@@ -154,13 +154,9 @@ public class TvPipBoundsState extends PipBoundsState {
|
||||
return mTvPipManuallyCollapsed;
|
||||
}
|
||||
|
||||
/** Sets whether expanded PiP is supported by the device. */
|
||||
public void setIsTvExpandedPipEnabled(boolean enabled) {
|
||||
mIsTvExpandedPipEnabled = enabled;
|
||||
/** Returns whether expanded PiP is supported by the device. */
|
||||
public boolean isTvExpandedPipSupported() {
|
||||
return mIsTvExpandedPipSupported;
|
||||
}
|
||||
|
||||
/** Returns whether expanded PiP is supported by the device. */
|
||||
public boolean isTvExpandedPipEnabled() {
|
||||
return mIsTvExpandedPipEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,9 +30,9 @@ import android.content.pm.ParceledListSlice;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Handler;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
import android.view.DisplayInfo;
|
||||
import android.view.Gravity;
|
||||
|
||||
import com.android.wm.shell.R;
|
||||
@@ -45,9 +45,11 @@ import com.android.wm.shell.common.TaskStackListenerImpl;
|
||||
import com.android.wm.shell.pip.PinnedStackListenerForwarder;
|
||||
import com.android.wm.shell.pip.Pip;
|
||||
import com.android.wm.shell.pip.PipAnimationController;
|
||||
import com.android.wm.shell.pip.PipBoundsState;
|
||||
import com.android.wm.shell.pip.PipMediaController;
|
||||
import com.android.wm.shell.pip.PipTaskOrganizer;
|
||||
import com.android.wm.shell.pip.PipTransitionController;
|
||||
import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
@@ -62,6 +64,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
private static final String TAG = "TvPipController";
|
||||
static final boolean DEBUG = false;
|
||||
|
||||
private static final double EPS = 1e-7;
|
||||
private static final int NONEXISTENT_TASK_ID = -1;
|
||||
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@@ -97,11 +100,13 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
private final TvPipNotificationController mPipNotificationController;
|
||||
private final TvPipMenuController mTvPipMenuController;
|
||||
private final ShellExecutor mMainExecutor;
|
||||
private final Handler mMainHandler;
|
||||
private final TvPipImpl mImpl = new TvPipImpl();
|
||||
|
||||
private @State int mState = STATE_NO_PIP;
|
||||
private int mPreviousGravity = TvPipBoundsState.DEFAULT_TV_GRAVITY;
|
||||
private int mPinnedTaskId = NONEXISTENT_TASK_ID;
|
||||
private Runnable mUnstashRunnable;
|
||||
|
||||
private int mResizeAnimationDuration;
|
||||
|
||||
@@ -117,7 +122,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
TaskStackListenerImpl taskStackListener,
|
||||
DisplayController displayController,
|
||||
WindowManagerShellWrapper wmShell,
|
||||
ShellExecutor mainExecutor) {
|
||||
ShellExecutor mainExecutor,
|
||||
Handler mainHandler) {
|
||||
return new TvPipController(
|
||||
context,
|
||||
tvPipBoundsState,
|
||||
@@ -130,7 +136,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
taskStackListener,
|
||||
displayController,
|
||||
wmShell,
|
||||
mainExecutor).mImpl;
|
||||
mainExecutor,
|
||||
mainHandler).mImpl;
|
||||
}
|
||||
|
||||
private TvPipController(
|
||||
@@ -145,9 +152,11 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
TaskStackListenerImpl taskStackListener,
|
||||
DisplayController displayController,
|
||||
WindowManagerShellWrapper wmShell,
|
||||
ShellExecutor mainExecutor) {
|
||||
ShellExecutor mainExecutor,
|
||||
Handler mainHandler) {
|
||||
mContext = context;
|
||||
mMainExecutor = mainExecutor;
|
||||
mMainHandler = mainHandler;
|
||||
|
||||
mTvPipBoundsState = tvPipBoundsState;
|
||||
mTvPipBoundsState.setDisplayId(context.getDisplayId());
|
||||
@@ -182,6 +191,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
|
||||
loadConfigurations();
|
||||
mPipNotificationController.onConfigurationChanged(mContext);
|
||||
mTvPipBoundsAlgorithm.onConfigurationChanged(mContext);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -206,13 +216,20 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
}
|
||||
|
||||
setState(STATE_PIP_MENU);
|
||||
movePinnedStack();
|
||||
updatePinnedStackBounds();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void closeMenu() {
|
||||
if (DEBUG) Log.d(TAG, "closeMenu(), state before=" + stateToName(mState));
|
||||
setState(STATE_PIP);
|
||||
mTvPipBoundsAlgorithm.keepUnstashedForCurrentKeepClearAreas();
|
||||
updatePinnedStackBounds();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInMoveModeChanged() {
|
||||
updatePinnedStackBounds();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -231,21 +248,21 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
if (DEBUG) Log.d(TAG, "togglePipExpansion()");
|
||||
boolean expanding = !mTvPipBoundsState.isTvPipExpanded();
|
||||
int saveGravity = mTvPipBoundsAlgorithm
|
||||
.updatePositionOnExpandToggled(mPreviousGravity, expanding);
|
||||
.updateGravityOnExpandToggled(mPreviousGravity, expanding);
|
||||
if (saveGravity != Gravity.NO_GRAVITY) {
|
||||
mPreviousGravity = saveGravity;
|
||||
}
|
||||
mTvPipBoundsState.setTvPipManuallyCollapsed(!expanding);
|
||||
mTvPipBoundsState.setTvPipExpanded(expanding);
|
||||
movePinnedStack();
|
||||
updatePinnedStackBounds();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void movePip(int keycode) {
|
||||
if (mTvPipBoundsAlgorithm.updatePosition(keycode)) {
|
||||
if (mTvPipBoundsAlgorithm.updateGravity(keycode)) {
|
||||
mTvPipMenuController.updateGravity(mTvPipBoundsState.getTvPipGravity());
|
||||
mPreviousGravity = Gravity.NO_GRAVITY;
|
||||
movePinnedStack();
|
||||
updatePinnedStackBounds();
|
||||
} else {
|
||||
if (DEBUG) Log.d(TAG, "Position hasn't changed");
|
||||
}
|
||||
@@ -265,20 +282,48 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
Set<Rect> unrestricted) {
|
||||
if (mTvPipBoundsState.getDisplayId() == displayId) {
|
||||
mTvPipBoundsState.setKeepClearAreas(restricted, unrestricted);
|
||||
movePinnedStack();
|
||||
updatePinnedStackBounds();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate to the updated position of the PiP based on the state and position of the PiP.
|
||||
* Update the PiP bounds based on the state of the PiP and keep clear areas.
|
||||
* Animates to the current PiP bounds, and schedules unstashing the PiP if necessary.
|
||||
*/
|
||||
private void movePinnedStack() {
|
||||
private void updatePinnedStackBounds() {
|
||||
if (mState == STATE_NO_PIP) {
|
||||
return;
|
||||
}
|
||||
|
||||
Rect bounds = mTvPipBoundsAlgorithm.getTvPipBounds(mTvPipBoundsState.isTvPipExpanded());
|
||||
if (DEBUG) Log.d(TAG, "movePinnedStack() - new pip bounds: " + bounds.toShortString());
|
||||
final boolean stayAtAnchorPosition = mTvPipMenuController.isInMoveMode();
|
||||
final boolean disallowStashing = mState == STATE_PIP_MENU || stayAtAnchorPosition;
|
||||
final Placement placement = mTvPipBoundsAlgorithm.getTvPipBounds();
|
||||
|
||||
int stashType =
|
||||
disallowStashing ? PipBoundsState.STASH_TYPE_NONE : placement.getStashType();
|
||||
mTvPipBoundsState.setStashed(stashType);
|
||||
|
||||
if (stayAtAnchorPosition) {
|
||||
movePinnedStackTo(placement.getAnchorBounds());
|
||||
} else if (disallowStashing) {
|
||||
movePinnedStackTo(placement.getUnstashedBounds());
|
||||
} else {
|
||||
movePinnedStackTo(placement.getBounds());
|
||||
}
|
||||
|
||||
if (mUnstashRunnable != null) {
|
||||
mMainHandler.removeCallbacks(mUnstashRunnable);
|
||||
mUnstashRunnable = null;
|
||||
}
|
||||
if (!disallowStashing && placement.getUnstashDestinationBounds() != null) {
|
||||
mUnstashRunnable = () -> movePinnedStackTo(placement.getUnstashDestinationBounds());
|
||||
mMainHandler.postAtTime(mUnstashRunnable, placement.getUnstashTime());
|
||||
}
|
||||
}
|
||||
|
||||
/** Animates the PiP to the given bounds. */
|
||||
private void movePinnedStackTo(Rect bounds) {
|
||||
if (DEBUG) Log.d(TAG, "movePinnedStackTo() - new pip bounds: " + bounds.toShortString());
|
||||
mPipTaskOrganizer.scheduleAnimateResizePip(bounds,
|
||||
mResizeAnimationDuration, rect -> {
|
||||
if (DEBUG) Log.d(TAG, "movePinnedStack() animation done");
|
||||
@@ -359,6 +404,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
if (DEBUG) Log.d(TAG, " > show menu");
|
||||
mTvPipMenuController.showMenu();
|
||||
}
|
||||
|
||||
updatePinnedStackBounds();
|
||||
}
|
||||
|
||||
private void loadConfigurations() {
|
||||
@@ -366,12 +413,6 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration);
|
||||
}
|
||||
|
||||
private DisplayInfo getDisplayInfo() {
|
||||
final DisplayInfo displayInfo = new DisplayInfo();
|
||||
mContext.getDisplay().getDisplayInfo(displayInfo);
|
||||
return displayInfo;
|
||||
}
|
||||
|
||||
private void registerTaskStackListenerCallback(TaskStackListenerImpl taskStackListener) {
|
||||
taskStackListener.addListener(new TaskStackListenerCallback() {
|
||||
@Override
|
||||
@@ -417,7 +458,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
mTvPipBoundsState.setImeVisibility(imeVisible, imeHeight);
|
||||
|
||||
if (mState != STATE_NO_PIP) {
|
||||
movePinnedStack();
|
||||
updatePinnedStackBounds();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,7 +470,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
mTvPipBoundsState.setAspectRatio(ratio);
|
||||
|
||||
if (!mTvPipBoundsState.isTvPipExpanded() && ratioChanged) {
|
||||
movePinnedStack();
|
||||
updatePinnedStackBounds();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -438,41 +479,45 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
if (DEBUG) Log.d(TAG, "onExpandedAspectRatioChanged: " + ratio);
|
||||
|
||||
// 0) No update to the ratio --> don't do anything
|
||||
if (mTvPipBoundsState.getTvExpandedAspectRatio() == ratio) {
|
||||
|
||||
if (Math.abs(mTvPipBoundsState.getDesiredTvExpandedAspectRatio() - ratio)
|
||||
< EPS) {
|
||||
return;
|
||||
}
|
||||
|
||||
mTvPipBoundsState.setTvExpandedAspectRatio(ratio, false);
|
||||
mTvPipBoundsState.setDesiredTvExpandedAspectRatio(ratio, false);
|
||||
|
||||
// 1) PiP is expanded and only aspect ratio changed, but wasn't disabled
|
||||
// --> update bounds, but don't toggle
|
||||
if (mTvPipBoundsState.isTvPipExpanded() && ratio != 0) {
|
||||
movePinnedStack();
|
||||
mTvPipBoundsAlgorithm.updateExpandedPipSize();
|
||||
updatePinnedStackBounds();
|
||||
}
|
||||
|
||||
// 2) PiP is expanded, but expanded PiP was disabled
|
||||
// --> collapse PiP
|
||||
if (mTvPipBoundsState.isTvPipExpanded() && ratio == 0) {
|
||||
int saveGravity = mTvPipBoundsAlgorithm
|
||||
.updatePositionOnExpandToggled(mPreviousGravity, false);
|
||||
.updateGravityOnExpandToggled(mPreviousGravity, false);
|
||||
if (saveGravity != Gravity.NO_GRAVITY) {
|
||||
mPreviousGravity = saveGravity;
|
||||
}
|
||||
mTvPipBoundsState.setTvPipExpanded(false);
|
||||
movePinnedStack();
|
||||
updatePinnedStackBounds();
|
||||
}
|
||||
|
||||
// 3) PiP not expanded and not manually collapsed and expand was enabled
|
||||
// --> expand to new ratio
|
||||
if (!mTvPipBoundsState.isTvPipExpanded() && ratio != 0
|
||||
&& !mTvPipBoundsState.isTvPipManuallyCollapsed()) {
|
||||
mTvPipBoundsAlgorithm.updateExpandedPipSize();
|
||||
int saveGravity = mTvPipBoundsAlgorithm
|
||||
.updatePositionOnExpandToggled(mPreviousGravity, true);
|
||||
.updateGravityOnExpandToggled(mPreviousGravity, true);
|
||||
if (saveGravity != Gravity.NO_GRAVITY) {
|
||||
mPreviousGravity = saveGravity;
|
||||
}
|
||||
mTvPipBoundsState.setTvPipExpanded(true);
|
||||
movePinnedStack();
|
||||
updatePinnedStackBounds();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,741 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.wm.shell.pip.tv
|
||||
|
||||
import android.graphics.Point
|
||||
import android.graphics.Rect
|
||||
import android.util.Size
|
||||
import android.view.Gravity
|
||||
import com.android.wm.shell.pip.PipBoundsState
|
||||
import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_BOTTOM
|
||||
import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_LEFT
|
||||
import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_NONE
|
||||
import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_RIGHT
|
||||
import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_TOP
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val DEFAULT_PIP_MARGINS = 48
|
||||
private const val DEFAULT_STASH_DURATION = 5000L
|
||||
private const val RELAX_DEPTH = 1
|
||||
private const val DEFAULT_MAX_RESTRICTED_DISTANCE_FRACTION = 0.15
|
||||
|
||||
/**
|
||||
* This class calculates an appropriate position for a Picture-In-Picture (PiP) window, taking
|
||||
* into account app defined keep clear areas.
|
||||
*
|
||||
* @param clock A function returning a current timestamp (in milliseconds)
|
||||
*/
|
||||
class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
|
||||
/**
|
||||
* Result of the positioning algorithm.
|
||||
*
|
||||
* @param bounds The bounds the PiP should be placed at
|
||||
* @param anchorBounds The bounds of the PiP anchor position
|
||||
* (where the PiP would be placed if there were no keep clear areas)
|
||||
* @param stashType Where the PiP has been stashed, if at all
|
||||
* @param unstashDestinationBounds If stashed, the PiP should move to this position after
|
||||
* [stashDuration] has passed.
|
||||
* @param unstashTime If stashed, the time at which the PiP should move
|
||||
* to [unstashDestinationBounds]
|
||||
*/
|
||||
data class Placement(
|
||||
val bounds: Rect,
|
||||
val anchorBounds: Rect,
|
||||
@PipBoundsState.StashType val stashType: Int = STASH_TYPE_NONE,
|
||||
val unstashDestinationBounds: Rect? = null,
|
||||
val unstashTime: Long = 0L
|
||||
) {
|
||||
/** Bounds to use if the PiP should not be stashed. */
|
||||
fun getUnstashedBounds() = unstashDestinationBounds ?: bounds
|
||||
}
|
||||
|
||||
/** The size of the screen */
|
||||
private var screenSize = Size(0, 0)
|
||||
|
||||
/** The bounds the PiP is allowed to move in */
|
||||
private var movementBounds = Rect()
|
||||
|
||||
/** Padding to add between a keep clear area that caused the PiP to move and the PiP */
|
||||
var pipAreaPadding = DEFAULT_PIP_MARGINS
|
||||
|
||||
/** The distance the PiP peeks into the screen when stashed */
|
||||
var stashOffset = DEFAULT_PIP_MARGINS
|
||||
|
||||
/**
|
||||
* How long (in milliseconds) the PiP should stay stashed for after the last time the
|
||||
* keep clear areas causing the PiP to stash have changed.
|
||||
*/
|
||||
var stashDuration = DEFAULT_STASH_DURATION
|
||||
|
||||
/** The fraction of screen width/height restricted keep clear areas can move the PiP */
|
||||
var maxRestrictedDistanceFraction = DEFAULT_MAX_RESTRICTED_DISTANCE_FRACTION
|
||||
|
||||
private var pipGravity = Gravity.BOTTOM or Gravity.RIGHT
|
||||
private var transformedScreenBounds = Rect()
|
||||
private var transformedMovementBounds = Rect()
|
||||
|
||||
private var lastAreasOverlappingUnstashPosition: Set<Rect> = emptySet()
|
||||
private var lastStashTime: Long = Long.MIN_VALUE
|
||||
|
||||
/**
|
||||
* Calculates the position the PiP should be placed at, taking into consideration the
|
||||
* given keep clear areas.
|
||||
*
|
||||
* Restricted keep clear areas can move the PiP only by a limited amount, and may be ignored
|
||||
* if there is no space for the PiP to move to.
|
||||
* Apps holding the permission [android.Manifest.permission.USE_UNRESTRICTED_KEEP_CLEAR_AREAS]
|
||||
* can declare unrestricted keep clear areas, which can move the PiP farther and placement will
|
||||
* always try to respect these areas.
|
||||
*
|
||||
* If no free space the PiP is allowed to move to can be found, a stashed position is returned
|
||||
* as [Placement.bounds], along with a position to move to once [Placement.unstashTime] has
|
||||
* passed as [Placement.unstashDestinationBounds].
|
||||
*
|
||||
* @param pipSize The size of the PiP window
|
||||
* @param restrictedAreas The restricted keep clear areas
|
||||
* @param unrestrictedAreas The unrestricted keep clear areas
|
||||
*
|
||||
*/
|
||||
fun calculatePipPosition(
|
||||
pipSize: Size,
|
||||
restrictedAreas: Set<Rect>,
|
||||
unrestrictedAreas: Set<Rect>
|
||||
): Placement {
|
||||
val transformedRestrictedAreas = transformAndFilterAreas(restrictedAreas)
|
||||
val transformedUnrestrictedAreas = transformAndFilterAreas(unrestrictedAreas)
|
||||
val pipAnchorBounds = getNormalPipAnchorBounds(pipSize, transformedMovementBounds)
|
||||
|
||||
val result = calculatePipPositionTransformed(
|
||||
pipAnchorBounds,
|
||||
transformedRestrictedAreas,
|
||||
transformedUnrestrictedAreas
|
||||
)
|
||||
|
||||
val screenSpaceBounds = fromTransformedSpace(result.bounds)
|
||||
return Placement(
|
||||
screenSpaceBounds,
|
||||
fromTransformedSpace(result.anchorBounds),
|
||||
getStashType(screenSpaceBounds, movementBounds),
|
||||
result.unstashDestinationBounds?.let { fromTransformedSpace(it) },
|
||||
result.unstashTime
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters out areas that encompass the entire movement bounds and returns them mapped to
|
||||
* the base case space.
|
||||
*
|
||||
* Areas encompassing the entire movement bounds can occur when a full-screen View gets focused,
|
||||
* but we don't want this to cause the PiP to get stashed.
|
||||
*/
|
||||
private fun transformAndFilterAreas(areas: Set<Rect>): Set<Rect> {
|
||||
return areas.mapNotNullTo(mutableSetOf()) {
|
||||
when {
|
||||
it.contains(movementBounds) -> null
|
||||
else -> toTransformedSpace(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the position the PiP should be placed at, taking into consideration the
|
||||
* given keep clear areas.
|
||||
* All parameters are transformed from screen space to the base case space, where the PiP
|
||||
* anchor is in the bottom right corner / on the right side.
|
||||
*
|
||||
* @see [calculatePipPosition]
|
||||
*/
|
||||
private fun calculatePipPositionTransformed(
|
||||
pipAnchorBounds: Rect,
|
||||
restrictedAreas: Set<Rect>,
|
||||
unrestrictedAreas: Set<Rect>
|
||||
): Placement {
|
||||
if (restrictedAreas.isEmpty() && unrestrictedAreas.isEmpty()) {
|
||||
return Placement(pipAnchorBounds, pipAnchorBounds)
|
||||
}
|
||||
|
||||
// First try to find a free position to move to
|
||||
val freeMovePos = findFreeMovePosition(pipAnchorBounds, restrictedAreas, unrestrictedAreas)
|
||||
if (freeMovePos != null) {
|
||||
lastAreasOverlappingUnstashPosition = emptySet()
|
||||
return Placement(freeMovePos, pipAnchorBounds)
|
||||
}
|
||||
|
||||
// If no free position is found, we have to stash the PiP.
|
||||
// Find the position the PiP should return to once it unstashes by doing a relaxed
|
||||
// search, or ignoring restricted areas, or returning to the anchor position
|
||||
val unstashBounds =
|
||||
findRelaxedMovePosition(pipAnchorBounds, restrictedAreas, unrestrictedAreas)
|
||||
?: findFreeMovePosition(pipAnchorBounds, emptySet(), unrestrictedAreas)
|
||||
?: pipAnchorBounds
|
||||
|
||||
val keepClearAreas = restrictedAreas + unrestrictedAreas
|
||||
val areasOverlappingUnstashPosition =
|
||||
keepClearAreas.filter { Rect.intersects(it, unstashBounds) }.toSet()
|
||||
val areasOverlappingUnstashPositionChanged =
|
||||
!lastAreasOverlappingUnstashPosition.containsAll(areasOverlappingUnstashPosition)
|
||||
lastAreasOverlappingUnstashPosition = areasOverlappingUnstashPosition
|
||||
|
||||
val now = clock()
|
||||
if (areasOverlappingUnstashPositionChanged) {
|
||||
lastStashTime = now
|
||||
}
|
||||
|
||||
// If overlapping areas haven't changed and the stash duration has passed, we can
|
||||
// place the PiP at the unstash position
|
||||
val unstashTime = lastStashTime + stashDuration
|
||||
if (now >= unstashTime) {
|
||||
return Placement(unstashBounds, pipAnchorBounds)
|
||||
}
|
||||
|
||||
// Otherwise, we'll stash it close to the unstash position
|
||||
val stashedBounds = getNearbyStashedPosition(unstashBounds, keepClearAreas)
|
||||
return Placement(
|
||||
stashedBounds,
|
||||
pipAnchorBounds,
|
||||
getStashType(stashedBounds, transformedMovementBounds),
|
||||
unstashBounds,
|
||||
unstashTime
|
||||
)
|
||||
}
|
||||
|
||||
@PipBoundsState.StashType
|
||||
private fun getStashType(stashedBounds: Rect, movementBounds: Rect): Int {
|
||||
return when {
|
||||
stashedBounds.left < movementBounds.left -> STASH_TYPE_LEFT
|
||||
stashedBounds.right > movementBounds.right -> STASH_TYPE_RIGHT
|
||||
stashedBounds.top < movementBounds.top -> STASH_TYPE_TOP
|
||||
stashedBounds.bottom > movementBounds.bottom -> STASH_TYPE_BOTTOM
|
||||
else -> STASH_TYPE_NONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun findRelaxedMovePosition(
|
||||
pipAnchorBounds: Rect,
|
||||
restrictedAreas: Set<Rect>,
|
||||
unrestrictedAreas: Set<Rect>
|
||||
): Rect? {
|
||||
if (RELAX_DEPTH <= 0) {
|
||||
// relaxed search disabled
|
||||
return null
|
||||
}
|
||||
|
||||
return findRelaxedMovePosition(
|
||||
RELAX_DEPTH,
|
||||
pipAnchorBounds,
|
||||
restrictedAreas.toMutableSet(),
|
||||
unrestrictedAreas
|
||||
)
|
||||
}
|
||||
|
||||
private fun findRelaxedMovePosition(
|
||||
depth: Int,
|
||||
pipAnchorBounds: Rect,
|
||||
restrictedAreas: MutableSet<Rect>,
|
||||
unrestrictedAreas: Set<Rect>
|
||||
): Rect? {
|
||||
if (depth == 0) {
|
||||
return findFreeMovePosition(pipAnchorBounds, restrictedAreas, unrestrictedAreas)
|
||||
}
|
||||
|
||||
val candidates = mutableListOf<Rect>()
|
||||
val areasToExclude = restrictedAreas.toList()
|
||||
for (area in areasToExclude) {
|
||||
restrictedAreas.remove(area)
|
||||
val candidate = findRelaxedMovePosition(
|
||||
depth - 1,
|
||||
pipAnchorBounds,
|
||||
restrictedAreas,
|
||||
unrestrictedAreas
|
||||
)
|
||||
restrictedAreas.add(area)
|
||||
|
||||
if (candidate != null) {
|
||||
candidates.add(candidate)
|
||||
}
|
||||
}
|
||||
return candidates.minByOrNull { candidateCost(it, pipAnchorBounds) }
|
||||
}
|
||||
|
||||
/** Cost function to evaluate candidate bounds */
|
||||
private fun candidateCost(candidateBounds: Rect, pipAnchorBounds: Rect): Int {
|
||||
// squared euclidean distance of corresponding rect corners
|
||||
val dx = candidateBounds.left - pipAnchorBounds.left
|
||||
val dy = candidateBounds.top - pipAnchorBounds.top
|
||||
return dx * dx + dy * dy
|
||||
}
|
||||
|
||||
private fun findFreeMovePosition(
|
||||
pipAnchorBounds: Rect,
|
||||
restrictedAreas: Set<Rect>,
|
||||
unrestrictedAreas: Set<Rect>
|
||||
): Rect? {
|
||||
val movementBounds = transformedMovementBounds
|
||||
val candidateEdgeRects = mutableListOf<Rect>()
|
||||
val minRestrictedLeft =
|
||||
pipAnchorBounds.right - screenSize.width * maxRestrictedDistanceFraction
|
||||
|
||||
candidateEdgeRects.add(
|
||||
movementBounds.offsetCopy(movementBounds.width() + pipAreaPadding, 0)
|
||||
)
|
||||
candidateEdgeRects.addAll(unrestrictedAreas)
|
||||
candidateEdgeRects.addAll(restrictedAreas.filter { it.left >= minRestrictedLeft })
|
||||
|
||||
// throw out edges that are too close to the left screen edge to fit the PiP
|
||||
val minLeft = movementBounds.left + pipAnchorBounds.width()
|
||||
candidateEdgeRects.retainAll { it.left - pipAreaPadding > minLeft }
|
||||
candidateEdgeRects.sortBy { -it.left }
|
||||
|
||||
val maxRestrictedDY = (screenSize.height * maxRestrictedDistanceFraction).roundToInt()
|
||||
|
||||
val candidateBounds = mutableListOf<Rect>()
|
||||
for (edgeRect in candidateEdgeRects) {
|
||||
val edge = edgeRect.left - pipAreaPadding
|
||||
val dx = (edge - pipAnchorBounds.width()) - pipAnchorBounds.left
|
||||
val candidatePipBounds = pipAnchorBounds.offsetCopy(dx, 0)
|
||||
val searchUp = true
|
||||
val searchDown = !isPipAnchoredToCorner()
|
||||
|
||||
if (searchUp) {
|
||||
val event = findMinMoveUp(candidatePipBounds, restrictedAreas, unrestrictedAreas)
|
||||
val padding = if (event.start) 0 else pipAreaPadding
|
||||
val dy = event.pos - pipAnchorBounds.bottom - padding
|
||||
val maxDY = if (event.unrestricted) movementBounds.height() else maxRestrictedDY
|
||||
val candidate = pipAnchorBounds.offsetCopy(dx, dy)
|
||||
val isOnScreen = candidate.top > movementBounds.top
|
||||
val hangingMidAir = !candidate.intersectsY(edgeRect)
|
||||
if (isOnScreen && abs(dy) <= maxDY && !hangingMidAir) {
|
||||
candidateBounds.add(candidate)
|
||||
}
|
||||
}
|
||||
|
||||
if (searchDown) {
|
||||
val event = findMinMoveDown(candidatePipBounds, restrictedAreas, unrestrictedAreas)
|
||||
val padding = if (event.start) 0 else pipAreaPadding
|
||||
val dy = event.pos - pipAnchorBounds.top + padding
|
||||
val maxDY = if (event.unrestricted) movementBounds.height() else maxRestrictedDY
|
||||
val candidate = pipAnchorBounds.offsetCopy(dx, dy)
|
||||
val isOnScreen = candidate.bottom < movementBounds.bottom
|
||||
val hangingMidAir = !candidate.intersectsY(edgeRect)
|
||||
if (isOnScreen && abs(dy) <= maxDY && !hangingMidAir) {
|
||||
candidateBounds.add(candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
candidateBounds.sortBy { candidateCost(it, pipAnchorBounds) }
|
||||
return candidateBounds.firstOrNull()
|
||||
}
|
||||
|
||||
private fun getNearbyStashedPosition(bounds: Rect, keepClearAreas: Set<Rect>): Rect {
|
||||
val screenBounds = transformedScreenBounds
|
||||
val stashCandidates = Array(2) { Rect(bounds) }
|
||||
val areasOverlappingPipX = keepClearAreas.filter { it.intersectsX(bounds) }
|
||||
val areasOverlappingPipY = keepClearAreas.filter { it.intersectsY(bounds) }
|
||||
|
||||
if (screenBounds.bottom - bounds.bottom <= bounds.top - screenBounds.top) {
|
||||
// bottom is closer than top, stash downwards
|
||||
val fullStashTop = screenBounds.bottom - stashOffset
|
||||
|
||||
val maxBottom = areasOverlappingPipX.maxByOrNull { it.bottom }!!.bottom
|
||||
val partialStashTop = maxBottom + pipAreaPadding
|
||||
|
||||
val downPosition = stashCandidates[0]
|
||||
downPosition.offsetTo(bounds.left, min(fullStashTop, partialStashTop))
|
||||
} else {
|
||||
// top is closer than bottom, stash upwards
|
||||
val fullStashY = screenBounds.top - bounds.height() + stashOffset
|
||||
|
||||
val minTop = areasOverlappingPipX.minByOrNull { it.top }!!.top
|
||||
val partialStashY = minTop - bounds.height() - pipAreaPadding
|
||||
|
||||
val upPosition = stashCandidates[0]
|
||||
upPosition.offsetTo(bounds.left, max(fullStashY, partialStashY))
|
||||
}
|
||||
|
||||
if (screenBounds.right - bounds.right <= bounds.left - screenBounds.left) {
|
||||
// right is closer than left, stash rightwards
|
||||
val fullStashLeft = screenBounds.right - stashOffset
|
||||
|
||||
val maxRight = areasOverlappingPipY.maxByOrNull { it.right }!!.right
|
||||
val partialStashLeft = maxRight + pipAreaPadding
|
||||
|
||||
val rightPosition = stashCandidates[1]
|
||||
rightPosition.offsetTo(min(fullStashLeft, partialStashLeft), bounds.top)
|
||||
} else {
|
||||
// left is closer than right, stash leftwards
|
||||
val fullStashLeft = screenBounds.left - bounds.width() + stashOffset
|
||||
|
||||
val minLeft = areasOverlappingPipY.minByOrNull { it.left }!!.left
|
||||
val partialStashLeft = minLeft - bounds.width() - pipAreaPadding
|
||||
|
||||
val rightPosition = stashCandidates[1]
|
||||
rightPosition.offsetTo(max(fullStashLeft, partialStashLeft), bounds.top)
|
||||
}
|
||||
|
||||
return stashCandidates.minByOrNull {
|
||||
val dx = abs(it.left - bounds.left)
|
||||
val dy = abs(it.top - bounds.top)
|
||||
dx * bounds.height() + dy * bounds.width()
|
||||
}!!
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents the PiP from being stashed for the current set of keep clear areas.
|
||||
* The PiP may stash again if keep clear areas change.
|
||||
*/
|
||||
fun keepUnstashedForCurrentKeepClearAreas() {
|
||||
lastStashTime = Long.MIN_VALUE
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the size of the screen.
|
||||
*
|
||||
* @param size The new size of the screen
|
||||
*/
|
||||
fun setScreenSize(size: Size) {
|
||||
if (screenSize == size) {
|
||||
return
|
||||
}
|
||||
|
||||
screenSize = size
|
||||
transformedScreenBounds =
|
||||
toTransformedSpace(Rect(0, 0, screenSize.width, screenSize.height))
|
||||
transformedMovementBounds = toTransformedSpace(transformedMovementBounds)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the bounds within which the PiP is allowed to move.
|
||||
*
|
||||
* @param bounds The new movement bounds
|
||||
*/
|
||||
fun setMovementBounds(bounds: Rect) {
|
||||
if (movementBounds == bounds) {
|
||||
return
|
||||
}
|
||||
|
||||
movementBounds.set(bounds)
|
||||
transformedMovementBounds = toTransformedSpace(movementBounds)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the corner/side of the PiP's home position.
|
||||
*/
|
||||
fun setGravity(gravity: Int) {
|
||||
if (pipGravity == gravity) return
|
||||
|
||||
pipGravity = gravity
|
||||
transformedScreenBounds =
|
||||
toTransformedSpace(Rect(0, 0, screenSize.width, screenSize.height))
|
||||
transformedMovementBounds = toTransformedSpace(movementBounds)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param open Whether this event marks the opening of an occupied segment
|
||||
* @param pos The coordinate of this event
|
||||
* @param unrestricted Whether this event was generated by an unrestricted keep clear area
|
||||
* @param start Marks the special start event. Earlier events are skipped when sweeping
|
||||
*/
|
||||
data class SweepLineEvent(
|
||||
val open: Boolean,
|
||||
val pos: Int,
|
||||
val unrestricted: Boolean,
|
||||
val start: Boolean = false
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns a [SweepLineEvent] representing the minimal move up from [pipBounds] that clears
|
||||
* the given keep clear areas.
|
||||
*/
|
||||
private fun findMinMoveUp(
|
||||
pipBounds: Rect,
|
||||
restrictedAreas: Set<Rect>,
|
||||
unrestrictedAreas: Set<Rect>
|
||||
): SweepLineEvent {
|
||||
val events = mutableListOf<SweepLineEvent>()
|
||||
val generateEvents: (Boolean) -> (Rect) -> Unit = { unrestricted ->
|
||||
{ area ->
|
||||
if (pipBounds.intersectsX(area)) {
|
||||
events.add(SweepLineEvent(true, area.bottom, unrestricted))
|
||||
events.add(SweepLineEvent(false, area.top, unrestricted))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
restrictedAreas.forEach(generateEvents(false))
|
||||
unrestrictedAreas.forEach(generateEvents(true))
|
||||
|
||||
return sweepLineFindEarliestGap(
|
||||
events,
|
||||
pipBounds.height() + pipAreaPadding,
|
||||
pipBounds.bottom,
|
||||
pipBounds.height()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [SweepLineEvent] representing the minimal move down from [pipBounds] that clears
|
||||
* the given keep clear areas.
|
||||
*/
|
||||
private fun findMinMoveDown(
|
||||
pipBounds: Rect,
|
||||
restrictedAreas: Set<Rect>,
|
||||
unrestrictedAreas: Set<Rect>
|
||||
): SweepLineEvent {
|
||||
val events = mutableListOf<SweepLineEvent>()
|
||||
val generateEvents: (Boolean) -> (Rect) -> Unit = { unrestricted ->
|
||||
{ area ->
|
||||
if (pipBounds.intersectsX(area)) {
|
||||
events.add(SweepLineEvent(true, -area.top, unrestricted))
|
||||
events.add(SweepLineEvent(false, -area.bottom, unrestricted))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
restrictedAreas.forEach(generateEvents(false))
|
||||
unrestrictedAreas.forEach(generateEvents(true))
|
||||
|
||||
val earliestEvent = sweepLineFindEarliestGap(
|
||||
events,
|
||||
pipBounds.height() + pipAreaPadding,
|
||||
-pipBounds.top,
|
||||
pipBounds.height()
|
||||
)
|
||||
|
||||
return earliestEvent.copy(pos = -earliestEvent.pos)
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a list of events representing the starts & ends of occupied segments, and
|
||||
* returns the earliest event whose position is unoccupied and has [gapSize] distance to the
|
||||
* next event.
|
||||
*
|
||||
* @param events List of [SweepLineEvent] representing occupied segments
|
||||
* @param gapSize Size of the gap to search for
|
||||
* @param startPos The position to start the search on.
|
||||
* Inserts a special event marked with [SweepLineEvent.start].
|
||||
* @param startGapSize Used instead of [gapSize] for the start event
|
||||
*/
|
||||
private fun sweepLineFindEarliestGap(
|
||||
events: MutableList<SweepLineEvent>,
|
||||
gapSize: Int,
|
||||
startPos: Int,
|
||||
startGapSize: Int
|
||||
): SweepLineEvent {
|
||||
events.add(
|
||||
SweepLineEvent(
|
||||
open = false,
|
||||
pos = startPos,
|
||||
unrestricted = true,
|
||||
start = true
|
||||
)
|
||||
)
|
||||
events.sortBy { -it.pos }
|
||||
|
||||
// sweep
|
||||
var openCount = 0
|
||||
var i = 0
|
||||
while (i < events.size) {
|
||||
val event = events[i]
|
||||
if (!event.start) {
|
||||
if (event.open) {
|
||||
openCount++
|
||||
} else {
|
||||
openCount--
|
||||
}
|
||||
}
|
||||
|
||||
if (openCount == 0) {
|
||||
// check if placement is possible
|
||||
val candidate = event.pos
|
||||
if (candidate > startPos) {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
val eventGapSize = if (event.start) startGapSize else gapSize
|
||||
val nextEvent = events.getOrNull(i + 1)
|
||||
if (nextEvent == null || nextEvent.pos < candidate - eventGapSize) {
|
||||
return event
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
return events.last()
|
||||
}
|
||||
|
||||
private fun shouldTransformFlipX(): Boolean {
|
||||
return when (pipGravity) {
|
||||
(Gravity.TOP), (Gravity.TOP or Gravity.CENTER_HORIZONTAL) -> true
|
||||
(Gravity.TOP or Gravity.LEFT) -> true
|
||||
(Gravity.LEFT), (Gravity.LEFT or Gravity.CENTER_VERTICAL) -> true
|
||||
(Gravity.BOTTOM or Gravity.LEFT) -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldTransformFlipY(): Boolean {
|
||||
return when (pipGravity) {
|
||||
(Gravity.TOP or Gravity.LEFT) -> true
|
||||
(Gravity.TOP or Gravity.RIGHT) -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldTransformRotate(): Boolean {
|
||||
val horizontalGravity = pipGravity and Gravity.HORIZONTAL_GRAVITY_MASK
|
||||
val leftOrRight = horizontalGravity == Gravity.LEFT || horizontalGravity == Gravity.RIGHT
|
||||
|
||||
if (leftOrRight) return false
|
||||
return when (pipGravity and Gravity.VERTICAL_GRAVITY_MASK) {
|
||||
(Gravity.TOP) -> true
|
||||
(Gravity.BOTTOM) -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the given rect from screen space into the base case space, where the PiP
|
||||
* anchor is positioned in the bottom right corner or on the right side (for expanded PiP).
|
||||
*
|
||||
* @see [fromTransformedSpace]
|
||||
*/
|
||||
private fun toTransformedSpace(r: Rect): Rect {
|
||||
var screenWidth = screenSize.width
|
||||
var screenHeight = screenSize.height
|
||||
|
||||
val tl = Point(r.left, r.top)
|
||||
val tr = Point(r.right, r.top)
|
||||
val br = Point(r.right, r.bottom)
|
||||
val bl = Point(r.left, r.bottom)
|
||||
val corners = arrayOf(tl, tr, br, bl)
|
||||
|
||||
// rotate first (CW)
|
||||
if (shouldTransformRotate()) {
|
||||
corners.forEach { p ->
|
||||
val px = p.x
|
||||
val py = p.y
|
||||
p.x = py
|
||||
p.y = -px
|
||||
p.y += screenWidth // shift back screen into positive quadrant
|
||||
}
|
||||
screenWidth = screenSize.height
|
||||
screenHeight = screenSize.width
|
||||
}
|
||||
|
||||
// flip second
|
||||
corners.forEach {
|
||||
if (shouldTransformFlipX()) it.x = screenWidth - it.x
|
||||
if (shouldTransformFlipY()) it.y = screenHeight - it.y
|
||||
}
|
||||
|
||||
val top = corners.minByOrNull { it.y }!!.y
|
||||
val right = corners.maxByOrNull { it.x }!!.x
|
||||
val bottom = corners.maxByOrNull { it.y }!!.y
|
||||
val left = corners.minByOrNull { it.x }!!.x
|
||||
|
||||
return Rect(left, top, right, bottom)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the given rect from the base case space, where the PiP anchor is positioned in
|
||||
* the bottom right corner or on the right side, back into screen space.
|
||||
*
|
||||
* @see [toTransformedSpace]
|
||||
*/
|
||||
private fun fromTransformedSpace(r: Rect): Rect {
|
||||
val rotate = shouldTransformRotate()
|
||||
val transformedScreenWidth = if (rotate) screenSize.height else screenSize.width
|
||||
val transformedScreenHeight = if (rotate) screenSize.width else screenSize.height
|
||||
|
||||
val tl = Point(r.left, r.top)
|
||||
val tr = Point(r.right, r.top)
|
||||
val br = Point(r.right, r.bottom)
|
||||
val bl = Point(r.left, r.bottom)
|
||||
val corners = arrayOf(tl, tr, br, bl)
|
||||
|
||||
// flip first
|
||||
corners.forEach {
|
||||
if (shouldTransformFlipX()) it.x = transformedScreenWidth - it.x
|
||||
if (shouldTransformFlipY()) it.y = transformedScreenHeight - it.y
|
||||
}
|
||||
|
||||
// rotate second (CCW)
|
||||
if (rotate) {
|
||||
corners.forEach { p ->
|
||||
p.y -= screenSize.width // undo shift back screen into positive quadrant
|
||||
val px = p.x
|
||||
val py = p.y
|
||||
p.x = -py
|
||||
p.y = px
|
||||
}
|
||||
}
|
||||
|
||||
val top = corners.minByOrNull { it.y }!!.y
|
||||
val right = corners.maxByOrNull { it.x }!!.x
|
||||
val bottom = corners.maxByOrNull { it.y }!!.y
|
||||
val left = corners.minByOrNull { it.x }!!.x
|
||||
|
||||
return Rect(left, top, right, bottom)
|
||||
}
|
||||
|
||||
/** PiP anchor bounds in base case for given gravity */
|
||||
private fun getNormalPipAnchorBounds(pipSize: Size, movementBounds: Rect): Rect {
|
||||
var size = pipSize
|
||||
val rotateCW = shouldTransformRotate()
|
||||
if (rotateCW) {
|
||||
size = Size(pipSize.height, pipSize.width)
|
||||
}
|
||||
|
||||
val pipBounds = Rect()
|
||||
if (isPipAnchoredToCorner()) {
|
||||
// bottom right
|
||||
Gravity.apply(
|
||||
Gravity.BOTTOM or Gravity.RIGHT,
|
||||
size.width,
|
||||
size.height,
|
||||
movementBounds,
|
||||
pipBounds
|
||||
)
|
||||
return pipBounds
|
||||
} else {
|
||||
// expanded, right side
|
||||
Gravity.apply(Gravity.RIGHT, size.width, size.height, movementBounds, pipBounds)
|
||||
return pipBounds
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPipAnchoredToCorner(): Boolean {
|
||||
val left = (pipGravity and Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.LEFT
|
||||
val right = (pipGravity and Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.RIGHT
|
||||
val top = (pipGravity and Gravity.VERTICAL_GRAVITY_MASK) == Gravity.TOP
|
||||
val bottom = (pipGravity and Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM
|
||||
|
||||
val horizontal = left || right
|
||||
val vertical = top || bottom
|
||||
|
||||
return horizontal && vertical
|
||||
}
|
||||
|
||||
private fun Rect.offsetCopy(dx: Int, dy: Int) = Rect(this).apply { offset(dx, dy) }
|
||||
private fun Rect.intersectsY(other: Rect) = bottom >= other.top && top <= other.bottom
|
||||
private fun Rect.intersectsX(other: Rect) = right >= other.left && left <= other.right
|
||||
}
|
||||
@@ -174,8 +174,8 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
|
||||
}
|
||||
|
||||
void updateExpansionState() {
|
||||
mPipMenuView.setExpandedModeEnabled(mTvPipBoundsState.isTvExpandedPipEnabled()
|
||||
&& mTvPipBoundsState.getTvExpandedAspectRatio() != 0);
|
||||
mPipMenuView.setExpandedModeEnabled(mTvPipBoundsState.isTvExpandedPipSupported()
|
||||
&& mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0);
|
||||
mPipMenuView.setIsExpanded(mTvPipBoundsState.isTvPipExpanded());
|
||||
}
|
||||
|
||||
@@ -202,12 +202,17 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
|
||||
}
|
||||
}
|
||||
|
||||
boolean isInMoveMode() {
|
||||
return mInMoveMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnterMoveMode() {
|
||||
if (DEBUG) Log.d(TAG, "onEnterMoveMode - " + mInMoveMode);
|
||||
mInMoveMode = true;
|
||||
mPipMenuView.showMenuButtons(false);
|
||||
mPipMenuView.showMovementHints(mDelegate.getPipGravity());
|
||||
mDelegate.onInMoveModeChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -217,6 +222,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
|
||||
mInMoveMode = false;
|
||||
mPipMenuView.showMenuButtons(true);
|
||||
mPipMenuView.hideMovementHints();
|
||||
mDelegate.onInMoveModeChanged();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -447,6 +453,8 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
|
||||
|
||||
void movePip(int keycode);
|
||||
|
||||
void onInMoveModeChanged();
|
||||
|
||||
int getPipGravity();
|
||||
|
||||
void togglePipExpansion();
|
||||
|
||||
@@ -0,0 +1,469 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.wm.shell.pip.tv
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.testing.AndroidTestingRunner
|
||||
import android.util.Size
|
||||
import android.view.Gravity
|
||||
import org.junit.runner.RunWith
|
||||
import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_NONE
|
||||
import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_BOTTOM
|
||||
import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_RIGHT
|
||||
import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import junit.framework.Assert.assertEquals
|
||||
import junit.framework.Assert.assertNull
|
||||
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
class TvPipKeepClearAlgorithmTest {
|
||||
private val DEFAULT_PIP_SIZE = Size(384, 216)
|
||||
private val EXPANDED_WIDE_PIP_SIZE = Size(384*2, 216)
|
||||
private val DASHBOARD_WIDTH = 484
|
||||
private val BOTTOM_SHEET_HEIGHT = 524
|
||||
private val STASH_OFFSET = 64
|
||||
private val PADDING = 16
|
||||
private val SCREEN_SIZE = Size(1920, 1080)
|
||||
private val SCREEN_EDGE_INSET = 50
|
||||
|
||||
private lateinit var pipSize: Size
|
||||
private lateinit var movementBounds: Rect
|
||||
private lateinit var algorithm: TvPipKeepClearAlgorithm
|
||||
private var currentTime = 0L
|
||||
private var restrictedAreas = mutableSetOf<Rect>()
|
||||
private var unrestrictedAreas = mutableSetOf<Rect>()
|
||||
private var gravity: Int = 0
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
movementBounds = Rect(0, 0, SCREEN_SIZE.width, SCREEN_SIZE.height)
|
||||
movementBounds.inset(SCREEN_EDGE_INSET, SCREEN_EDGE_INSET)
|
||||
|
||||
restrictedAreas.clear()
|
||||
unrestrictedAreas.clear()
|
||||
currentTime = 0L
|
||||
pipSize = DEFAULT_PIP_SIZE
|
||||
gravity = Gravity.BOTTOM or Gravity.RIGHT
|
||||
|
||||
algorithm = TvPipKeepClearAlgorithm({ currentTime })
|
||||
algorithm.setScreenSize(SCREEN_SIZE)
|
||||
algorithm.setMovementBounds(movementBounds)
|
||||
algorithm.pipAreaPadding = PADDING
|
||||
algorithm.stashOffset = STASH_OFFSET
|
||||
algorithm.stashDuration = 5000L
|
||||
algorithm.setGravity(gravity)
|
||||
algorithm.maxRestrictedDistanceFraction = 0.3
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAnchorPosition_BottomRight() {
|
||||
gravity = Gravity.BOTTOM or Gravity.RIGHT
|
||||
testAnchorPosition()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAnchorPosition_TopRight() {
|
||||
gravity = Gravity.TOP or Gravity.RIGHT
|
||||
testAnchorPosition()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAnchorPosition_TopLeft() {
|
||||
gravity = Gravity.TOP or Gravity.LEFT
|
||||
testAnchorPosition()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAnchorPosition_BottomLeft() {
|
||||
gravity = Gravity.BOTTOM or Gravity.LEFT
|
||||
testAnchorPosition()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAnchorPosition_Right() {
|
||||
gravity = Gravity.RIGHT
|
||||
testAnchorPosition()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAnchorPosition_Left() {
|
||||
gravity = Gravity.LEFT
|
||||
testAnchorPosition()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAnchorPosition_Top() {
|
||||
gravity = Gravity.TOP
|
||||
testAnchorPosition()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAnchorPosition_Bottom() {
|
||||
gravity = Gravity.BOTTOM
|
||||
testAnchorPosition()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAnchorPosition_TopCenterHorizontal() {
|
||||
gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
|
||||
testAnchorPosition()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAnchorPosition_BottomCenterHorizontal() {
|
||||
gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
|
||||
testAnchorPosition()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAnchorPosition_RightCenterVertical() {
|
||||
gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL
|
||||
testAnchorPosition()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAnchorPosition_LeftCenterVertical() {
|
||||
gravity = Gravity.LEFT or Gravity.CENTER_VERTICAL
|
||||
testAnchorPosition()
|
||||
}
|
||||
|
||||
fun testAnchorPosition() {
|
||||
val placement = getActualPlacement()
|
||||
|
||||
assertEquals(getExpectedAnchorBounds(), placement.bounds)
|
||||
assertNotStashed(placement)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_AnchorBottomRight_KeepClearNotObstructing_StayAtAnchor() {
|
||||
gravity = Gravity.BOTTOM or Gravity.RIGHT
|
||||
|
||||
val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.LEFT)
|
||||
unrestrictedAreas.add(sidebar)
|
||||
|
||||
val expectedBounds = getExpectedAnchorBounds()
|
||||
|
||||
val placement = getActualPlacement()
|
||||
assertEquals(expectedBounds, placement.bounds)
|
||||
assertNotStashed(placement)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_AnchorBottomRight_UnrestrictedRightSidebar_PushedLeft() {
|
||||
gravity = Gravity.BOTTOM or Gravity.RIGHT
|
||||
|
||||
val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.RIGHT)
|
||||
unrestrictedAreas.add(sidebar)
|
||||
|
||||
val expectedBounds = anchorBoundsOffsetBy(SCREEN_EDGE_INSET - sidebar.width() - PADDING, 0)
|
||||
|
||||
val placement = getActualPlacement()
|
||||
assertEquals(expectedBounds, placement.bounds)
|
||||
assertNotStashed(placement)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_AnchorTopRight_UnrestrictedRightSidebar_PushedLeft() {
|
||||
gravity = Gravity.TOP or Gravity.RIGHT
|
||||
|
||||
val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.RIGHT)
|
||||
unrestrictedAreas.add(sidebar)
|
||||
|
||||
val expectedBounds = anchorBoundsOffsetBy(SCREEN_EDGE_INSET - sidebar.width() - PADDING, 0)
|
||||
|
||||
val placement = getActualPlacement()
|
||||
assertEquals(expectedBounds, placement.bounds)
|
||||
assertNotStashed(placement)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_AnchorBottomLeft_UnrestrictedRightSidebar_StayAtAnchor() {
|
||||
gravity = Gravity.BOTTOM or Gravity.LEFT
|
||||
|
||||
val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.RIGHT)
|
||||
unrestrictedAreas.add(sidebar)
|
||||
|
||||
val expectedBounds = anchorBoundsOffsetBy(0, 0)
|
||||
|
||||
val placement = getActualPlacement()
|
||||
assertEquals(expectedBounds, placement.bounds)
|
||||
assertNotStashed(placement)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_AnchorBottom_UnrestrictedRightSidebar_StayAtAnchor() {
|
||||
gravity = Gravity.BOTTOM
|
||||
|
||||
val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.RIGHT)
|
||||
unrestrictedAreas.add(sidebar)
|
||||
|
||||
val expectedBounds = anchorBoundsOffsetBy(0, 0)
|
||||
|
||||
val placement = getActualPlacement()
|
||||
assertEquals(expectedBounds, placement.bounds)
|
||||
assertNotStashed(placement)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExpanded_AnchorBottom_UnrestrictedRightSidebar_StayAtAnchor() {
|
||||
pipSize = EXPANDED_WIDE_PIP_SIZE
|
||||
gravity = Gravity.BOTTOM
|
||||
|
||||
val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.RIGHT)
|
||||
unrestrictedAreas.add(sidebar)
|
||||
|
||||
val expectedBounds = anchorBoundsOffsetBy(0, 0)
|
||||
|
||||
val placement = getActualPlacement()
|
||||
assertEquals(expectedBounds, placement.bounds)
|
||||
assertNotStashed(placement)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_AnchorBottomRight_RestrictedSmallBottomBar_PushedUp() {
|
||||
gravity = Gravity.BOTTOM or Gravity.RIGHT
|
||||
|
||||
val bottomBar = makeBottomBar(96)
|
||||
restrictedAreas.add(bottomBar)
|
||||
|
||||
val expectedBounds = anchorBoundsOffsetBy(0,
|
||||
SCREEN_EDGE_INSET - bottomBar.height() - PADDING)
|
||||
|
||||
val placement = getActualPlacement()
|
||||
assertEquals(expectedBounds, placement.bounds)
|
||||
assertNotStashed(placement)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_AnchorBottomRight_RestrictedBottomSheet_StashDownAtAnchor() {
|
||||
gravity = Gravity.BOTTOM or Gravity.RIGHT
|
||||
|
||||
val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT)
|
||||
restrictedAreas.add(bottomBar)
|
||||
|
||||
val expectedBounds = getExpectedAnchorBounds()
|
||||
expectedBounds.offsetTo(expectedBounds.left, SCREEN_SIZE.height - STASH_OFFSET)
|
||||
|
||||
val placement = getActualPlacement()
|
||||
assertEquals(expectedBounds, placement.bounds)
|
||||
assertEquals(STASH_TYPE_BOTTOM, placement.stashType)
|
||||
assertEquals(getExpectedAnchorBounds(), placement.unstashDestinationBounds)
|
||||
assertEquals(algorithm.stashDuration, placement.unstashTime)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_AnchorBottomRight_UnrestrictedBottomSheet_PushUp() {
|
||||
gravity = Gravity.BOTTOM or Gravity.RIGHT
|
||||
|
||||
val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT)
|
||||
unrestrictedAreas.add(bottomBar)
|
||||
|
||||
val expectedBounds = anchorBoundsOffsetBy(0,
|
||||
SCREEN_EDGE_INSET - bottomBar.height() - PADDING)
|
||||
|
||||
val placement = getActualPlacement()
|
||||
assertEquals(expectedBounds, placement.bounds)
|
||||
assertNotStashed(placement)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_AnchorBottomRight_UnrestrictedBottomSheet_RestrictedSidebar_StashAboveBottomSheet() {
|
||||
gravity = Gravity.BOTTOM or Gravity.RIGHT
|
||||
|
||||
val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT)
|
||||
unrestrictedAreas.add(bottomBar)
|
||||
|
||||
val maxRestrictedHorizontalPush =
|
||||
(algorithm.maxRestrictedDistanceFraction * SCREEN_SIZE.width).toInt()
|
||||
val sideBar = makeSideBar(maxRestrictedHorizontalPush + 100, Gravity.RIGHT)
|
||||
restrictedAreas.add(sideBar)
|
||||
|
||||
val expectedUnstashBounds =
|
||||
anchorBoundsOffsetBy(0, SCREEN_EDGE_INSET - bottomBar.height() - PADDING)
|
||||
|
||||
val expectedBounds = Rect(expectedUnstashBounds)
|
||||
expectedBounds.offsetTo(SCREEN_SIZE.width - STASH_OFFSET, expectedBounds.top)
|
||||
|
||||
val placement = getActualPlacement()
|
||||
assertEquals(expectedBounds, placement.bounds)
|
||||
assertEquals(STASH_TYPE_RIGHT, placement.stashType)
|
||||
assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds)
|
||||
assertEquals(algorithm.stashDuration, placement.unstashTime)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_AnchorBottomRight_UnrestrictedBottomSheet_UnrestrictedSidebar_PushUpLeft() {
|
||||
gravity = Gravity.BOTTOM or Gravity.RIGHT
|
||||
|
||||
val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT)
|
||||
unrestrictedAreas.add(bottomBar)
|
||||
|
||||
val maxRestrictedHorizontalPush =
|
||||
(algorithm.maxRestrictedDistanceFraction * SCREEN_SIZE.width).toInt()
|
||||
val sideBar = makeSideBar(maxRestrictedHorizontalPush + 100, Gravity.RIGHT)
|
||||
unrestrictedAreas.add(sideBar)
|
||||
|
||||
val expectedBounds = anchorBoundsOffsetBy(
|
||||
SCREEN_EDGE_INSET - sideBar.width() - PADDING,
|
||||
SCREEN_EDGE_INSET - bottomBar.height() - PADDING
|
||||
)
|
||||
|
||||
val placement = getActualPlacement()
|
||||
assertEquals(expectedBounds, placement.bounds)
|
||||
assertNotStashed(placement)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_Stashed_UnstashBoundsBecomeUnobstructed_Unstashes() {
|
||||
gravity = Gravity.BOTTOM or Gravity.RIGHT
|
||||
|
||||
val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT)
|
||||
unrestrictedAreas.add(bottomBar)
|
||||
|
||||
val maxRestrictedHorizontalPush =
|
||||
(algorithm.maxRestrictedDistanceFraction * SCREEN_SIZE.width).toInt()
|
||||
val sideBar = makeSideBar(maxRestrictedHorizontalPush + 100, Gravity.RIGHT)
|
||||
restrictedAreas.add(sideBar)
|
||||
|
||||
val expectedUnstashBounds =
|
||||
anchorBoundsOffsetBy(0, SCREEN_EDGE_INSET - bottomBar.height() - PADDING)
|
||||
|
||||
val expectedBounds = Rect(expectedUnstashBounds)
|
||||
expectedBounds.offsetTo(SCREEN_SIZE.width - STASH_OFFSET, expectedBounds.top)
|
||||
|
||||
var placement = getActualPlacement()
|
||||
assertEquals(expectedBounds, placement.bounds)
|
||||
assertEquals(STASH_TYPE_RIGHT, placement.stashType)
|
||||
assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds)
|
||||
assertEquals(algorithm.stashDuration, placement.unstashTime)
|
||||
|
||||
currentTime += 1000
|
||||
|
||||
restrictedAreas.remove(sideBar)
|
||||
placement = getActualPlacement()
|
||||
assertEquals(expectedUnstashBounds, placement.bounds)
|
||||
assertNotStashed(placement)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_Stashed_UnstashBoundsStaysObstructed_UnstashesAfterTimeout() {
|
||||
gravity = Gravity.BOTTOM or Gravity.RIGHT
|
||||
|
||||
val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT)
|
||||
unrestrictedAreas.add(bottomBar)
|
||||
|
||||
val maxRestrictedHorizontalPush =
|
||||
(algorithm.maxRestrictedDistanceFraction * SCREEN_SIZE.width).toInt()
|
||||
val sideBar = makeSideBar(maxRestrictedHorizontalPush + 100, Gravity.RIGHT)
|
||||
restrictedAreas.add(sideBar)
|
||||
|
||||
val expectedUnstashBounds =
|
||||
anchorBoundsOffsetBy(0, SCREEN_EDGE_INSET - bottomBar.height() - PADDING)
|
||||
|
||||
val expectedBounds = Rect(expectedUnstashBounds)
|
||||
expectedBounds.offsetTo(SCREEN_SIZE.width - STASH_OFFSET, expectedBounds.top)
|
||||
|
||||
var placement = getActualPlacement()
|
||||
assertEquals(expectedBounds, placement.bounds)
|
||||
assertEquals(STASH_TYPE_RIGHT, placement.stashType)
|
||||
assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds)
|
||||
assertEquals(algorithm.stashDuration, placement.unstashTime)
|
||||
|
||||
currentTime += algorithm.stashDuration
|
||||
|
||||
placement = getActualPlacement()
|
||||
assertEquals(expectedUnstashBounds, placement.bounds)
|
||||
assertNotStashed(placement)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_Stashed_UnstashBoundsObstructionChanges_UnstashTimeExtended() {
|
||||
gravity = Gravity.BOTTOM or Gravity.RIGHT
|
||||
|
||||
val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT)
|
||||
unrestrictedAreas.add(bottomBar)
|
||||
|
||||
val maxRestrictedHorizontalPush =
|
||||
(algorithm.maxRestrictedDistanceFraction * SCREEN_SIZE.width).toInt()
|
||||
val sideBar = makeSideBar(maxRestrictedHorizontalPush + 100, Gravity.RIGHT)
|
||||
restrictedAreas.add(sideBar)
|
||||
|
||||
val expectedUnstashBounds =
|
||||
anchorBoundsOffsetBy(0, SCREEN_EDGE_INSET - bottomBar.height() - PADDING)
|
||||
|
||||
val expectedBounds = Rect(expectedUnstashBounds)
|
||||
expectedBounds.offsetTo(SCREEN_SIZE.width - STASH_OFFSET, expectedBounds.top)
|
||||
|
||||
var placement = getActualPlacement()
|
||||
assertEquals(expectedBounds, placement.bounds)
|
||||
assertEquals(STASH_TYPE_RIGHT, placement.stashType)
|
||||
assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds)
|
||||
assertEquals(algorithm.stashDuration, placement.unstashTime)
|
||||
|
||||
currentTime += 1000
|
||||
|
||||
val newObstruction = Rect(
|
||||
0,
|
||||
expectedUnstashBounds.top,
|
||||
expectedUnstashBounds.right,
|
||||
expectedUnstashBounds.bottom
|
||||
)
|
||||
restrictedAreas.add(newObstruction)
|
||||
|
||||
placement = getActualPlacement()
|
||||
assertEquals(expectedBounds, placement.bounds)
|
||||
assertEquals(STASH_TYPE_RIGHT, placement.stashType)
|
||||
assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds)
|
||||
assertEquals(currentTime + algorithm.stashDuration, placement.unstashTime)
|
||||
}
|
||||
|
||||
private fun makeSideBar(width: Int, @Gravity.GravityFlags side: Int): Rect {
|
||||
val sidebar = Rect(0, 0, width, SCREEN_SIZE.height)
|
||||
if (side == Gravity.RIGHT) {
|
||||
sidebar.offsetTo(SCREEN_SIZE.width - width, 0)
|
||||
}
|
||||
return sidebar
|
||||
}
|
||||
|
||||
private fun makeBottomBar(height: Int): Rect {
|
||||
return Rect(0, SCREEN_SIZE.height - height, SCREEN_SIZE.width, SCREEN_SIZE.height)
|
||||
}
|
||||
|
||||
private fun getExpectedAnchorBounds(): Rect {
|
||||
val expectedBounds = Rect()
|
||||
Gravity.apply(gravity, pipSize.width, pipSize.height, movementBounds, expectedBounds)
|
||||
return expectedBounds
|
||||
}
|
||||
|
||||
private fun anchorBoundsOffsetBy(dx: Int, dy: Int): Rect {
|
||||
val bounds = getExpectedAnchorBounds()
|
||||
bounds.offset(dx, dy)
|
||||
return bounds
|
||||
}
|
||||
|
||||
private fun getActualPlacement(): Placement {
|
||||
algorithm.setGravity(gravity)
|
||||
return algorithm.calculatePipPosition(pipSize, restrictedAreas, unrestrictedAreas)
|
||||
}
|
||||
|
||||
private fun assertNotStashed(actual: Placement) {
|
||||
assertEquals(STASH_TYPE_NONE, actual.stashType)
|
||||
assertNull(actual.unstashDestinationBounds)
|
||||
assertEquals(0L, actual.unstashTime)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user