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