Home | History | Annotate | Download | only in policy
      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 
     17 package com.android.internal.policy;
     18 
     19 import android.content.Context;
     20 import android.content.res.Configuration;
     21 import android.content.res.Resources;
     22 import android.graphics.Point;
     23 import android.graphics.PointF;
     24 import android.graphics.Rect;
     25 import android.util.Size;
     26 import android.view.Gravity;
     27 import android.view.ViewConfiguration;
     28 import android.widget.Scroller;
     29 
     30 import java.io.PrintWriter;
     31 import java.util.ArrayList;
     32 
     33 /**
     34  * Calculates the snap targets and the snap position for the PIP given a position and a velocity.
     35  * All bounds are relative to the display top/left.
     36  */
     37 public class PipSnapAlgorithm {
     38 
     39     // The below SNAP_MODE_* constants correspond to the config resource value
     40     // config_pictureInPictureSnapMode and should not be changed independently.
     41     // Allows snapping to the four corners
     42     private static final int SNAP_MODE_CORNERS_ONLY = 0;
     43     // Allows snapping to the four corners and the mid-points on the long edge in each orientation
     44     private static final int SNAP_MODE_CORNERS_AND_SIDES = 1;
     45     // Allows snapping to anywhere along the edge of the screen
     46     private static final int SNAP_MODE_EDGE = 2;
     47     // Allows snapping anywhere along the edge of the screen and magnets towards corners
     48     private static final int SNAP_MODE_EDGE_MAGNET_CORNERS = 3;
     49     // Allows snapping on the long edge in each orientation and magnets towards corners
     50     private static final int SNAP_MODE_LONG_EDGE_MAGNET_CORNERS = 4;
     51 
     52     // The friction multiplier to control how slippery the PIP is when flung
     53     private static final float SCROLL_FRICTION_MULTIPLIER = 8f;
     54 
     55     // Threshold to magnet to a corner
     56     private static final float CORNER_MAGNET_THRESHOLD = 0.3f;
     57 
     58     private final Context mContext;
     59 
     60     private final ArrayList<Integer> mSnapGravities = new ArrayList<>();
     61     private final int mDefaultSnapMode = SNAP_MODE_EDGE_MAGNET_CORNERS;
     62     private int mSnapMode = mDefaultSnapMode;
     63 
     64     private final float mDefaultSizePercent;
     65     private final float mMinAspectRatioForMinSize;
     66     private final float mMaxAspectRatioForMinSize;
     67 
     68     private Scroller mScroller;
     69     private int mOrientation = Configuration.ORIENTATION_UNDEFINED;
     70 
     71     private final int mMinimizedVisibleSize;
     72     private boolean mIsMinimized;
     73 
     74     public PipSnapAlgorithm(Context context) {
     75         Resources res = context.getResources();
     76         mContext = context;
     77         mMinimizedVisibleSize = res.getDimensionPixelSize(
     78                 com.android.internal.R.dimen.pip_minimized_visible_size);
     79         mDefaultSizePercent = res.getFloat(
     80                 com.android.internal.R.dimen.config_pictureInPictureDefaultSizePercent);
     81         mMaxAspectRatioForMinSize = res.getFloat(
     82                 com.android.internal.R.dimen.config_pictureInPictureAspectRatioLimitForMinSize);
     83         mMinAspectRatioForMinSize = 1f / mMaxAspectRatioForMinSize;
     84         onConfigurationChanged();
     85     }
     86 
     87     /**
     88      * Updates the snap algorithm when the configuration changes.
     89      */
     90     public void onConfigurationChanged() {
     91         Resources res = mContext.getResources();
     92         mOrientation = res.getConfiguration().orientation;
     93         mSnapMode = res.getInteger(com.android.internal.R.integer.config_pictureInPictureSnapMode);
     94         calculateSnapTargets();
     95     }
     96 
     97     /**
     98      * Sets the PIP's minimized state.
     99      */
    100     public void setMinimized(boolean isMinimized) {
    101         mIsMinimized = isMinimized;
    102     }
    103 
    104     /**
    105      * @return the closest absolute snap stack bounds for the given {@param stackBounds} moving at
    106      * the given {@param velocityX} and {@param velocityY}.  The {@param movementBounds} should be
    107      * those for the given {@param stackBounds}.
    108      */
    109     public Rect findClosestSnapBounds(Rect movementBounds, Rect stackBounds, float velocityX,
    110             float velocityY) {
    111         final Rect finalStackBounds = new Rect(stackBounds);
    112         if (mScroller == null) {
    113             final ViewConfiguration viewConfig = ViewConfiguration.get(mContext);
    114             mScroller = new Scroller(mContext);
    115             mScroller.setFriction(viewConfig.getScrollFriction() * SCROLL_FRICTION_MULTIPLIER);
    116         }
    117         mScroller.fling(stackBounds.left, stackBounds.top,
    118                 (int) velocityX, (int) velocityY,
    119                 movementBounds.left, movementBounds.right,
    120                 movementBounds.top, movementBounds.bottom);
    121         finalStackBounds.offsetTo(mScroller.getFinalX(), mScroller.getFinalY());
    122         mScroller.abortAnimation();
    123         return findClosestSnapBounds(movementBounds, finalStackBounds);
    124     }
    125 
    126     /**
    127      * @return the closest absolute snap stack bounds for the given {@param stackBounds}.  The
    128      * {@param movementBounds} should be those for the given {@param stackBounds}.
    129      */
    130     public Rect findClosestSnapBounds(Rect movementBounds, Rect stackBounds) {
    131         final Rect pipBounds = new Rect(movementBounds.left, movementBounds.top,
    132                 movementBounds.right + stackBounds.width(),
    133                 movementBounds.bottom + stackBounds.height());
    134         final Rect newBounds = new Rect(stackBounds);
    135         if (mSnapMode == SNAP_MODE_LONG_EDGE_MAGNET_CORNERS
    136                 || mSnapMode == SNAP_MODE_EDGE_MAGNET_CORNERS) {
    137             final Rect tmpBounds = new Rect();
    138             final Point[] snapTargets = new Point[mSnapGravities.size()];
    139             for (int i = 0; i < mSnapGravities.size(); i++) {
    140                 Gravity.apply(mSnapGravities.get(i), stackBounds.width(), stackBounds.height(),
    141                         pipBounds, 0, 0, tmpBounds);
    142                 snapTargets[i] = new Point(tmpBounds.left, tmpBounds.top);
    143             }
    144             Point snapTarget = findClosestPoint(stackBounds.left, stackBounds.top, snapTargets);
    145             float distance = distanceToPoint(snapTarget, stackBounds.left, stackBounds.top);
    146             final float thresh = Math.max(stackBounds.width(), stackBounds.height())
    147                     * CORNER_MAGNET_THRESHOLD;
    148             if (distance < thresh) {
    149                 newBounds.offsetTo(snapTarget.x, snapTarget.y);
    150             } else {
    151                 snapRectToClosestEdge(stackBounds, movementBounds, newBounds);
    152             }
    153         } else if (mSnapMode == SNAP_MODE_EDGE) {
    154             // Find the closest edge to the given stack bounds and snap to it
    155             snapRectToClosestEdge(stackBounds, movementBounds, newBounds);
    156         } else {
    157             // Find the closest snap point
    158             final Rect tmpBounds = new Rect();
    159             final Point[] snapTargets = new Point[mSnapGravities.size()];
    160             for (int i = 0; i < mSnapGravities.size(); i++) {
    161                 Gravity.apply(mSnapGravities.get(i), stackBounds.width(), stackBounds.height(),
    162                         pipBounds, 0, 0, tmpBounds);
    163                 snapTargets[i] = new Point(tmpBounds.left, tmpBounds.top);
    164             }
    165             Point snapTarget = findClosestPoint(stackBounds.left, stackBounds.top, snapTargets);
    166             newBounds.offsetTo(snapTarget.x, snapTarget.y);
    167         }
    168         return newBounds;
    169     }
    170 
    171     /**
    172      * Applies the offset to the {@param stackBounds} to adjust it to a minimized state.
    173      */
    174     public void applyMinimizedOffset(Rect stackBounds, Rect movementBounds, Point displaySize,
    175             Rect stableInsets) {
    176         if (stackBounds.left <= movementBounds.centerX()) {
    177             stackBounds.offsetTo(stableInsets.left + mMinimizedVisibleSize - stackBounds.width(),
    178                     stackBounds.top);
    179         } else {
    180             stackBounds.offsetTo(displaySize.x - stableInsets.right - mMinimizedVisibleSize,
    181                     stackBounds.top);
    182         }
    183     }
    184 
    185     /**
    186      * @return returns a fraction that describes where along the {@param movementBounds} the
    187      *         {@param stackBounds} are. If the {@param stackBounds} are not currently on the
    188      *         {@param movementBounds} exactly, then they will be snapped to the movement bounds.
    189      *
    190      *         The fraction is defined in a clockwise fashion against the {@param movementBounds}:
    191      *
    192      *            0   1
    193      *          4 +---+ 1
    194      *            |   |
    195      *          3 +---+ 2
    196      *            3   2
    197      */
    198     public float getSnapFraction(Rect stackBounds, Rect movementBounds) {
    199         final Rect tmpBounds = new Rect();
    200         snapRectToClosestEdge(stackBounds, movementBounds, tmpBounds);
    201         final float widthFraction = (float) (tmpBounds.left - movementBounds.left) /
    202                 movementBounds.width();
    203         final float heightFraction = (float) (tmpBounds.top - movementBounds.top) /
    204                 movementBounds.height();
    205         if (tmpBounds.top == movementBounds.top) {
    206             return widthFraction;
    207         } else if (tmpBounds.left == movementBounds.right) {
    208             return 1f + heightFraction;
    209         } else if (tmpBounds.top == movementBounds.bottom) {
    210             return 2f + (1f - widthFraction);
    211         } else {
    212             return 3f + (1f - heightFraction);
    213         }
    214     }
    215 
    216     /**
    217      * Moves the {@param stackBounds} along the {@param movementBounds} to the given snap fraction.
    218      * See {@link #getSnapFraction(Rect, Rect)}.
    219      *
    220      * The fraction is define in a clockwise fashion against the {@param movementBounds}:
    221      *
    222      *    0   1
    223      *  4 +---+ 1
    224      *    |   |
    225      *  3 +---+ 2
    226      *    3   2
    227      */
    228     public void applySnapFraction(Rect stackBounds, Rect movementBounds, float snapFraction) {
    229         if (snapFraction < 1f) {
    230             int offset = movementBounds.left + (int) (snapFraction * movementBounds.width());
    231             stackBounds.offsetTo(offset, movementBounds.top);
    232         } else if (snapFraction < 2f) {
    233             snapFraction -= 1f;
    234             int offset = movementBounds.top + (int) (snapFraction * movementBounds.height());
    235             stackBounds.offsetTo(movementBounds.right, offset);
    236         } else if (snapFraction < 3f) {
    237             snapFraction -= 2f;
    238             int offset = movementBounds.left + (int) ((1f - snapFraction) * movementBounds.width());
    239             stackBounds.offsetTo(offset, movementBounds.bottom);
    240         } else {
    241             snapFraction -= 3f;
    242             int offset = movementBounds.top + (int) ((1f - snapFraction) * movementBounds.height());
    243             stackBounds.offsetTo(movementBounds.left, offset);
    244         }
    245     }
    246 
    247     /**
    248      * Adjusts {@param movementBoundsOut} so that it is the movement bounds for the given
    249      * {@param stackBounds}.
    250      */
    251     public void getMovementBounds(Rect stackBounds, Rect insetBounds, Rect movementBoundsOut,
    252             int imeHeight) {
    253         // Adjust the right/bottom to ensure the stack bounds never goes offscreen
    254         movementBoundsOut.set(insetBounds);
    255         movementBoundsOut.right = Math.max(insetBounds.left, insetBounds.right -
    256                 stackBounds.width());
    257         movementBoundsOut.bottom = Math.max(insetBounds.top, insetBounds.bottom -
    258                 stackBounds.height());
    259         movementBoundsOut.bottom -= imeHeight;
    260     }
    261 
    262     /**
    263      * @return the size of the PiP at the given {@param aspectRatio}, ensuring that the minimum edge
    264      * is at least {@param minEdgeSize}.
    265      */
    266     public Size getSizeForAspectRatio(float aspectRatio, float minEdgeSize, int displayWidth,
    267             int displayHeight) {
    268         final int smallestDisplaySize = Math.min(displayWidth, displayHeight);
    269         final int minSize = (int) Math.max(minEdgeSize, smallestDisplaySize * mDefaultSizePercent);
    270 
    271         final int width;
    272         final int height;
    273         if (aspectRatio <= mMinAspectRatioForMinSize || aspectRatio > mMaxAspectRatioForMinSize) {
    274             // Beyond these points, we can just use the min size as the shorter edge
    275             if (aspectRatio <= 1) {
    276                 // Portrait, width is the minimum size
    277                 width = minSize;
    278                 height = Math.round(width / aspectRatio);
    279             } else {
    280                 // Landscape, height is the minimum size
    281                 height = minSize;
    282                 width = Math.round(height * aspectRatio);
    283             }
    284         } else {
    285             // Within these points, we ensure that the bounds fit within the radius of the limits
    286             // at the points
    287             final float widthAtMaxAspectRatioForMinSize = mMaxAspectRatioForMinSize * minSize;
    288             final float radius = PointF.length(widthAtMaxAspectRatioForMinSize, minSize);
    289             height = (int) Math.round(Math.sqrt((radius * radius) /
    290                     (aspectRatio * aspectRatio + 1)));
    291             width = Math.round(height * aspectRatio);
    292         }
    293         return new Size(width, height);
    294     }
    295 
    296     /**
    297      * @return the closest point in {@param points} to the given {@param x} and {@param y}.
    298      */
    299     private Point findClosestPoint(int x, int y, Point[] points) {
    300         Point closestPoint = null;
    301         float minDistance = Float.MAX_VALUE;
    302         for (Point p : points) {
    303             float distance = distanceToPoint(p, x, y);
    304             if (distance < minDistance) {
    305                 closestPoint = p;
    306                 minDistance = distance;
    307             }
    308         }
    309         return closestPoint;
    310     }
    311 
    312     /**
    313      * Snaps the {@param stackBounds} to the closest edge of the {@param movementBounds} and writes
    314      * the new bounds out to {@param boundsOut}.
    315      */
    316     private void snapRectToClosestEdge(Rect stackBounds, Rect movementBounds, Rect boundsOut) {
    317         // If the stackBounds are minimized, then it should only be snapped back horizontally
    318         final int boundedLeft = Math.max(movementBounds.left, Math.min(movementBounds.right,
    319                 stackBounds.left));
    320         final int boundedTop = Math.max(movementBounds.top, Math.min(movementBounds.bottom,
    321                 stackBounds.top));
    322         boundsOut.set(stackBounds);
    323         if (mIsMinimized) {
    324             boundsOut.offsetTo(boundedLeft, boundedTop);
    325             return;
    326         }
    327 
    328         // Otherwise, just find the closest edge
    329         final int fromLeft = Math.abs(stackBounds.left - movementBounds.left);
    330         final int fromTop = Math.abs(stackBounds.top - movementBounds.top);
    331         final int fromRight = Math.abs(movementBounds.right - stackBounds.left);
    332         final int fromBottom = Math.abs(movementBounds.bottom - stackBounds.top);
    333         int shortest;
    334         if (mSnapMode == SNAP_MODE_LONG_EDGE_MAGNET_CORNERS) {
    335             // Only check longest edges
    336             shortest = (mOrientation == Configuration.ORIENTATION_LANDSCAPE)
    337                     ? Math.min(fromTop, fromBottom)
    338                     : Math.min(fromLeft, fromRight);
    339         } else {
    340             shortest = Math.min(Math.min(fromLeft, fromRight), Math.min(fromTop, fromBottom));
    341         }
    342         if (shortest == fromLeft) {
    343             boundsOut.offsetTo(movementBounds.left, boundedTop);
    344         } else if (shortest == fromTop) {
    345             boundsOut.offsetTo(boundedLeft, movementBounds.top);
    346         } else if (shortest == fromRight) {
    347             boundsOut.offsetTo(movementBounds.right, boundedTop);
    348         } else {
    349             boundsOut.offsetTo(boundedLeft, movementBounds.bottom);
    350         }
    351     }
    352 
    353     /**
    354      * @return the distance between point {@param p} and the given {@param x} and {@param y}.
    355      */
    356     private float distanceToPoint(Point p, int x, int y) {
    357         return PointF.length(p.x - x, p.y - y);
    358     }
    359 
    360     /**
    361      * Calculate the snap targets for the discrete snap modes.
    362      */
    363     private void calculateSnapTargets() {
    364         mSnapGravities.clear();
    365         switch (mSnapMode) {
    366             case SNAP_MODE_CORNERS_AND_SIDES:
    367                 if (mOrientation == Configuration.ORIENTATION_LANDSCAPE) {
    368                     mSnapGravities.add(Gravity.TOP | Gravity.CENTER_HORIZONTAL);
    369                     mSnapGravities.add(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
    370                 } else {
    371                     mSnapGravities.add(Gravity.CENTER_VERTICAL | Gravity.LEFT);
    372                     mSnapGravities.add(Gravity.CENTER_VERTICAL | Gravity.RIGHT);
    373                 }
    374                 // Fall through
    375             case SNAP_MODE_CORNERS_ONLY:
    376             case SNAP_MODE_EDGE_MAGNET_CORNERS:
    377             case SNAP_MODE_LONG_EDGE_MAGNET_CORNERS:
    378                 mSnapGravities.add(Gravity.TOP | Gravity.LEFT);
    379                 mSnapGravities.add(Gravity.TOP | Gravity.RIGHT);
    380                 mSnapGravities.add(Gravity.BOTTOM | Gravity.LEFT);
    381                 mSnapGravities.add(Gravity.BOTTOM | Gravity.RIGHT);
    382                 break;
    383             default:
    384                 // Skip otherwise
    385                 break;
    386         }
    387     }
    388 
    389     public void dump(PrintWriter pw, String prefix) {
    390         final String innerPrefix = prefix + "  ";
    391         pw.println(prefix + PipSnapAlgorithm.class.getSimpleName());
    392         pw.println(innerPrefix + "mSnapMode=" + mSnapMode);
    393         pw.println(innerPrefix + "mOrientation=" + mOrientation);
    394         pw.println(innerPrefix + "mMinimizedVisibleSize=" + mMinimizedVisibleSize);
    395         pw.println(innerPrefix + "mIsMinimized=" + mIsMinimized);
    396     }
    397 }
    398