Merge "Move TV PiP away from keep clear areas" into tm-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
3f574e253e
@@ -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>
|
||||
@@ -69,7 +69,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,
|
||||
@@ -83,7 +84,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);
|
||||
|
||||
@@ -55,11 +55,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