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 PipSnapAlgorithm(Context context)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 */ onConfigurationChanged()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 */ setMinimized(boolean isMinimized)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 */ findClosestSnapBounds(Rect movementBounds, Rect stackBounds, float velocityX, float velocityY, Point dragStartPosition)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 */ getEdgeIntersect(Rect stackBounds, Rect movementBounds, float velX, float velY, Point dragStartPosition)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 findY(float slope, float yIntercept, float x)194 private int findY(float slope, float yIntercept, float x) { 195 return (int) ((slope * x) + yIntercept); 196 } 197 findX(float slope, float yIntercept, float y)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 */ findClosestSnapBounds(Rect movementBounds, Rect stackBounds)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 */ applyMinimizedOffset(Rect stackBounds, Rect movementBounds, Point displaySize, Rect stableInsets)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 */ getSnapFraction(Rect stackBounds, Rect movementBounds)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 */ applySnapFraction(Rect stackBounds, Rect movementBounds, float snapFraction)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 */ getMovementBounds(Rect stackBounds, Rect insetBounds, Rect movementBoundsOut, int bottomOffset)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 */ getSizeForAspectRatio(float aspectRatio, float minEdgeSize, int displayWidth, int displayHeight)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 */ findClosestPoint(int x, int y, Point[] points)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 */ snapRectToClosestEdge(Rect stackBounds, Rect movementBounds, Rect boundsOut)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 */ distanceToPoint(Point p, int x, int y)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 */ calculateSnapTargets()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 dump(PrintWriter pw, String prefix)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