Merge "Fix some issues with pinch to resize" into sc-dev

This commit is contained in:
Winson Chung
2021-03-31 16:03:23 +00:00
committed by Android (Google) Code Review
4 changed files with 154 additions and 226 deletions

View File

@@ -94,11 +94,14 @@ public class PipSurfaceTransactionHelper {
public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash,
Rect sourceBounds, Rect destinationBounds, float degrees) {
mTmpSourceRectF.set(sourceBounds);
// We want the matrix to position the surface relative to the screen coordinates so offset
// the source to 0,0
mTmpSourceRectF.offsetTo(0, 0);
mTmpDestinationRectF.set(destinationBounds);
mTmpTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL);
mTmpTransform.postRotate(degrees);
tx.setMatrix(leash, mTmpTransform, mTmpFloat9)
.setPosition(leash, mTmpDestinationRectF.left, mTmpDestinationRectF.top);
mTmpTransform.postRotate(degrees,
mTmpDestinationRectF.centerX(), mTmpDestinationRectF.centerY());
tx.setMatrix(leash, mTmpTransform, mTmpFloat9);
return this;
}

View File

@@ -16,111 +16,115 @@
package com.android.wm.shell.pip.phone;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
/**
* Helper class to calculate the new size given two-fingers pinch to resize.
*/
public class PipPinchResizingAlgorithm {
private static final Rect TMP_RECT = new Rect();
private static final int PINCH_RESIZE_MAX_ANGLE_ROTATION = 45;
private static final float OVERROTATE_DAMP_FACTOR = 0.4f;
private static final float ANGLE_THRESHOLD = 5f;
private final PointF mTmpDownVector = new PointF();
private final PointF mTmpLastVector = new PointF();
private final PointF mTmpDownCentroid = new PointF();
private final PointF mTmpLastCentroid = new PointF();
/**
* Given inputs and requirements and current PiP bounds, return the new size.
*
* @param x0 x-coordinate of the primary input.
* @param y0 y-coordinate of the primary input.
* @param x1 x-coordinate of the secondary input.
* @param y1 y-coordinate of the secondary input.
* @param downx0 x-coordinate of the original down point of the primary input.
* @param downy0 y-coordinate of the original down ponit of the primary input.
* @param downx1 x-coordinate of the original down point of the secondary input.
* @param downy1 y-coordinate of the original down point of the secondary input.
* @param currentPipBounds current PiP bounds.
* @param minVisibleWidth minimum visible width.
* @param minVisibleHeight minimum visible height.
* @param maxSize max size.
* @return The new resized PiP bounds, sharing the same center.
* Updates {@param resizeBoundsOut} with the new bounds of the PIP, and returns the angle in
* degrees that the PIP should be rotated.
*/
public static Rect pinchResize(float x0, float y0, float x1, float y1,
float downx0, float downy0, float downx1, float downy1, Rect currentPipBounds,
int minVisibleWidth, int minVisibleHeight, Point maxSize) {
public float calculateBoundsAndAngle(PointF downPoint, PointF downSecondPoint,
PointF lastPoint, PointF lastSecondPoint, Point minSize, Point maxSize,
Rect initialBounds, Rect resizeBoundsOut) {
float downDist = (float) Math.hypot(downSecondPoint.x - downPoint.x,
downSecondPoint.y - downPoint.y);
float dist = (float) Math.hypot(lastSecondPoint.x - lastPoint.x,
lastSecondPoint.y - lastPoint.y);
float minScale = getMinScale(initialBounds, minSize);
float maxScale = getMaxScale(initialBounds, maxSize);
float scale = Math.max(minScale, Math.min(maxScale, dist / downDist));
int width = currentPipBounds.width();
int height = currentPipBounds.height();
int left = currentPipBounds.left;
int top = currentPipBounds.top;
int right = currentPipBounds.right;
int bottom = currentPipBounds.bottom;
final float aspect = (float) width / (float) height;
final int widthDelta = Math.round(Math.abs(x0 - x1) - Math.abs(downx0 - downx1));
final int heightDelta = Math.round(Math.abs(y0 - y1) - Math.abs(downy0 - downy1));
final int dx = (int) ((x0 - downx0 + x1 - downx1) / 2);
final int dy = (int) ((y0 - downy0 + y1 - downy1) / 2);
// Scale the bounds by the change in distance between the points
resizeBoundsOut.set(initialBounds);
scaleRectAboutCenter(resizeBoundsOut, scale);
width = Math.max(minVisibleWidth, Math.min(width + widthDelta, maxSize.x));
height = Math.max(minVisibleHeight, Math.min(height + heightDelta, maxSize.y));
// Translate by the centroid movement
getCentroid(downPoint, downSecondPoint, mTmpDownCentroid);
getCentroid(lastPoint, lastSecondPoint, mTmpLastCentroid);
resizeBoundsOut.offset((int) (mTmpLastCentroid.x - mTmpDownCentroid.x),
(int) (mTmpLastCentroid.y - mTmpDownCentroid.y));
// Calculate 2 rectangles fulfilling all requirements for either X or Y being the major
// drag axis. What ever is producing the bigger rectangle will be chosen.
int width1;
int width2;
int height1;
int height2;
if (aspect > 1.0f) {
// Assuming that the width is our target we calculate the height.
width1 = Math.max(minVisibleWidth, Math.min(maxSize.x, width));
height1 = Math.round((float) width1 / aspect);
if (height1 < minVisibleHeight) {
// If the resulting height is too small we adjust to the minimal size.
height1 = minVisibleHeight;
width1 = Math.max(minVisibleWidth,
Math.min(maxSize.x, Math.round((float) height1 * aspect)));
}
// Assuming that the height is our target we calculate the width.
height2 = Math.max(minVisibleHeight, Math.min(maxSize.y, height));
width2 = Math.round((float) height2 * aspect);
if (width2 < minVisibleWidth) {
// If the resulting width is too small we adjust to the minimal size.
width2 = minVisibleWidth;
height2 = Math.max(minVisibleHeight,
Math.min(maxSize.y, Math.round((float) width2 / aspect)));
}
} else {
// Assuming that the width is our target we calculate the height.
width1 = Math.max(minVisibleWidth, Math.min(maxSize.x, width));
height1 = Math.round((float) width1 / aspect);
if (height1 < minVisibleHeight) {
// If the resulting height is too small we adjust to the minimal size.
height1 = minVisibleHeight;
width1 = Math.max(minVisibleWidth,
Math.min(maxSize.x, Math.round((float) height1 * aspect)));
}
// Assuming that the height is our target we calculate the width.
height2 = Math.max(minVisibleHeight, Math.min(maxSize.y, height));
width2 = Math.round((float) height2 * aspect);
if (width2 < minVisibleWidth) {
// If the resulting width is too small we adjust to the minimal size.
width2 = minVisibleWidth;
height2 = Math.max(minVisibleHeight,
Math.min(maxSize.y, Math.round((float) width2 / aspect)));
}
// Calculate the angle
mTmpDownVector.set(downSecondPoint.x - downPoint.x,
downSecondPoint.y - downPoint.y);
mTmpLastVector.set(lastSecondPoint.x - lastPoint.x,
lastSecondPoint.y - lastPoint.y);
float angle = (float) Math.atan2(cross(mTmpDownVector, mTmpLastVector),
dot(mTmpDownVector, mTmpLastVector));
return constrainRotationAngle((float) Math.toDegrees(angle));
}
private float getMinScale(Rect bounds, Point minSize) {
return Math.max((float) minSize.x / bounds.width(), (float) minSize.y / bounds.height());
}
private float getMaxScale(Rect bounds, Point maxSize) {
return Math.min((float) maxSize.x / bounds.width(), (float) maxSize.y / bounds.height());
}
private float constrainRotationAngle(float angle) {
// Remove some degrees so that user doesn't immediately start rotating until a threshold
return Math.signum(angle) * Math.max(0, (Math.abs(dampedRotate(angle)) - ANGLE_THRESHOLD));
}
/**
* Given the current rotation angle, dampen it so that as it approaches the maximum angle,
* dampen it.
*/
private float dampedRotate(float amount) {
if (Float.compare(amount, 0) == 0) return 0;
float f = amount / PINCH_RESIZE_MAX_ANGLE_ROTATION;
f = f / (Math.abs(f)) * (overRotateInfluenceCurve(Math.abs(f)));
// Clamp this factor, f, to -1 < f < 1
if (Math.abs(f) >= 1) {
f /= Math.abs(f);
}
return OVERROTATE_DAMP_FACTOR * f * PINCH_RESIZE_MAX_ANGLE_ROTATION;
}
// Use the bigger of the two rectangles if the major change was positive, otherwise
// do the opposite.
final boolean grows = width > (right - left) || height > (bottom - top);
if (grows == (width1 * height1 > width2 * height2)) {
width = width1;
height = height1;
} else {
width = width2;
height = height2;
/**
* Returns a value that corresponds to y = (f - 1)^3 + 1.
*/
private float overRotateInfluenceCurve(float f) {
f -= 1.0f;
return f * f * f + 1.0f;
}
private void getCentroid(PointF p1, PointF p2, PointF centroidOut) {
centroidOut.set((p2.x + p1.x) / 2, (p2.y + p1.y) / 2);
}
private float dot(PointF p1, PointF p2) {
return p1.x * p2.x + p1.y * p2.y;
}
private float cross(PointF p1, PointF p2) {
return p1.x * p2.y - p1.y * p2.x;
}
private void scaleRectAboutCenter(Rect r, float scale) {
if (scale != 1.0f) {
int cx = r.centerX();
int cy = r.centerY();
r.offset(-cx, -cy);
r.scale(scale);
r.offset(cx, cy);
}
TMP_RECT.set(currentPipBounds.centerX() - width / 2,
currentPipBounds.centerY() - height / 2,
currentPipBounds.centerX() + width / 2,
currentPipBounds.centerY() + height / 2);
TMP_RECT.offset(dx, dy);
return TMP_RECT;
}
}

View File

@@ -63,10 +63,7 @@ public class PipResizeGestureHandler {
private static final String TAG = "PipResizeGestureHandler";
private static final int PINCH_RESIZE_SNAP_DURATION = 250;
private static final int PINCH_RESIZE_MAX_ANGLE_ROTATION = 45;
private static final float PINCH_RESIZE_AUTO_MAX_RATIO = 0.9f;
private static final float OVERROTATE_DAMP_FACTOR = 0.4f;
private static final float ANGLE_THRESHOLD = 5f;
private final Context mContext;
private final PipBoundsAlgorithm mPipBoundsAlgorithm;
@@ -75,17 +72,20 @@ public class PipResizeGestureHandler {
private final PipTaskOrganizer mPipTaskOrganizer;
private final PhonePipMenuController mPhonePipMenuController;
private final PipUiEventLogger mPipUiEventLogger;
private final PipPinchResizingAlgorithm mPinchResizingAlgorithm;
private final int mDisplayId;
private final ShellExecutor mMainExecutor;
private final Region mTmpRegion = new Region();
private final PointF mDownPoint = new PointF();
private final PointF mDownSecondaryPoint = new PointF();
private final PointF mDownSecondPoint = new PointF();
private final PointF mLastPoint = new PointF();
private final PointF mLastSecondPoint = new PointF();
private final Point mMaxSize = new Point();
private final Point mMinSize = new Point();
private final Rect mLastResizeBounds = new Rect();
private final Rect mUserResizeBounds = new Rect();
private final Rect mLastDownBounds = new Rect();
private final Rect mDownBounds = new Rect();
private final Rect mDragCornerSize = new Rect();
private final Rect mTmpTopLeftCorner = new Rect();
private final Rect mTmpTopRightCorner = new Rect();
@@ -103,11 +103,7 @@ public class PipResizeGestureHandler {
private boolean mIsEnabled;
private boolean mEnablePinchResize;
private boolean mIsSysUiStateValid;
// For drag-resize
private boolean mThresholdCrossed;
// For pinch-resize
private boolean mThresholdCrossed0;
private boolean mThresholdCrossed1;
private boolean mOngoingPinchToResize = false;
private float mAngle = 0;
int mFirstIndex = -1;
@@ -135,6 +131,7 @@ public class PipResizeGestureHandler {
mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable;
mPhonePipMenuController = menuActivityController;
mPipUiEventLogger = pipUiEventLogger;
mPinchResizingAlgorithm = new PipPinchResizingAlgorithm();
}
public void init() {
@@ -294,6 +291,10 @@ public class PipResizeGestureHandler {
return mEnablePinchResize;
}
public boolean isResizing() {
return mAllowGesture;
}
public boolean willStartResizeGesture(MotionEvent ev) {
if (isInValidSysUiState()) {
switch (ev.getActionMasked()) {
@@ -362,6 +363,7 @@ public class PipResizeGestureHandler {
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
mFirstIndex = -1;
mSecondIndex = -1;
mAllowGesture = false;
finishResize();
}
@@ -371,14 +373,16 @@ public class PipResizeGestureHandler {
if (action == MotionEvent.ACTION_POINTER_DOWN) {
if (mFirstIndex == -1 && mSecondIndex == -1) {
mAllowGesture = true;
mFirstIndex = 0;
mSecondIndex = 1;
mDownPoint.set(ev.getRawX(mFirstIndex), ev.getRawY(mFirstIndex));
mDownSecondaryPoint.set(ev.getRawX(mSecondIndex), ev.getRawY(mSecondIndex));
mDownSecondPoint.set(ev.getRawX(mSecondIndex), ev.getRawY(mSecondIndex));
mDownBounds.set(mPipBoundsState.getBounds());
mLastDownBounds.set(mPipBoundsState.getBounds());
mLastResizeBounds.set(mLastDownBounds);
mLastPoint.set(mDownPoint);
mLastSecondPoint.set(mLastSecondPoint);
mLastResizeBounds.set(mDownBounds);
}
}
@@ -391,133 +395,36 @@ public class PipResizeGestureHandler {
float y0 = ev.getRawY(mFirstIndex);
float x1 = ev.getRawX(mSecondIndex);
float y1 = ev.getRawY(mSecondIndex);
mLastPoint.set(x0, y0);
mLastSecondPoint.set(x1, y1);
double hypot0 = Math.hypot(x0 - mDownPoint.x, y0 - mDownPoint.y);
double hypot1 = Math.hypot(x1 - mDownSecondaryPoint.x, y1 - mDownSecondaryPoint.y);
// Capture inputs
if (hypot0 > mTouchSlop && !mThresholdCrossed0) {
if (!mThresholdCrossed
&& (distanceBetween(mDownSecondPoint, mLastSecondPoint) > mTouchSlop
|| distanceBetween(mDownPoint, mLastPoint) > mTouchSlop)) {
mInputMonitor.pilferPointers();
mThresholdCrossed0 = true;
mThresholdCrossed = true;
// Reset the down to begin resizing from this point
mDownPoint.set(x0, y0);
mDownPoint.set(mLastPoint);
mDownSecondPoint.set(mLastSecondPoint);
}
if (hypot1 > mTouchSlop && !mThresholdCrossed1) {
mInputMonitor.pilferPointers();
mThresholdCrossed1 = true;
// Reset the down to begin resizing from this point
mDownSecondaryPoint.set(x1, y1);
}
if (mThresholdCrossed0 || mThresholdCrossed1) {
if (mThresholdCrossed) {
if (mPhonePipMenuController.isMenuVisible()) {
mPhonePipMenuController.hideMenu();
}
x0 = mThresholdCrossed0 ? x0 : mDownPoint.x;
y0 = mThresholdCrossed0 ? y0 : mDownPoint.y;
x1 = mThresholdCrossed1 ? x1 : mDownSecondaryPoint.x;
y1 = mThresholdCrossed1 ? y1 : mDownSecondaryPoint.y;
mAngle = mPinchResizingAlgorithm.calculateBoundsAndAngle(mDownPoint,
mDownSecondPoint, mLastPoint, mLastSecondPoint, mMinSize, mMaxSize,
mDownBounds, mLastResizeBounds);
final Rect originalPipBounds = mPipBoundsState.getBounds();
int focusX = (int) originalPipBounds.centerX();
int focusY = (int) originalPipBounds.centerY();
float down0X = mDownPoint.x;
float down0Y = mDownPoint.y;
float down1X = mDownSecondaryPoint.x;
float down1Y = mDownSecondaryPoint.y;
float angle = 0;
if (down0X > focusX && down0Y < focusY && down1X < focusX && down1Y > focusY) {
// Top right + Bottom left pinch to zoom.
angle = calculateRotationAngle(mLastResizeBounds.centerX(),
mLastResizeBounds.centerY(), x0, y0, x1, y1, true);
} else if (down1X > focusX && down1Y < focusY
&& down0X < focusX && down0Y > focusY) {
// Top right + Bottom left pinch to zoom.
angle = calculateRotationAngle(mLastResizeBounds.centerX(),
mLastResizeBounds.centerY(), x1, y1, x0, y0, true);
} else if (down0X < focusX && down0Y < focusY
&& down1X > focusX && down1Y > focusY) {
// Top left + bottom right pinch to zoom.
angle = calculateRotationAngle(mLastResizeBounds.centerX(),
mLastResizeBounds.centerY(), x0, y0, x1, y1, false);
} else if (down1X < focusX && down1Y < focusY
&& down0X > focusX && down0Y > focusY) {
// Top left + bottom right pinch to zoom.
angle = calculateRotationAngle(mLastResizeBounds.centerX(),
mLastResizeBounds.centerY(), x1, y1, x0, y0, false);
}
mAngle = angle;
mLastResizeBounds.set(PipPinchResizingAlgorithm.pinchResize(x0, y0, x1, y1,
mDownPoint.x, mDownPoint.y, mDownSecondaryPoint.x, mDownSecondaryPoint.y,
originalPipBounds, mMinSize.x, mMinSize.y, mMaxSize));
mPipTaskOrganizer.scheduleUserResizePip(mLastDownBounds, mLastResizeBounds,
(float) -mAngle, null);
mPipTaskOrganizer.scheduleUserResizePip(mDownBounds, mLastResizeBounds,
mAngle, null);
mPipBoundsState.setHasUserResizedPip(true);
}
}
}
private float calculateRotationAngle(int pivotX, int pivotY, float topX, float topY,
float bottomX, float bottomY, boolean positive) {
// The base angle is the angle formed by taking the angle between the center horizontal
// and one of the corners.
double baseAngle = Math.toDegrees(Math.atan2(Math.abs(mLastResizeBounds.top - pivotY),
Math.abs(mLastResizeBounds.right - pivotX)));
double angle0 = mThresholdCrossed0
? Math.toDegrees(Math.atan2(pivotY - topY, topX - pivotX)) : baseAngle;
double angle1 = mThresholdCrossed0
? Math.toDegrees(Math.atan2(pivotY - bottomY, bottomX - pivotX)) : baseAngle;
if (positive) {
angle1 = angle1 < 0 ? 180 + angle1 : angle1 - 180;
} else {
angle0 = angle0 < 0 ? -angle0 - 180 : 180 - angle0;
angle1 = -angle1;
}
// Calculate the percentage difference of [0, 90] compare to the base angle.
double diff0 = (Math.max(-90, Math.min(angle0, 90)) - baseAngle) / 90;
double diff1 = (Math.max(-90, Math.min(angle1, 90)) - baseAngle) / 90;
final float angle =
(float) (diff0 + diff1) / 2 * PINCH_RESIZE_MAX_ANGLE_ROTATION * (positive ? 1 : -1);
// Remove some degrees so that user doesn't immediately start rotating until a threshold
return angle / Math.abs(angle)
* Math.max(0, (Math.abs(dampedRotate(angle)) - ANGLE_THRESHOLD));
}
/**
* Given the current rotation angle, dampen it so that as it approaches the maximum angle,
* dampen it.
*/
private float dampedRotate(float amount) {
if (Float.compare(amount, 0) == 0) return 0;
float f = amount / PINCH_RESIZE_MAX_ANGLE_ROTATION;
f = f / (Math.abs(f)) * (overRotateInfluenceCurve(Math.abs(f)));
// Clamp this factor, f, to -1 < f < 1
if (Math.abs(f) >= 1) {
f /= Math.abs(f);
}
return OVERROTATE_DAMP_FACTOR * f * PINCH_RESIZE_MAX_ANGLE_ROTATION;
}
/**
* Returns a value that corresponds to y = (f - 1)^3 + 1.
*/
private float overRotateInfluenceCurve(float f) {
f -= 1.0f;
return f * f * f + 1.0f;
}
private void onDragCornerResize(MotionEvent ev) {
int action = ev.getActionMasked();
float x = ev.getX();
@@ -529,7 +436,7 @@ public class PipResizeGestureHandler {
if (mAllowGesture) {
setCtrlType((int) x, (int) y);
mDownPoint.set(x, y);
mLastDownBounds.set(mPipBoundsState.getBounds());
mDownBounds.set(mPipBoundsState.getBounds());
}
if (!currentPipBounds.contains((int) ev.getX(), (int) ev.getY())
&& mPhonePipMenuController.isMenuVisible()) {
@@ -560,11 +467,11 @@ public class PipResizeGestureHandler {
mLastResizeBounds.set(TaskResizingAlgorithm.resizeDrag(x, y,
mDownPoint.x, mDownPoint.y, currentPipBounds, mCtrlType, mMinSize.x,
mMinSize.y, mMaxSize, true,
mLastDownBounds.width() > mLastDownBounds.height()));
mDownBounds.width() > mDownBounds.height()));
mPipBoundsAlgorithm.transformBoundsToAspectRatio(mLastResizeBounds,
mPipBoundsState.getAspectRatio(), false /* useCurrentMinEdgeSize */,
true /* useCurrentSize */);
mPipTaskOrganizer.scheduleUserResizePip(mLastDownBounds, mLastResizeBounds,
mPipTaskOrganizer.scheduleUserResizePip(mDownBounds, mLastResizeBounds,
null);
mPipBoundsState.setHasUserResizedPip(true);
}
@@ -593,12 +500,12 @@ public class PipResizeGestureHandler {
// If user resize is pretty close to max size, just auto resize to max.
if (mLastResizeBounds.width() >= PINCH_RESIZE_AUTO_MAX_RATIO * mMaxSize.x
|| mLastResizeBounds.height() >= PINCH_RESIZE_AUTO_MAX_RATIO * mMaxSize.y) {
mLastResizeBounds.set(0, 0, mMaxSize.x, mMaxSize.y);
resizeRectAboutCenter(mLastResizeBounds, mMaxSize.x, mMaxSize.y);
}
final float snapFraction = mPipBoundsAlgorithm.getSnapFraction(mLastResizeBounds);
mPipBoundsAlgorithm.applySnapFraction(mLastResizeBounds, snapFraction);
mPipTaskOrganizer.scheduleAnimateResizePip(startBounds, mLastResizeBounds,
PINCH_RESIZE_SNAP_DURATION, -mAngle, callback);
PINCH_RESIZE_SNAP_DURATION, mAngle, callback);
} else {
mPipTaskOrganizer.scheduleFinishResizePip(mLastResizeBounds,
PipAnimationController.TRANSITION_DIRECTION_USER_RESIZE, callback);
@@ -642,6 +549,20 @@ public class PipResizeGestureHandler {
mOhmOffset = offset;
}
private float distanceBetween(PointF p1, PointF p2) {
return (float) Math.hypot(p2.x - p1.x, p2.y - p1.y);
}
private void resizeRectAboutCenter(Rect rect, int w, int h) {
int cx = rect.centerX();
int cy = rect.centerY();
int l = cx - w / 2;
int r = l + w;
int t = cy - h / 2;
int b = t + h;
rect.set(l, t, r, b);
}
public void dump(PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
pw.println(prefix + TAG);

View File

@@ -664,7 +664,7 @@ public class PipTouchHandler {
} else if (menuState == MENU_STATE_NONE && mMenuState == MENU_STATE_FULL) {
// Try and restore the PiP to the closest edge, using the saved snap fraction
// if possible
if (resize) {
if (resize && !mPipResizeGestureHandler.isResizing()) {
if (mDeferResizeToNormalBoundsUntilRotation == -1) {
// This is a very special case: when the menu is expanded and visible,
// navigating to another activity can trigger auto-enter PiP, and if the