package com.android.internal.policy;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.util.Size;
import android.view.Gravity;
import android.view.ViewConfiguration;
import android.widget.Scroller;
import java.io.PrintWriter;
import java.util.ArrayList;
public class PipSnapAlgorithm {
private static final int SNAP_MODE_CORNERS_ONLY = 0;
private static final int SNAP_MODE_CORNERS_AND_SIDES = 1;
private static final int SNAP_MODE_EDGE = 2;
private static final int SNAP_MODE_EDGE_MAGNET_CORNERS = 3;
private static final int SNAP_MODE_LONG_EDGE_MAGNET_CORNERS = 4;
private static final float CORNER_MAGNET_THRESHOLD = 0.3f;
private final Context mContext;
private final ArrayList<Integer> mSnapGravities = new ArrayList<>();
private final int mDefaultSnapMode = SNAP_MODE_EDGE_MAGNET_CORNERS;
private int mSnapMode = mDefaultSnapMode;
private final float mDefaultSizePercent;
private final float mMinAspectRatioForMinSize;
private final float mMaxAspectRatioForMinSize;
private final int mFlingDeceleration;
private int mOrientation = Configuration.ORIENTATION_UNDEFINED;
private final int mMinimizedVisibleSize;
private boolean mIsMinimized;
public PipSnapAlgorithm(Context context) {
Resources res = context.getResources();
mContext = context;
mMinimizedVisibleSize = res.getDimensionPixelSize(
com.android.internal.R.dimen.pip_minimized_visible_size);
mDefaultSizePercent = res.getFloat(
com.android.internal.R.dimen.config_pictureInPictureDefaultSizePercent);
mMaxAspectRatioForMinSize = res.getFloat(
com.android.internal.R.dimen.config_pictureInPictureAspectRatioLimitForMinSize);
mMinAspectRatioForMinSize = 1f / mMaxAspectRatioForMinSize;
mFlingDeceleration = mContext.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.pip_fling_deceleration);
onConfigurationChanged();
}
public void onConfigurationChanged() {
Resources res = mContext.getResources();
mOrientation = res.getConfiguration().orientation;
mSnapMode = res.getInteger(com.android.internal.R.integer.config_pictureInPictureSnapMode);
calculateSnapTargets();
}
public void setMinimized(boolean isMinimized) {
mIsMinimized = isMinimized;
}
public Rect findClosestSnapBounds(Rect movementBounds, Rect stackBounds, float velocityX,
float velocityY, Point dragStartPosition) {
final Rect intersectStackBounds = new Rect(stackBounds);
final Point intersect = getEdgeIntersect(stackBounds, movementBounds, velocityX, velocityY,
dragStartPosition);
intersectStackBounds.offsetTo(intersect.x, intersect.y);
return findClosestSnapBounds(movementBounds, intersectStackBounds);
}
public Point getEdgeIntersect(Rect stackBounds, Rect movementBounds, float velX, float velY,
Point dragStartPosition) {
final boolean isLandscape = mOrientation == Configuration.ORIENTATION_LANDSCAPE;
final int x = stackBounds.left;
final int y = stackBounds.top;
final float slope = velY / velX;
final float yIntercept = y - slope * x;
Point vertPoint = new Point();
Point horizPoint = new Point();
vertPoint.x = velX > 0 ? movementBounds.right : movementBounds.left;
vertPoint.y = findY(slope, yIntercept, vertPoint.x);
horizPoint.y = velY > 0 ? movementBounds.bottom : movementBounds.top;
horizPoint.x = findX(slope, yIntercept, horizPoint.y);
int maxDistance;
if (isLandscape) {
maxDistance = velX > 0
? movementBounds.right - stackBounds.left
: stackBounds.left - movementBounds.left;
} else {
maxDistance = velY > 0
? movementBounds.bottom - stackBounds.top
: stackBounds.top - movementBounds.top;
}
if (maxDistance > 0) {
final int startPoint = isLandscape ? dragStartPosition.y : dragStartPosition.x;
final int endPoint = isLandscape ? horizPoint.y : horizPoint.x;
final int center = movementBounds.centerX();
if ((startPoint < center && endPoint < center)
|| (startPoint > center && endPoint > center)) {
int distance = (int) (0 - Math.pow(isLandscape ? velX : velY, 2))
/ (2 * mFlingDeceleration);
distance = Math.min(distance, maxDistance);
if (isLandscape) {
horizPoint.x = stackBounds.left + (velX > 0 ? distance : -distance);
} else {
horizPoint.y = stackBounds.top + (velY > 0 ? distance : -distance);
}
return horizPoint;
}
}
final double distanceVert = Math.hypot(vertPoint.x - x, vertPoint.y - y);
final double distanceHoriz = Math.hypot(horizPoint.x - x, horizPoint.y - y);
if (distanceVert == 0) {
return horizPoint;
}
if (distanceHoriz == 0) {
return vertPoint;
}
return Math.abs(distanceVert) > Math.abs(distanceHoriz) ? horizPoint : vertPoint;
}
private int findY(float slope, float yIntercept, float x) {
return (int) ((slope * x) + yIntercept);
}
private int findX(float slope, float yIntercept, float y) {
return (int) ((y - yIntercept) / slope);
}
public Rect findClosestSnapBounds(Rect movementBounds, Rect stackBounds) {
final Rect pipBounds = new Rect(movementBounds.left, movementBounds.top,
movementBounds.right + stackBounds.width(),
movementBounds.bottom + stackBounds.height());
final Rect newBounds = new Rect(stackBounds);
if (mSnapMode == SNAP_MODE_LONG_EDGE_MAGNET_CORNERS
|| mSnapMode == SNAP_MODE_EDGE_MAGNET_CORNERS) {
final Rect tmpBounds = new Rect();
final Point[] snapTargets = new Point[mSnapGravities.size()];
for (int i = 0; i < mSnapGravities.size(); i++) {
Gravity.apply(mSnapGravities.get(i), stackBounds.width(), stackBounds.height(),
pipBounds, 0, 0, tmpBounds);
snapTargets[i] = new Point(tmpBounds.left, tmpBounds.top);
}
Point snapTarget = findClosestPoint(stackBounds.left, stackBounds.top, snapTargets);
float distance = distanceToPoint(snapTarget, stackBounds.left, stackBounds.top);
final float thresh = Math.max(stackBounds.width(), stackBounds.height())
* CORNER_MAGNET_THRESHOLD;
if (distance < thresh) {
newBounds.offsetTo(snapTarget.x, snapTarget.y);
} else {
snapRectToClosestEdge(stackBounds, movementBounds, newBounds);
}
} else if (mSnapMode == SNAP_MODE_EDGE) {
snapRectToClosestEdge(stackBounds, movementBounds, newBounds);
} else {
final Rect tmpBounds = new Rect();
final Point[] snapTargets = new Point[mSnapGravities.size()];
for (int i = 0; i < mSnapGravities.size(); i++) {
Gravity.apply(mSnapGravities.get(i), stackBounds.width(), stackBounds.height(),
pipBounds, 0, 0, tmpBounds);
snapTargets[i] = new Point(tmpBounds.left, tmpBounds.top);
}
Point snapTarget = findClosestPoint(stackBounds.left, stackBounds.top, snapTargets);
newBounds.offsetTo(snapTarget.x, snapTarget.y);
}
return newBounds;
}
public void applyMinimizedOffset(Rect stackBounds, Rect movementBounds, Point displaySize,
Rect stableInsets) {
if (stackBounds.left <= movementBounds.centerX()) {
stackBounds.offsetTo(stableInsets.left + mMinimizedVisibleSize - stackBounds.width(),
stackBounds.top);
} else {
stackBounds.offsetTo(displaySize.x - stableInsets.right - mMinimizedVisibleSize,
stackBounds.top);
}
}
public float getSnapFraction(Rect stackBounds, Rect movementBounds) {
final Rect tmpBounds = new Rect();
snapRectToClosestEdge(stackBounds, movementBounds, tmpBounds);
final float widthFraction = (float) (tmpBounds.left - movementBounds.left) /
movementBounds.width();
final float heightFraction = (float) (tmpBounds.top - movementBounds.top) /
movementBounds.height();
if (tmpBounds.top == movementBounds.top) {
return widthFraction;
} else if (tmpBounds.left == movementBounds.right) {
return 1f + heightFraction;
} else if (tmpBounds.top == movementBounds.bottom) {
return 2f + (1f - widthFraction);
} else {
return 3f + (1f - heightFraction);
}
}
public void applySnapFraction(Rect stackBounds, Rect movementBounds, float snapFraction) {
if (snapFraction < 1f) {
int offset = movementBounds.left + (int) (snapFraction * movementBounds.width());
stackBounds.offsetTo(offset, movementBounds.top);
} else if (snapFraction < 2f) {
snapFraction -= 1f;
int offset = movementBounds.top + (int) (snapFraction * movementBounds.height());
stackBounds.offsetTo(movementBounds.right, offset);
} else if (snapFraction < 3f) {
snapFraction -= 2f;
int offset = movementBounds.left + (int) ((1f - snapFraction) * movementBounds.width());
stackBounds.offsetTo(offset, movementBounds.bottom);
} else {
snapFraction -= 3f;
int offset = movementBounds.top + (int) ((1f - snapFraction) * movementBounds.height());
stackBounds.offsetTo(movementBounds.left, offset);
}
}
public void getMovementBounds(Rect stackBounds, Rect insetBounds, Rect movementBoundsOut,
int bottomOffset) {
movementBoundsOut.set(insetBounds);
movementBoundsOut.right = Math.max(insetBounds.left, insetBounds.right -
stackBounds.width());
movementBoundsOut.bottom = Math.max(insetBounds.top, insetBounds.bottom -
stackBounds.height());
movementBoundsOut.bottom -= bottomOffset;
}
public Size getSizeForAspectRatio(float aspectRatio, float minEdgeSize, int displayWidth,
int displayHeight) {
final int smallestDisplaySize = Math.min(displayWidth, displayHeight);
final int minSize = (int) Math.max(minEdgeSize, smallestDisplaySize * mDefaultSizePercent);
final int width;
final int height;
if (aspectRatio <= mMinAspectRatioForMinSize || aspectRatio > mMaxAspectRatioForMinSize) {
if (aspectRatio <= 1) {
width = minSize;
height = Math.round(width / aspectRatio);
} else {
height = minSize;
width = Math.round(height * aspectRatio);
}
} else {
final float widthAtMaxAspectRatioForMinSize = mMaxAspectRatioForMinSize * minSize;
final float radius = PointF.length(widthAtMaxAspectRatioForMinSize, minSize);
height = (int) Math.round(Math.sqrt((radius * radius) /
(aspectRatio * aspectRatio + 1)));
width = Math.round(height * aspectRatio);
}
return new Size(width, height);
}
private Point findClosestPoint(int x, int y, Point[] points) {
Point closestPoint = null;
float minDistance = Float.MAX_VALUE;
for (Point p : points) {
float distance = distanceToPoint(p, x, y);
if (distance < minDistance) {
closestPoint = p;
minDistance = distance;
}
}
return closestPoint;
}
private void snapRectToClosestEdge(Rect stackBounds, Rect movementBounds, Rect boundsOut) {
final int boundedLeft = Math.max(movementBounds.left, Math.min(movementBounds.right,
stackBounds.left));
final int boundedTop = Math.max(movementBounds.top, Math.min(movementBounds.bottom,
stackBounds.top));
boundsOut.set(stackBounds);
if (mIsMinimized) {
boundsOut.offsetTo(boundedLeft, boundedTop);
return;
}
final int fromLeft = Math.abs(stackBounds.left - movementBounds.left);
final int fromTop = Math.abs(stackBounds.top - movementBounds.top);
final int fromRight = Math.abs(movementBounds.right - stackBounds.left);
final int fromBottom = Math.abs(movementBounds.bottom - stackBounds.top);
int shortest;
if (mSnapMode == SNAP_MODE_LONG_EDGE_MAGNET_CORNERS) {
shortest = (mOrientation == Configuration.ORIENTATION_LANDSCAPE)
? Math.min(fromTop, fromBottom)
: Math.min(fromLeft, fromRight);
} else {
shortest = Math.min(Math.min(fromLeft, fromRight), Math.min(fromTop, fromBottom));
}
if (shortest == fromLeft) {
boundsOut.offsetTo(movementBounds.left, boundedTop);
} else if (shortest == fromTop) {
boundsOut.offsetTo(boundedLeft, movementBounds.top);
} else if (shortest == fromRight) {
boundsOut.offsetTo(movementBounds.right, boundedTop);
} else {
boundsOut.offsetTo(boundedLeft, movementBounds.bottom);
}
}
private float distanceToPoint(Point p, int x, int y) {
return PointF.length(p.x - x, p.y - y);
}
private void calculateSnapTargets() {
mSnapGravities.clear();
switch (mSnapMode) {
case SNAP_MODE_CORNERS_AND_SIDES:
if (mOrientation == Configuration.ORIENTATION_LANDSCAPE) {
mSnapGravities.add(Gravity.TOP | Gravity.CENTER_HORIZONTAL);
mSnapGravities.add(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
} else {
mSnapGravities.add(Gravity.CENTER_VERTICAL | Gravity.LEFT);
mSnapGravities.add(Gravity.CENTER_VERTICAL | Gravity.RIGHT);
}
case SNAP_MODE_CORNERS_ONLY:
case SNAP_MODE_EDGE_MAGNET_CORNERS:
case SNAP_MODE_LONG_EDGE_MAGNET_CORNERS:
mSnapGravities.add(Gravity.TOP | Gravity.LEFT);
mSnapGravities.add(Gravity.TOP | Gravity.RIGHT);
mSnapGravities.add(Gravity.BOTTOM | Gravity.LEFT);
mSnapGravities.add(Gravity.BOTTOM | Gravity.RIGHT);
break;
default:
break;
}
}
public void dump(PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
pw.println(prefix + PipSnapAlgorithm.class.getSimpleName());
pw.println(innerPrefix + "mSnapMode=" + mSnapMode);
pw.println(innerPrefix + "mOrientation=" + mOrientation);
pw.println(innerPrefix + "mMinimizedVisibleSize=" + mMinimizedVisibleSize);
pw.println(innerPrefix + "mIsMinimized=" + mIsMinimized);
}
}