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 
17 package com.android.wm.shell.common.pip;
18 
19 import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_LEFT;
20 import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_NONE;
21 import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_RIGHT;
22 
23 import android.graphics.Rect;
24 
25 import com.android.internal.annotations.VisibleForTesting;
26 
27 /**
28  * Calculates the snap targets and the snap position for the PIP given a position and a velocity.
29  * All bounds are relative to the display top/left.
30  */
31 public class PipSnapAlgorithm {
32 
33     /**
34      * Returns a fraction that describes where the PiP bounds is.
35      * See {@link #getSnapFraction(Rect, Rect, int)}.
36      */
getSnapFraction(Rect stackBounds, Rect movementBounds)37     public float getSnapFraction(Rect stackBounds, Rect movementBounds) {
38         return getSnapFraction(stackBounds, movementBounds, STASH_TYPE_NONE);
39     }
40 
41     /**
42      * @return returns a fraction that describes where along the movementBounds the
43      *         stackBounds are. If the stackBounds are not currently on the
44      *         movementBounds exactly, then they will be snapped to the movement bounds.
45      *         stashType dictates whether the PiP is stashed (off-screen) or not. If
46      *         that's the case, we will have to do some math to calculate the snap fraction
47      *         correctly.
48      *
49      *         The fraction is defined in a clockwise fashion against the movementBounds:
50      *
51      *            0   1
52      *          4 +---+ 1
53      *            |   |
54      *          3 +---+ 2
55      *            3   2
56      */
getSnapFraction(Rect stackBounds, Rect movementBounds, @PipBoundsState.StashType int stashType)57     public float getSnapFraction(Rect stackBounds, Rect movementBounds,
58             @PipBoundsState.StashType int stashType) {
59         final Rect tmpBounds = new Rect();
60         snapRectToClosestEdge(stackBounds, movementBounds, tmpBounds, stashType);
61         final float widthFraction = (float) (tmpBounds.left - movementBounds.left)
62                 / movementBounds.width();
63         final float heightFraction = (float) (tmpBounds.top - movementBounds.top)
64                 / movementBounds.height();
65         if (tmpBounds.top == movementBounds.top) {
66             return widthFraction;
67         } else if (tmpBounds.left == movementBounds.right) {
68             return 1f + heightFraction;
69         } else if (tmpBounds.top == movementBounds.bottom) {
70             return 2f + (1f - widthFraction);
71         } else {
72             return 3f + (1f - heightFraction);
73         }
74     }
75 
76     /**
77      * Moves the stackBounds along the movementBounds to the given snap fraction.
78      * See {@link #getSnapFraction(Rect, Rect)}.
79      *
80      * The fraction is define in a clockwise fashion against the movementBounds:
81      *
82      *    0   1
83      *  4 +---+ 1
84      *    |   |
85      *  3 +---+ 2
86      *    3   2
87      */
applySnapFraction(Rect stackBounds, Rect movementBounds, float snapFraction)88     public void applySnapFraction(Rect stackBounds, Rect movementBounds, float snapFraction) {
89         if (snapFraction < 1f) {
90             int offset = movementBounds.left + (int) (snapFraction * movementBounds.width());
91             stackBounds.offsetTo(offset, movementBounds.top);
92         } else if (snapFraction < 2f) {
93             snapFraction -= 1f;
94             int offset = movementBounds.top + (int) (snapFraction * movementBounds.height());
95             stackBounds.offsetTo(movementBounds.right, offset);
96         } else if (snapFraction < 3f) {
97             snapFraction -= 2f;
98             int offset = movementBounds.left + (int) ((1f - snapFraction) * movementBounds.width());
99             stackBounds.offsetTo(offset, movementBounds.bottom);
100         } else {
101             snapFraction -= 3f;
102             int offset = movementBounds.top + (int) ((1f - snapFraction) * movementBounds.height());
103             stackBounds.offsetTo(movementBounds.left, offset);
104         }
105     }
106 
107     /**
108      * Same as {@link #applySnapFraction(Rect, Rect, float)}, but take stash state into
109      * consideration.
110      */
applySnapFraction(Rect stackBounds, Rect movementBounds, float snapFraction, @PipBoundsState.StashType int stashType, int stashOffset, Rect displayBounds, Rect insetBounds)111     public void applySnapFraction(Rect stackBounds, Rect movementBounds, float snapFraction,
112             @PipBoundsState.StashType int stashType, int stashOffset, Rect displayBounds,
113             Rect insetBounds) {
114         applySnapFraction(stackBounds, movementBounds, snapFraction);
115 
116         if (stashType != STASH_TYPE_NONE) {
117             stackBounds.offsetTo(stashType == STASH_TYPE_LEFT
118                             ? stashOffset - stackBounds.width() + insetBounds.left
119                             : displayBounds.right - stashOffset - insetBounds.right,
120                     stackBounds.top);
121         }
122     }
123 
124     /**
125      * Snaps the stackBounds to the closest edge of the movementBounds and writes
126      * the new bounds out to boundsOut.
127      */
128     @VisibleForTesting
snapRectToClosestEdge(Rect stackBounds, Rect movementBounds, Rect boundsOut, @PipBoundsState.StashType int stashType)129     public void snapRectToClosestEdge(Rect stackBounds, Rect movementBounds, Rect boundsOut,
130             @PipBoundsState.StashType int stashType) {
131         int leftEdge = stackBounds.left;
132         if (stashType == STASH_TYPE_LEFT) {
133             leftEdge = movementBounds.left;
134         } else if (stashType == STASH_TYPE_RIGHT) {
135             leftEdge = movementBounds.right;
136         }
137         final int boundedLeft = Math.max(movementBounds.left, Math.min(movementBounds.right,
138                 leftEdge));
139         final int boundedTop = Math.max(movementBounds.top, Math.min(movementBounds.bottom,
140                 stackBounds.top));
141         boundsOut.set(stackBounds);
142 
143         // Otherwise, just find the closest edge
144         final int fromLeft = Math.abs(leftEdge - movementBounds.left);
145         final int fromTop = Math.abs(stackBounds.top - movementBounds.top);
146         final int fromRight = Math.abs(movementBounds.right - leftEdge);
147         final int fromBottom = Math.abs(movementBounds.bottom - stackBounds.top);
148         final int shortest = Math.min(Math.min(fromLeft, fromRight), Math.min(fromTop, fromBottom));
149         if (shortest == fromLeft) {
150             boundsOut.offsetTo(movementBounds.left, boundedTop);
151         } else if (shortest == fromTop) {
152             boundsOut.offsetTo(boundedLeft, movementBounds.top);
153         } else if (shortest == fromRight) {
154             boundsOut.offsetTo(movementBounds.right, boundedTop);
155         } else {
156             boundsOut.offsetTo(boundedLeft, movementBounds.bottom);
157         }
158     }
159 }
160