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 PipSnapAlgorithm(Context context)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 */ onConfigurationChanged()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 */ setMinimized(boolean isMinimized)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 */ findClosestSnapBounds(Rect movementBounds, Rect stackBounds, float velocityX, float velocityY)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 */ findClosestSnapBounds(Rect movementBounds, Rect stackBounds)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 */ applyMinimizedOffset(Rect stackBounds, Rect movementBounds, Point displaySize, Rect stableInsets)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 */ getSnapFraction(Rect stackBounds, Rect movementBounds)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 */ applySnapFraction(Rect stackBounds, Rect movementBounds, float snapFraction)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 */ getMovementBounds(Rect stackBounds, Rect insetBounds, Rect movementBoundsOut, int imeHeight)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 */ getSizeForAspectRatio(float aspectRatio, float minEdgeSize, int displayWidth, int displayHeight)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 */ findClosestPoint(int x, int y, Point[] points)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 */ snapRectToClosestEdge(Rect stackBounds, Rect movementBounds, Rect boundsOut)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 */ distanceToPoint(Point p, int x, int y)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 */ calculateSnapTargets()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 dump(PrintWriter pw, String prefix)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