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     // Threshold to magnet to a corner
     53     private static final float CORNER_MAGNET_THRESHOLD = 0.3f;
     54 
     55     private final Context mContext;
     56 
     57     private final ArrayList<Integer> mSnapGravities = new ArrayList<>();
     58     private final int mDefaultSnapMode = SNAP_MODE_EDGE_MAGNET_CORNERS;
     59     private int mSnapMode = mDefaultSnapMode;
     60 
     61     private final float mDefaultSizePercent;
     62     private final float mMinAspectRatioForMinSize;
     63     private final float mMaxAspectRatioForMinSize;
     64     private final int mFlingDeceleration;
     65 
     66     private int mOrientation = Configuration.ORIENTATION_UNDEFINED;
     67 
     68     private final int mMinimizedVisibleSize;
     69     private boolean mIsMinimized;
     70 
     71     public PipSnapAlgorithm(Context context) {
     72         Resources res = context.getResources();
     73         mContext = context;
     74         mMinimizedVisibleSize = res.getDimensionPixelSize(
     75                 com.android.internal.R.dimen.pip_minimized_visible_size);
     76         mDefaultSizePercent = res.getFloat(
     77                 com.android.internal.R.dimen.config_pictureInPictureDefaultSizePercent);
     78         mMaxAspectRatioForMinSize = res.getFloat(
     79                 com.android.internal.R.dimen.config_pictureInPictureAspectRatioLimitForMinSize);
     80         mMinAspectRatioForMinSize = 1f / mMaxAspectRatioForMinSize;
     81         mFlingDeceleration = mContext.getResources().getDimensionPixelSize(
     82                 com.android.internal.R.dimen.pip_fling_deceleration);
     83         onConfigurationChanged();
     84     }
     85 
     86     /**
     87      * Updates the snap algorithm when the configuration changes.
     88      */
     89     public void onConfigurationChanged() {
     90         Resources res = mContext.getResources();
     91         mOrientation = res.getConfiguration().orientation;
     92         mSnapMode = res.getInteger(com.android.internal.R.integer.config_pictureInPictureSnapMode);
     93         calculateSnapTargets();
     94     }
     95 
     96     /**
     97      * Sets the PIP's minimized state.
     98      */
     99     public void setMinimized(boolean isMinimized) {
    100         mIsMinimized = isMinimized;
    101     }
    102 
    103     /**
    104      * @return the closest absolute snap stack bounds for the given {@param stackBounds} moving at
    105      * the given {@param velocityX} and {@param velocityY}.  The {@param movementBounds} should be
    106      * those for the given {@param stackBounds}.
    107      */
    108     public Rect findClosestSnapBounds(Rect movementBounds, Rect stackBounds, float velocityX,
    109             float velocityY, Point dragStartPosition) {
    110         final Rect intersectStackBounds = new Rect(stackBounds);
    111         final Point intersect = getEdgeIntersect(stackBounds, movementBounds, velocityX, velocityY,
    112                 dragStartPosition);
    113         intersectStackBounds.offsetTo(intersect.x, intersect.y);
    114         return findClosestSnapBounds(movementBounds, intersectStackBounds);
    115     }
    116 
    117     /**
    118      * @return The point along the {@param movementBounds} that the PIP would intersect with based
    119      *         on the provided {@param velX}, {@param velY} along with the position of the PIP when
    120      *         the gesture started, {@param dragStartPosition}.
    121      */
    122     public Point getEdgeIntersect(Rect stackBounds, Rect movementBounds, float velX, float velY,
    123             Point dragStartPosition) {
    124         final boolean isLandscape = mOrientation == Configuration.ORIENTATION_LANDSCAPE;
    125         final int x = stackBounds.left;
    126         final int y = stackBounds.top;
    127 
    128         // Find the line of movement the PIP is on. Line defined by: y = slope * x + yIntercept
    129         final float slope = velY / velX; // slope = rise / run
    130         final float yIntercept = y - slope * x; // rearrange line equation for yIntercept
    131         // The PIP can have two intercept points:
    132         // 1) Where the line intersects with one of the edges of the screen (vertical line)
    133         Point vertPoint = new Point();
    134         // 2) Where the line intersects with the top or bottom of the screen (horizontal line)
    135         Point horizPoint = new Point();
    136 
    137         // Find the vertical line intersection, x will be one of the edges
    138         vertPoint.x = velX > 0 ? movementBounds.right : movementBounds.left;
    139         // Sub in x in our line equation to determine y position
    140         vertPoint.y = findY(slope, yIntercept, vertPoint.x);
    141 
    142         // Find the horizontal line intersection, y will be the top or bottom of the screen
    143         horizPoint.y = velY > 0 ? movementBounds.bottom : movementBounds.top;
    144         // Sub in y in our line equation to determine x position
    145         horizPoint.x = findX(slope, yIntercept, horizPoint.y);
    146 
    147         // Now pick one of these points -- first determine if we're flinging along the current edge.
    148         // Only fling along current edge if it's a direction with space for the PIP to move to
    149         int maxDistance;
    150         if (isLandscape) {
    151             maxDistance = velX > 0
    152                     ? movementBounds.right - stackBounds.left
    153                     : stackBounds.left - movementBounds.left;
    154         } else {
    155             maxDistance = velY > 0
    156                     ? movementBounds.bottom - stackBounds.top
    157                     : stackBounds.top - movementBounds.top;
    158         }
    159         if (maxDistance > 0) {
    160             // Only fling along the current edge if the start and end point are on the same side
    161             final int startPoint = isLandscape ? dragStartPosition.y : dragStartPosition.x;
    162             final int endPoint = isLandscape ? horizPoint.y : horizPoint.x;
    163             final int center = movementBounds.centerX();
    164             if ((startPoint < center && endPoint < center)
    165                     || (startPoint > center && endPoint > center)) {
    166                 // We are flinging along the current edge, figure out how far it should travel
    167                 // based on velocity and assumed deceleration.
    168                 int distance = (int) (0 - Math.pow(isLandscape ? velX : velY, 2))
    169                         / (2 * mFlingDeceleration);
    170                 distance = Math.min(distance, maxDistance);
    171                 // Adjust the point for the distance
    172                 if (isLandscape) {
    173                     horizPoint.x = stackBounds.left + (velX > 0 ? distance : -distance);
    174                 } else {
    175                     horizPoint.y = stackBounds.top + (velY > 0 ? distance : -distance);
    176                 }
    177                 return horizPoint;
    178             }
    179         }
    180         // If we're not flinging along the current edge, find the closest point instead.
    181         final double distanceVert = Math.hypot(vertPoint.x - x, vertPoint.y - y);
    182         final double distanceHoriz = Math.hypot(horizPoint.x - x, horizPoint.y - y);
    183         // Ensure that we're actually going somewhere
    184         if (distanceVert == 0) {
    185             return horizPoint;
    186         }
    187         if (distanceHoriz == 0) {
    188             return vertPoint;
    189         }
    190         // Otherwise use the closest point
    191         return Math.abs(distanceVert) > Math.abs(distanceHoriz) ? horizPoint : vertPoint;
    192     }
    193 
    194     private int findY(float slope, float yIntercept, float x) {
    195         return (int) ((slope * x) + yIntercept);
    196     }
    197 
    198     private int findX(float slope, float yIntercept, float y) {
    199         return (int) ((y - yIntercept) / slope);
    200     }
    201 
    202     /**
    203      * @return the closest absolute snap stack bounds for the given {@param stackBounds}.  The
    204      * {@param movementBounds} should be those for the given {@param stackBounds}.
    205      */
    206     public Rect findClosestSnapBounds(Rect movementBounds, Rect stackBounds) {
    207         final Rect pipBounds = new Rect(movementBounds.left, movementBounds.top,
    208                 movementBounds.right + stackBounds.width(),
    209                 movementBounds.bottom + stackBounds.height());
    210         final Rect newBounds = new Rect(stackBounds);
    211         if (mSnapMode == SNAP_MODE_LONG_EDGE_MAGNET_CORNERS
    212                 || mSnapMode == SNAP_MODE_EDGE_MAGNET_CORNERS) {
    213             final Rect tmpBounds = new Rect();
    214             final Point[] snapTargets = new Point[mSnapGravities.size()];
    215             for (int i = 0; i < mSnapGravities.size(); i++) {
    216                 Gravity.apply(mSnapGravities.get(i), stackBounds.width(), stackBounds.height(),
    217                         pipBounds, 0, 0, tmpBounds);
    218                 snapTargets[i] = new Point(tmpBounds.left, tmpBounds.top);
    219             }
    220             Point snapTarget = findClosestPoint(stackBounds.left, stackBounds.top, snapTargets);
    221             float distance = distanceToPoint(snapTarget, stackBounds.left, stackBounds.top);
    222             final float thresh = Math.max(stackBounds.width(), stackBounds.height())
    223                     * CORNER_MAGNET_THRESHOLD;
    224             if (distance < thresh) {
    225                 newBounds.offsetTo(snapTarget.x, snapTarget.y);
    226             } else {
    227                 snapRectToClosestEdge(stackBounds, movementBounds, newBounds);
    228             }
    229         } else if (mSnapMode == SNAP_MODE_EDGE) {
    230             // Find the closest edge to the given stack bounds and snap to it
    231             snapRectToClosestEdge(stackBounds, movementBounds, newBounds);
    232         } else {
    233             // Find the closest snap point
    234             final Rect tmpBounds = new Rect();
    235             final Point[] snapTargets = new Point[mSnapGravities.size()];
    236             for (int i = 0; i < mSnapGravities.size(); i++) {
    237                 Gravity.apply(mSnapGravities.get(i), stackBounds.width(), stackBounds.height(),
    238                         pipBounds, 0, 0, tmpBounds);
    239                 snapTargets[i] = new Point(tmpBounds.left, tmpBounds.top);
    240             }
    241             Point snapTarget = findClosestPoint(stackBounds.left, stackBounds.top, snapTargets);
    242             newBounds.offsetTo(snapTarget.x, snapTarget.y);
    243         }
    244         return newBounds;
    245     }
    246 
    247     /**
    248      * Applies the offset to the {@param stackBounds} to adjust it to a minimized state.
    249      */
    250     public void applyMinimizedOffset(Rect stackBounds, Rect movementBounds, Point displaySize,
    251             Rect stableInsets) {
    252         if (stackBounds.left <= movementBounds.centerX()) {
    253             stackBounds.offsetTo(stableInsets.left + mMinimizedVisibleSize - stackBounds.width(),
    254                     stackBounds.top);
    255         } else {
    256             stackBounds.offsetTo(displaySize.x - stableInsets.right - mMinimizedVisibleSize,
    257                     stackBounds.top);
    258         }
    259     }
    260 
    261     /**
    262      * @return returns a fraction that describes where along the {@param movementBounds} the
    263      *         {@param stackBounds} are. If the {@param stackBounds} are not currently on the
    264      *         {@param movementBounds} exactly, then they will be snapped to the movement bounds.
    265      *
    266      *         The fraction is defined in a clockwise fashion against the {@param movementBounds}:
    267      *
    268      *            0   1
    269      *          4 +---+ 1
    270      *            |   |
    271      *          3 +---+ 2
    272      *            3   2
    273      */
    274     public float getSnapFraction(Rect stackBounds, Rect movementBounds) {
    275         final Rect tmpBounds = new Rect();
    276         snapRectToClosestEdge(stackBounds, movementBounds, tmpBounds);
    277         final float widthFraction = (float) (tmpBounds.left - movementBounds.left) /
    278                 movementBounds.width();
    279         final float heightFraction = (float) (tmpBounds.top - movementBounds.top) /
    280                 movementBounds.height();
    281         if (tmpBounds.top == movementBounds.top) {
    282             return widthFraction;
    283         } else if (tmpBounds.left == movementBounds.right) {
    284             return 1f + heightFraction;
    285         } else if (tmpBounds.top == movementBounds.bottom) {
    286             return 2f + (1f - widthFraction);
    287         } else {
    288             return 3f + (1f - heightFraction);
    289         }
    290     }
    291 
    292     /**
    293      * Moves the {@param stackBounds} along the {@param movementBounds} to the given snap fraction.
    294      * See {@link #getSnapFraction(Rect, Rect)}.
    295      *
    296      * The fraction is define in a clockwise fashion against the {@param movementBounds}:
    297      *
    298      *    0   1
    299      *  4 +---+ 1
    300      *    |   |
    301      *  3 +---+ 2
    302      *    3   2
    303      */
    304     public void applySnapFraction(Rect stackBounds, Rect movementBounds, float snapFraction) {
    305         if (snapFraction < 1f) {
    306             int offset = movementBounds.left + (int) (snapFraction * movementBounds.width());
    307             stackBounds.offsetTo(offset, movementBounds.top);
    308         } else if (snapFraction < 2f) {
    309             snapFraction -= 1f;
    310             int offset = movementBounds.top + (int) (snapFraction * movementBounds.height());
    311             stackBounds.offsetTo(movementBounds.right, offset);
    312         } else if (snapFraction < 3f) {
    313             snapFraction -= 2f;
    314             int offset = movementBounds.left + (int) ((1f - snapFraction) * movementBounds.width());
    315             stackBounds.offsetTo(offset, movementBounds.bottom);
    316         } else {
    317             snapFraction -= 3f;
    318             int offset = movementBounds.top + (int) ((1f - snapFraction) * movementBounds.height());
    319             stackBounds.offsetTo(movementBounds.left, offset);
    320         }
    321     }
    322 
    323     /**
    324      * Adjusts {@param movementBoundsOut} so that it is the movement bounds for the given
    325      * {@param stackBounds}.
    326      */
    327     public void getMovementBounds(Rect stackBounds, Rect insetBounds, Rect movementBoundsOut,
    328             int bottomOffset) {
    329         // Adjust the right/bottom to ensure the stack bounds never goes offscreen
    330         movementBoundsOut.set(insetBounds);
    331         movementBoundsOut.right = Math.max(insetBounds.left, insetBounds.right -
    332                 stackBounds.width());
    333         movementBoundsOut.bottom = Math.max(insetBounds.top, insetBounds.bottom -
    334                 stackBounds.height());
    335         movementBoundsOut.bottom -= bottomOffset;
    336     }
    337 
    338     /**
    339      * @return the size of the PiP at the given {@param aspectRatio}, ensuring that the minimum edge
    340      * is at least {@param minEdgeSize}.
    341      */
    342     public Size getSizeForAspectRatio(float aspectRatio, float minEdgeSize, int displayWidth,
    343             int displayHeight) {
    344         final int smallestDisplaySize = Math.min(displayWidth, displayHeight);
    345         final int minSize = (int) Math.max(minEdgeSize, smallestDisplaySize * mDefaultSizePercent);
    346 
    347         final int width;
    348         final int height;
    349         if (aspectRatio <= mMinAspectRatioForMinSize || aspectRatio > mMaxAspectRatioForMinSize) {
    350             // Beyond these points, we can just use the min size as the shorter edge
    351             if (aspectRatio <= 1) {
    352                 // Portrait, width is the minimum size
    353                 width = minSize;
    354                 height = Math.round(width / aspectRatio);
    355             } else {
    356                 // Landscape, height is the minimum size
    357                 height = minSize;
    358                 width = Math.round(height * aspectRatio);
    359             }
    360         } else {
    361             // Within these points, we ensure that the bounds fit within the radius of the limits
    362             // at the points
    363             final float widthAtMaxAspectRatioForMinSize = mMaxAspectRatioForMinSize * minSize;
    364             final float radius = PointF.length(widthAtMaxAspectRatioForMinSize, minSize);
    365             height = (int) Math.round(Math.sqrt((radius * radius) /
    366                     (aspectRatio * aspectRatio + 1)));
    367             width = Math.round(height * aspectRatio);
    368         }
    369         return new Size(width, height);
    370     }
    371 
    372     /**
    373      * @return the closest point in {@param points} to the given {@param x} and {@param y}.
    374      */
    375     private Point findClosestPoint(int x, int y, Point[] points) {
    376         Point closestPoint = null;
    377         float minDistance = Float.MAX_VALUE;
    378         for (Point p : points) {
    379             float distance = distanceToPoint(p, x, y);
    380             if (distance < minDistance) {
    381                 closestPoint = p;
    382                 minDistance = distance;
    383             }
    384         }
    385         return closestPoint;
    386     }
    387 
    388     /**
    389      * Snaps the {@param stackBounds} to the closest edge of the {@param movementBounds} and writes
    390      * the new bounds out to {@param boundsOut}.
    391      */
    392     private void snapRectToClosestEdge(Rect stackBounds, Rect movementBounds, Rect boundsOut) {
    393         // If the stackBounds are minimized, then it should only be snapped back horizontally
    394         final int boundedLeft = Math.max(movementBounds.left, Math.min(movementBounds.right,
    395                 stackBounds.left));
    396         final int boundedTop = Math.max(movementBounds.top, Math.min(movementBounds.bottom,
    397                 stackBounds.top));
    398         boundsOut.set(stackBounds);
    399         if (mIsMinimized) {
    400             boundsOut.offsetTo(boundedLeft, boundedTop);
    401             return;
    402         }
    403 
    404         // Otherwise, just find the closest edge
    405         final int fromLeft = Math.abs(stackBounds.left - movementBounds.left);
    406         final int fromTop = Math.abs(stackBounds.top - movementBounds.top);
    407         final int fromRight = Math.abs(movementBounds.right - stackBounds.left);
    408         final int fromBottom = Math.abs(movementBounds.bottom - stackBounds.top);
    409         int shortest;
    410         if (mSnapMode == SNAP_MODE_LONG_EDGE_MAGNET_CORNERS) {
    411             // Only check longest edges
    412             shortest = (mOrientation == Configuration.ORIENTATION_LANDSCAPE)
    413                     ? Math.min(fromTop, fromBottom)
    414                     : Math.min(fromLeft, fromRight);
    415         } else {
    416             shortest = Math.min(Math.min(fromLeft, fromRight), Math.min(fromTop, fromBottom));
    417         }
    418         if (shortest == fromLeft) {
    419             boundsOut.offsetTo(movementBounds.left, boundedTop);
    420         } else if (shortest == fromTop) {
    421             boundsOut.offsetTo(boundedLeft, movementBounds.top);
    422         } else if (shortest == fromRight) {
    423             boundsOut.offsetTo(movementBounds.right, boundedTop);
    424         } else {
    425             boundsOut.offsetTo(boundedLeft, movementBounds.bottom);
    426         }
    427     }
    428 
    429     /**
    430      * @return the distance between point {@param p} and the given {@param x} and {@param y}.
    431      */
    432     private float distanceToPoint(Point p, int x, int y) {
    433         return PointF.length(p.x - x, p.y - y);
    434     }
    435 
    436     /**
    437      * Calculate the snap targets for the discrete snap modes.
    438      */
    439     private void calculateSnapTargets() {
    440         mSnapGravities.clear();
    441         switch (mSnapMode) {
    442             case SNAP_MODE_CORNERS_AND_SIDES:
    443                 if (mOrientation == Configuration.ORIENTATION_LANDSCAPE) {
    444                     mSnapGravities.add(Gravity.TOP | Gravity.CENTER_HORIZONTAL);
    445                     mSnapGravities.add(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
    446                 } else {
    447                     mSnapGravities.add(Gravity.CENTER_VERTICAL | Gravity.LEFT);
    448                     mSnapGravities.add(Gravity.CENTER_VERTICAL | Gravity.RIGHT);
    449                 }
    450                 // Fall through
    451             case SNAP_MODE_CORNERS_ONLY:
    452             case SNAP_MODE_EDGE_MAGNET_CORNERS:
    453             case SNAP_MODE_LONG_EDGE_MAGNET_CORNERS:
    454                 mSnapGravities.add(Gravity.TOP | Gravity.LEFT);
    455                 mSnapGravities.add(Gravity.TOP | Gravity.RIGHT);
    456                 mSnapGravities.add(Gravity.BOTTOM | Gravity.LEFT);
    457                 mSnapGravities.add(Gravity.BOTTOM | Gravity.RIGHT);
    458                 break;
    459             default:
    460                 // Skip otherwise
    461                 break;
    462         }
    463     }
    464 
    465     public void dump(PrintWriter pw, String prefix) {
    466         final String innerPrefix = prefix + "  ";
    467         pw.println(prefix + PipSnapAlgorithm.class.getSimpleName());
    468         pw.println(innerPrefix + "mSnapMode=" + mSnapMode);
    469         pw.println(innerPrefix + "mOrientation=" + mOrientation);
    470         pw.println(innerPrefix + "mMinimizedVisibleSize=" + mMinimizedVisibleSize);
    471         pw.println(innerPrefix + "mIsMinimized=" + mIsMinimized);
    472     }
    473 }
    474