PipSnapAlgorithm.java revision fa7053789f6f874ea1f950826d2471d910114f6e
1/*
2 * Copyright (C) 2016 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
17package com.android.internal.policy;
18
19import android.content.Context;
20import android.content.res.Configuration;
21import android.graphics.Point;
22import android.graphics.PointF;
23import android.graphics.Rect;
24import android.hardware.display.DisplayManager;
25import android.view.Gravity;
26import android.view.ViewConfiguration;
27import android.widget.Scroller;
28
29import java.util.ArrayList;
30
31/**
32 * Calculates the snap targets and the snap position for the PIP given a position and a velocity.
33 * All bounds are relative to the display top/left.
34 */
35public class PipSnapAlgorithm {
36
37    // Allows snapping to the four corners
38    private static final int SNAP_MODE_CORNERS_ONLY = 0;
39    // Allows snapping to the four corners and the mid-points on the long edge in each orientation
40    private static final int SNAP_MODE_CORNERS_AND_SIDES = 1;
41    // Allows snapping to anywhere along the edge of the screen
42    private static final int SNAP_MODE_EDGE = 2;
43
44    private static final float SCROLL_FRICTION_MULTIPLIER = 8f;
45
46    private final Context mContext;
47
48    private final ArrayList<Integer> mSnapGravities = new ArrayList<>();
49    private final int mDefaultSnapMode = SNAP_MODE_CORNERS_ONLY;
50    private int mSnapMode = mDefaultSnapMode;
51
52    private Scroller mScroller;
53    private int mOrientation = Configuration.ORIENTATION_UNDEFINED;
54
55    public PipSnapAlgorithm(Context context) {
56        mContext = context;
57        onConfigurationChanged();
58    }
59
60    /**
61     * Updates the snap algorithm when the configuration changes.
62     */
63    public void onConfigurationChanged() {
64        mOrientation = mContext.getResources().getConfiguration().orientation;
65        calculateSnapTargets();
66    }
67
68    /**
69     * Enables snapping to the closest edge.
70     */
71    public void setSnapToEdge(boolean snapToEdge) {
72        mSnapMode = snapToEdge ? SNAP_MODE_EDGE : mDefaultSnapMode;
73    }
74
75    /**
76     * @return the closest absolute snap stack bounds for the given {@param stackBounds} moving at
77     * the given {@param velocityX} and {@param velocityY}.  The {@param movementBounds} should be
78     * those for the given {@param stackBounds}.
79     */
80    public Rect findClosestSnapBounds(Rect movementBounds, Rect stackBounds, float velocityX,
81            float velocityY) {
82        final Rect finalStackBounds = new Rect(stackBounds);
83        if (mScroller == null) {
84            final ViewConfiguration viewConfig = ViewConfiguration.get(mContext);
85            mScroller = new Scroller(mContext);
86            mScroller.setFriction(viewConfig.getScrollFriction() * SCROLL_FRICTION_MULTIPLIER);
87        }
88        mScroller.fling(stackBounds.left, stackBounds.top,
89                (int) velocityX, (int) velocityY,
90                movementBounds.left, movementBounds.right,
91                movementBounds.top, movementBounds.bottom);
92        finalStackBounds.offsetTo(mScroller.getFinalX(), mScroller.getFinalY());
93        mScroller.abortAnimation();
94        return findClosestSnapBounds(movementBounds, finalStackBounds);
95    }
96
97    /**
98     * @return the closest absolute snap stack bounds for the given {@param stackBounds}.  The
99     * {@param movementBounds} should be those for the given {@param stackBounds}.
100     */
101    public Rect findClosestSnapBounds(Rect movementBounds, Rect stackBounds) {
102        final Rect pipBounds = new Rect(movementBounds.left, movementBounds.top,
103                movementBounds.right + stackBounds.width(),
104                movementBounds.bottom + stackBounds.height());
105        final Rect newBounds = new Rect(stackBounds);
106        if (mSnapMode == SNAP_MODE_EDGE) {
107            // Find the closest edge to the given stack bounds and snap to it
108            snapRectToClosestEdge(stackBounds, movementBounds, newBounds);
109        } else {
110            // Find the closest snap point
111            final Rect tmpBounds = new Rect();
112            final Point[] snapTargets = new Point[mSnapGravities.size()];
113            for (int i = 0; i < mSnapGravities.size(); i++) {
114                Gravity.apply(mSnapGravities.get(i), stackBounds.width(), stackBounds.height(),
115                        pipBounds, 0, 0, tmpBounds);
116                snapTargets[i] = new Point(tmpBounds.left, tmpBounds.top);
117            }
118            Point snapTarget = findClosestPoint(stackBounds.left, stackBounds.top, snapTargets);
119            newBounds.offsetTo(snapTarget.x, snapTarget.y);
120        }
121        return newBounds;
122    }
123
124    /**
125     * @return returns a fraction that describes where along the {@param movementBounds} the
126     *         {@param stackBounds} are. If the {@param stackBounds} are not currently on the
127     *         {@param movementBounds} exactly, then they will be snapped to the movement bounds.
128     *
129     *         The fraction is defined in a clockwise fashion against the {@param movementBounds}:
130     *
131     *            0   1
132     *          4 +---+ 1
133     *            |   |
134     *          3 +---+ 2
135     *            3   2
136     */
137    public float getSnapFraction(Rect stackBounds, Rect movementBounds) {
138        final Rect tmpBounds = new Rect();
139        snapRectToClosestEdge(stackBounds, movementBounds, tmpBounds);
140        final float widthFraction = (float) (tmpBounds.left - movementBounds.left) /
141                movementBounds.width();
142        final float heightFraction = (float) (tmpBounds.top - movementBounds.top) /
143                movementBounds.height();
144        if (tmpBounds.top == movementBounds.top) {
145            return widthFraction;
146        } else if (tmpBounds.left == movementBounds.right) {
147            return 1f + heightFraction;
148        } else if (tmpBounds.top == movementBounds.bottom) {
149            return 2f + (1f - widthFraction);
150        } else {
151            return 3f + (1f - heightFraction);
152        }
153    }
154
155    /**
156     * Moves the {@param stackBounds} along the {@param movementBounds} to the given snap fraction.
157     * See {@link #getSnapFraction(Rect, Rect)}.
158     *
159     * The fraction is define in a clockwise fashion against the {@param movementBounds}:
160     *
161     *    0   1
162     *  4 +---+ 1
163     *    |   |
164     *  3 +---+ 2
165     *    3   2
166     */
167    public void applySnapFraction(Rect stackBounds, Rect movementBounds, float snapFraction) {
168        if (snapFraction < 1f) {
169            int offset = movementBounds.left + (int) (snapFraction * movementBounds.width());
170            stackBounds.offsetTo(offset, movementBounds.top);
171        } else if (snapFraction < 2f) {
172            snapFraction -= 1f;
173            int offset = movementBounds.top + (int) (snapFraction * movementBounds.height());
174            stackBounds.offsetTo(movementBounds.right, offset);
175        } else if (snapFraction < 3f) {
176            snapFraction -= 2f;
177            int offset = movementBounds.left + (int) ((1f - snapFraction) * movementBounds.width());
178            stackBounds.offsetTo(offset, movementBounds.bottom);
179        } else {
180            snapFraction -= 3f;
181            int offset = movementBounds.top + (int) ((1f - snapFraction) * movementBounds.height());
182            stackBounds.offsetTo(movementBounds.left, offset);
183        }
184    }
185
186    /**
187     * @return the closest point in {@param points} to the given {@param x} and {@param y}.
188     */
189    private Point findClosestPoint(int x, int y, Point[] points) {
190        Point closestPoint = null;
191        float minDistance = Float.MAX_VALUE;
192        for (Point p : points) {
193            float distance = distanceToPoint(p, x, y);
194            if (distance < minDistance) {
195                closestPoint = p;
196                minDistance = distance;
197            }
198        }
199        return closestPoint;
200    }
201
202    /**
203     * Snaps the {@param stackBounds} to the closest edge of the {@param movementBounds} and writes
204     * the new bounds out to {@param boundsOut}.
205     */
206    private void snapRectToClosestEdge(Rect stackBounds, Rect movementBounds, Rect boundsOut) {
207        final int fromLeft = Math.abs(stackBounds.left - movementBounds.left);
208        final int fromTop = Math.abs(stackBounds.top - movementBounds.top);
209        final int fromRight = Math.abs(movementBounds.right - stackBounds.left);
210        final int fromBottom = Math.abs(movementBounds.bottom - stackBounds.top);
211        final int boundedLeft = Math.max(movementBounds.left, Math.min(movementBounds.right,
212                stackBounds.left));
213        final int boundedTop = Math.max(movementBounds.top, Math.min(movementBounds.bottom,
214                stackBounds.top));
215        boundsOut.set(stackBounds);
216        if (fromLeft <= fromTop && fromLeft <= fromRight && fromLeft <= fromBottom) {
217            boundsOut.offsetTo(movementBounds.left, boundedTop);
218        } else if (fromTop <= fromLeft && fromTop <= fromRight && fromTop <= fromBottom) {
219            boundsOut.offsetTo(boundedLeft, movementBounds.top);
220        } else if (fromRight < fromLeft && fromRight < fromTop && fromRight < fromBottom) {
221            boundsOut.offsetTo(movementBounds.right, boundedTop);
222        } else {
223            boundsOut.offsetTo(boundedLeft, movementBounds.bottom);
224        }
225    }
226
227    /**
228     * @return the distance between point {@param p} and the given {@param x} and {@param y}.
229     */
230    private float distanceToPoint(Point p, int x, int y) {
231        return PointF.length(p.x - x, p.y - y);
232    }
233
234    /**
235     * Calculate the snap targets for the discrete snap modes.
236     */
237    private void calculateSnapTargets() {
238        mSnapGravities.clear();
239        switch (mSnapMode) {
240            case SNAP_MODE_CORNERS_AND_SIDES:
241                if (mOrientation == Configuration.ORIENTATION_LANDSCAPE) {
242                    mSnapGravities.add(Gravity.TOP | Gravity.CENTER_HORIZONTAL);
243                    mSnapGravities.add(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
244                } else {
245                    mSnapGravities.add(Gravity.CENTER_VERTICAL | Gravity.LEFT);
246                    mSnapGravities.add(Gravity.CENTER_VERTICAL | Gravity.RIGHT);
247                }
248                // Fall through
249            case SNAP_MODE_CORNERS_ONLY:
250                mSnapGravities.add(Gravity.TOP | Gravity.LEFT);
251                mSnapGravities.add(Gravity.TOP | Gravity.RIGHT);
252                mSnapGravities.add(Gravity.BOTTOM | Gravity.LEFT);
253                mSnapGravities.add(Gravity.BOTTOM | Gravity.RIGHT);
254                break;
255            default:
256                // Skip otherwise
257                break;
258        }
259    }
260}
261