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