1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.wm.shell.common.pip;
17 
18 import android.graphics.Point;
19 import android.graphics.PointF;
20 import android.graphics.Rect;
21 
22 /**
23  * Helper class to calculate the new size given two-fingers pinch to resize.
24  */
25 public class PipPinchResizingAlgorithm {
26 
27     private static final int PINCH_RESIZE_MAX_ANGLE_ROTATION = 45;
28     private static final float OVERROTATE_DAMP_FACTOR = 0.4f;
29     private static final float ANGLE_THRESHOLD = 5f;
30     private static final float OVERRESIZE_DAMP_FACTOR = 0.25f;
31 
32     private final PointF mTmpDownVector = new PointF();
33     private final PointF mTmpLastVector = new PointF();
34     private final PointF mTmpDownCentroid = new PointF();
35     private final PointF mTmpLastCentroid = new PointF();
36 
37     /**
38      * Updates resizeBoundsOut with the new bounds of the PIP, and returns the angle in
39      * degrees that the PIP should be rotated.
40      */
calculateBoundsAndAngle(PointF downPoint, PointF downSecondPoint, PointF lastPoint, PointF lastSecondPoint, Point minSize, Point maxSize, Rect initialBounds, Rect resizeBoundsOut)41     public float calculateBoundsAndAngle(PointF downPoint, PointF downSecondPoint,
42             PointF lastPoint, PointF lastSecondPoint, Point minSize, Point maxSize,
43             Rect initialBounds, Rect resizeBoundsOut) {
44         float downDist = (float) Math.hypot(downSecondPoint.x - downPoint.x,
45                 downSecondPoint.y - downPoint.y);
46         float dist = (float) Math.hypot(lastSecondPoint.x - lastPoint.x,
47                 lastSecondPoint.y - lastPoint.y);
48         float minScale = getMinScale(initialBounds, minSize);
49         float maxScale = getMaxScale(initialBounds, maxSize);
50         float overStretchMin = minScale - dist / downDist > 0 ? minScale - dist / downDist : 0;
51         float overStretchMax = dist / downDist - maxScale > 0 ? dist / downDist - maxScale : 0;
52         float scale = Math.max(minScale - overStretchMin * OVERRESIZE_DAMP_FACTOR,
53                 Math.min(maxScale + overStretchMax * OVERRESIZE_DAMP_FACTOR, dist / downDist));
54 
55         // Scale the bounds by the change in distance between the points
56         resizeBoundsOut.set(initialBounds);
57         scaleRectAboutCenter(resizeBoundsOut, scale);
58 
59         // Translate by the centroid movement
60         getCentroid(downPoint, downSecondPoint, mTmpDownCentroid);
61         getCentroid(lastPoint, lastSecondPoint, mTmpLastCentroid);
62         resizeBoundsOut.offset((int) (mTmpLastCentroid.x - mTmpDownCentroid.x),
63                 (int) (mTmpLastCentroid.y - mTmpDownCentroid.y));
64 
65         // Calculate the angle
66         mTmpDownVector.set(downSecondPoint.x - downPoint.x,
67                 downSecondPoint.y - downPoint.y);
68         mTmpLastVector.set(lastSecondPoint.x - lastPoint.x,
69                 lastSecondPoint.y - lastPoint.y);
70         float angle = (float) Math.atan2(cross(mTmpDownVector, mTmpLastVector),
71                 dot(mTmpDownVector, mTmpLastVector));
72         return constrainRotationAngle((float) Math.toDegrees(angle));
73     }
74 
getMinScale(Rect bounds, Point minSize)75     private float getMinScale(Rect bounds, Point minSize) {
76         return Math.max((float) minSize.x / bounds.width(), (float) minSize.y / bounds.height());
77     }
78 
getMaxScale(Rect bounds, Point maxSize)79     private float getMaxScale(Rect bounds, Point maxSize) {
80         return Math.min((float) maxSize.x / bounds.width(), (float) maxSize.y / bounds.height());
81     }
82 
constrainRotationAngle(float angle)83     private float constrainRotationAngle(float angle) {
84         // Remove some degrees so that user doesn't immediately start rotating until a threshold
85         return Math.signum(angle) * Math.max(0, (Math.abs(dampedRotate(angle)) - ANGLE_THRESHOLD));
86     }
87 
88     /**
89      * Given the current rotation angle, dampen it so that as it approaches the maximum angle,
90      * dampen it.
91      */
dampedRotate(float amount)92     private float dampedRotate(float amount) {
93         if (Float.compare(amount, 0) == 0) return 0;
94 
95         float f = amount / PINCH_RESIZE_MAX_ANGLE_ROTATION;
96         f = f / (Math.abs(f)) * (overRotateInfluenceCurve(Math.abs(f)));
97 
98         // Clamp this factor, f, to -1 < f < 1
99         if (Math.abs(f) >= 1) {
100             f /= Math.abs(f);
101         }
102         return OVERROTATE_DAMP_FACTOR * f * PINCH_RESIZE_MAX_ANGLE_ROTATION;
103     }
104 
105     /**
106      * Returns a value that corresponds to y = (f - 1)^3 + 1.
107      */
overRotateInfluenceCurve(float f)108     private float overRotateInfluenceCurve(float f) {
109         f -= 1.0f;
110         return f * f * f + 1.0f;
111     }
112 
getCentroid(PointF p1, PointF p2, PointF centroidOut)113     private void getCentroid(PointF p1, PointF p2, PointF centroidOut) {
114         centroidOut.set((p2.x + p1.x) / 2, (p2.y + p1.y) / 2);
115     }
116 
dot(PointF p1, PointF p2)117     private float dot(PointF p1, PointF p2) {
118         return p1.x * p2.x + p1.y * p2.y;
119     }
120 
cross(PointF p1, PointF p2)121     private float cross(PointF p1, PointF p2) {
122         return p1.x * p2.y - p1.y * p2.x;
123     }
124 
scaleRectAboutCenter(Rect r, float scale)125     private void scaleRectAboutCenter(Rect r, float scale) {
126         if (scale != 1.0f) {
127             int cx = r.centerX();
128             int cy = r.centerY();
129             r.offset(-cx, -cy);
130             r.scale(scale);
131             r.offset(cx, cy);
132         }
133     }
134 }
135