1 /* 2 * Copyright (C) 2015 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 static android.view.WindowManager.DOCKED_INVALID; 20 import static android.view.WindowManager.DOCKED_LEFT; 21 import static android.view.WindowManager.DOCKED_RIGHT; 22 23 import android.content.Context; 24 import android.content.res.Configuration; 25 import android.content.res.Resources; 26 import android.graphics.Rect; 27 import android.hardware.display.DisplayManager; 28 import android.view.Display; 29 import android.view.DisplayInfo; 30 31 import java.util.ArrayList; 32 33 /** 34 * Calculates the snap targets and the snap position given a position and a velocity. All positions 35 * here are to be interpreted as the left/top edge of the divider rectangle. 36 * 37 * @hide 38 */ 39 public class DividerSnapAlgorithm { 40 41 private static final int MIN_FLING_VELOCITY_DP_PER_SECOND = 400; 42 private static final int MIN_DISMISS_VELOCITY_DP_PER_SECOND = 600; 43 44 /** 45 * 3 snap targets: left/top has 16:9 ratio (for videos), 1:1, and right/bottom has 16:9 ratio 46 */ 47 private static final int SNAP_MODE_16_9 = 0; 48 49 /** 50 * 3 snap targets: fixed ratio, 1:1, (1 - fixed ratio) 51 */ 52 private static final int SNAP_FIXED_RATIO = 1; 53 54 /** 55 * 1 snap target: 1:1 56 */ 57 private static final int SNAP_ONLY_1_1 = 2; 58 59 /** 60 * 1 snap target: minimized height, (1 - minimized height) 61 */ 62 private static final int SNAP_MODE_MINIMIZED = 3; 63 64 private final float mMinFlingVelocityPxPerSecond; 65 private final float mMinDismissVelocityPxPerSecond; 66 private final int mDisplayWidth; 67 private final int mDisplayHeight; 68 private final int mDividerSize; 69 private final ArrayList<SnapTarget> mTargets = new ArrayList<>(); 70 private final Rect mInsets = new Rect(); 71 private final int mSnapMode; 72 private final boolean mFreeSnapMode; 73 private final int mMinimalSizeResizableTask; 74 private final int mTaskHeightInMinimizedMode; 75 private final float mFixedRatio; 76 private boolean mIsHorizontalDivision; 77 78 /** The first target which is still splitting the screen */ 79 private final SnapTarget mFirstSplitTarget; 80 81 /** The last target which is still splitting the screen */ 82 private final SnapTarget mLastSplitTarget; 83 84 private final SnapTarget mDismissStartTarget; 85 private final SnapTarget mDismissEndTarget; 86 private final SnapTarget mMiddleTarget; 87 create(Context ctx, Rect insets)88 public static DividerSnapAlgorithm create(Context ctx, Rect insets) { 89 DisplayInfo displayInfo = new DisplayInfo(); 90 ctx.getSystemService(DisplayManager.class).getDisplay( 91 Display.DEFAULT_DISPLAY).getDisplayInfo(displayInfo); 92 int dividerWindowWidth = ctx.getResources().getDimensionPixelSize( 93 com.android.internal.R.dimen.docked_stack_divider_thickness); 94 int dividerInsets = ctx.getResources().getDimensionPixelSize( 95 com.android.internal.R.dimen.docked_stack_divider_insets); 96 return new DividerSnapAlgorithm(ctx.getResources(), 97 displayInfo.logicalWidth, displayInfo.logicalHeight, 98 dividerWindowWidth - 2 * dividerInsets, 99 ctx.getApplicationContext().getResources().getConfiguration().orientation 100 == Configuration.ORIENTATION_PORTRAIT, 101 insets); 102 } 103 DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize, boolean isHorizontalDivision, Rect insets)104 public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize, 105 boolean isHorizontalDivision, Rect insets) { 106 this(res, displayWidth, displayHeight, dividerSize, isHorizontalDivision, insets, 107 DOCKED_INVALID, false /* minimized */, true /* resizable */); 108 } 109 DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize, boolean isHorizontalDivision, Rect insets, int dockSide)110 public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize, 111 boolean isHorizontalDivision, Rect insets, int dockSide) { 112 this(res, displayWidth, displayHeight, dividerSize, isHorizontalDivision, insets, 113 dockSide, false /* minimized */, true /* resizable */); 114 } 115 DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize, boolean isHorizontalDivision, Rect insets, int dockSide, boolean isMinimizedMode, boolean isHomeResizable)116 public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize, 117 boolean isHorizontalDivision, Rect insets, int dockSide, boolean isMinimizedMode, 118 boolean isHomeResizable) { 119 mMinFlingVelocityPxPerSecond = 120 MIN_FLING_VELOCITY_DP_PER_SECOND * res.getDisplayMetrics().density; 121 mMinDismissVelocityPxPerSecond = 122 MIN_DISMISS_VELOCITY_DP_PER_SECOND * res.getDisplayMetrics().density; 123 mDividerSize = dividerSize; 124 mDisplayWidth = displayWidth; 125 mDisplayHeight = displayHeight; 126 mIsHorizontalDivision = isHorizontalDivision; 127 mInsets.set(insets); 128 mSnapMode = isMinimizedMode ? SNAP_MODE_MINIMIZED : 129 res.getInteger(com.android.internal.R.integer.config_dockedStackDividerSnapMode); 130 mFreeSnapMode = res.getBoolean( 131 com.android.internal.R.bool.config_dockedStackDividerFreeSnapMode); 132 mFixedRatio = res.getFraction( 133 com.android.internal.R.fraction.docked_stack_divider_fixed_ratio, 1, 1); 134 mMinimalSizeResizableTask = res.getDimensionPixelSize( 135 com.android.internal.R.dimen.default_minimal_size_resizable_task); 136 mTaskHeightInMinimizedMode = isHomeResizable ? res.getDimensionPixelSize( 137 com.android.internal.R.dimen.task_height_of_minimized_mode) : 0; 138 calculateTargets(isHorizontalDivision, dockSide); 139 mFirstSplitTarget = mTargets.get(1); 140 mLastSplitTarget = mTargets.get(mTargets.size() - 2); 141 mDismissStartTarget = mTargets.get(0); 142 mDismissEndTarget = mTargets.get(mTargets.size() - 1); 143 mMiddleTarget = mTargets.get(mTargets.size() / 2); 144 mMiddleTarget.isMiddleTarget = true; 145 } 146 147 /** 148 * @return whether it's feasible to enable split screen in the current configuration, i.e. when 149 * snapping in the middle both tasks are larger than the minimal task size. 150 */ isSplitScreenFeasible()151 public boolean isSplitScreenFeasible() { 152 int statusBarSize = mInsets.top; 153 int navBarSize = mIsHorizontalDivision ? mInsets.bottom : mInsets.right; 154 int size = mIsHorizontalDivision 155 ? mDisplayHeight 156 : mDisplayWidth; 157 int availableSpace = size - navBarSize - statusBarSize - mDividerSize; 158 return availableSpace / 2 >= mMinimalSizeResizableTask; 159 } 160 calculateSnapTarget(int position, float velocity)161 public SnapTarget calculateSnapTarget(int position, float velocity) { 162 return calculateSnapTarget(position, velocity, true /* hardDismiss */); 163 } 164 165 /** 166 * @param position the top/left position of the divider 167 * @param velocity current dragging velocity 168 * @param hardDismiss if set, make it a bit harder to get reach the dismiss targets 169 */ calculateSnapTarget(int position, float velocity, boolean hardDismiss)170 public SnapTarget calculateSnapTarget(int position, float velocity, boolean hardDismiss) { 171 if (position < mFirstSplitTarget.position && velocity < -mMinDismissVelocityPxPerSecond) { 172 return mDismissStartTarget; 173 } 174 if (position > mLastSplitTarget.position && velocity > mMinDismissVelocityPxPerSecond) { 175 return mDismissEndTarget; 176 } 177 if (Math.abs(velocity) < mMinFlingVelocityPxPerSecond) { 178 return snap(position, hardDismiss); 179 } 180 if (velocity < 0) { 181 return mFirstSplitTarget; 182 } else { 183 return mLastSplitTarget; 184 } 185 } 186 calculateNonDismissingSnapTarget(int position)187 public SnapTarget calculateNonDismissingSnapTarget(int position) { 188 SnapTarget target = snap(position, false /* hardDismiss */); 189 if (target == mDismissStartTarget) { 190 return mFirstSplitTarget; 191 } else if (target == mDismissEndTarget) { 192 return mLastSplitTarget; 193 } else { 194 return target; 195 } 196 } 197 calculateDismissingFraction(int position)198 public float calculateDismissingFraction(int position) { 199 if (position < mFirstSplitTarget.position) { 200 return 1f - (float) (position - getStartInset()) 201 / (mFirstSplitTarget.position - getStartInset()); 202 } else if (position > mLastSplitTarget.position) { 203 return (float) (position - mLastSplitTarget.position) 204 / (mDismissEndTarget.position - mLastSplitTarget.position - mDividerSize); 205 } 206 return 0f; 207 } 208 getClosestDismissTarget(int position)209 public SnapTarget getClosestDismissTarget(int position) { 210 if (position < mFirstSplitTarget.position) { 211 return mDismissStartTarget; 212 } else if (position > mLastSplitTarget.position) { 213 return mDismissEndTarget; 214 } else if (position - mDismissStartTarget.position 215 < mDismissEndTarget.position - position) { 216 return mDismissStartTarget; 217 } else { 218 return mDismissEndTarget; 219 } 220 } 221 getFirstSplitTarget()222 public SnapTarget getFirstSplitTarget() { 223 return mFirstSplitTarget; 224 } 225 getLastSplitTarget()226 public SnapTarget getLastSplitTarget() { 227 return mLastSplitTarget; 228 } 229 getDismissStartTarget()230 public SnapTarget getDismissStartTarget() { 231 return mDismissStartTarget; 232 } 233 getDismissEndTarget()234 public SnapTarget getDismissEndTarget() { 235 return mDismissEndTarget; 236 } 237 getStartInset()238 private int getStartInset() { 239 if (mIsHorizontalDivision) { 240 return mInsets.top; 241 } else { 242 return mInsets.left; 243 } 244 } 245 getEndInset()246 private int getEndInset() { 247 if (mIsHorizontalDivision) { 248 return mInsets.bottom; 249 } else { 250 return mInsets.right; 251 } 252 } 253 shouldApplyFreeSnapMode(int position)254 private boolean shouldApplyFreeSnapMode(int position) { 255 if (!mFreeSnapMode) { 256 return false; 257 } 258 if (!isFirstSplitTargetAvailable() || !isLastSplitTargetAvailable()) { 259 return false; 260 } 261 return mFirstSplitTarget.position < position && position < mLastSplitTarget.position; 262 } 263 snap(int position, boolean hardDismiss)264 private SnapTarget snap(int position, boolean hardDismiss) { 265 if (shouldApplyFreeSnapMode(position)) { 266 return new SnapTarget(position, position, SnapTarget.FLAG_NONE); 267 } 268 int minIndex = -1; 269 float minDistance = Float.MAX_VALUE; 270 int size = mTargets.size(); 271 for (int i = 0; i < size; i++) { 272 SnapTarget target = mTargets.get(i); 273 float distance = Math.abs(position - target.position); 274 if (hardDismiss) { 275 distance /= target.distanceMultiplier; 276 } 277 if (distance < minDistance) { 278 minIndex = i; 279 minDistance = distance; 280 } 281 } 282 return mTargets.get(minIndex); 283 } 284 calculateTargets(boolean isHorizontalDivision, int dockedSide)285 private void calculateTargets(boolean isHorizontalDivision, int dockedSide) { 286 mTargets.clear(); 287 int dividerMax = isHorizontalDivision 288 ? mDisplayHeight 289 : mDisplayWidth; 290 int navBarSize = isHorizontalDivision ? mInsets.bottom : mInsets.right; 291 int startPos = -mDividerSize; 292 if (dockedSide == DOCKED_RIGHT) { 293 startPos += mInsets.left; 294 } 295 mTargets.add(new SnapTarget(startPos, startPos, SnapTarget.FLAG_DISMISS_START, 296 0.35f)); 297 switch (mSnapMode) { 298 case SNAP_MODE_16_9: 299 addRatio16_9Targets(isHorizontalDivision, dividerMax); 300 break; 301 case SNAP_FIXED_RATIO: 302 addFixedDivisionTargets(isHorizontalDivision, dividerMax); 303 break; 304 case SNAP_ONLY_1_1: 305 addMiddleTarget(isHorizontalDivision); 306 break; 307 case SNAP_MODE_MINIMIZED: 308 addMinimizedTarget(isHorizontalDivision, dockedSide); 309 break; 310 } 311 mTargets.add(new SnapTarget(dividerMax - navBarSize, dividerMax, 312 SnapTarget.FLAG_DISMISS_END, 0.35f)); 313 } 314 addNonDismissingTargets(boolean isHorizontalDivision, int topPosition, int bottomPosition, int dividerMax)315 private void addNonDismissingTargets(boolean isHorizontalDivision, int topPosition, 316 int bottomPosition, int dividerMax) { 317 maybeAddTarget(topPosition, topPosition - getStartInset()); 318 addMiddleTarget(isHorizontalDivision); 319 maybeAddTarget(bottomPosition, 320 dividerMax - getEndInset() - (bottomPosition + mDividerSize)); 321 } 322 addFixedDivisionTargets(boolean isHorizontalDivision, int dividerMax)323 private void addFixedDivisionTargets(boolean isHorizontalDivision, int dividerMax) { 324 int start = isHorizontalDivision ? mInsets.top : mInsets.left; 325 int end = isHorizontalDivision 326 ? mDisplayHeight - mInsets.bottom 327 : mDisplayWidth - mInsets.right; 328 int size = (int) (mFixedRatio * (end - start)) - mDividerSize / 2; 329 int topPosition = start + size; 330 int bottomPosition = end - size - mDividerSize; 331 addNonDismissingTargets(isHorizontalDivision, topPosition, bottomPosition, dividerMax); 332 } 333 addRatio16_9Targets(boolean isHorizontalDivision, int dividerMax)334 private void addRatio16_9Targets(boolean isHorizontalDivision, int dividerMax) { 335 int start = isHorizontalDivision ? mInsets.top : mInsets.left; 336 int end = isHorizontalDivision 337 ? mDisplayHeight - mInsets.bottom 338 : mDisplayWidth - mInsets.right; 339 int startOther = isHorizontalDivision ? mInsets.left : mInsets.top; 340 int endOther = isHorizontalDivision 341 ? mDisplayWidth - mInsets.right 342 : mDisplayHeight - mInsets.bottom; 343 float size = 9.0f / 16.0f * (endOther - startOther); 344 int sizeInt = (int) Math.floor(size); 345 int topPosition = start + sizeInt; 346 int bottomPosition = end - sizeInt - mDividerSize; 347 addNonDismissingTargets(isHorizontalDivision, topPosition, bottomPosition, dividerMax); 348 } 349 350 /** 351 * Adds a target at {@param position} but only if the area with size of {@param smallerSize} 352 * meets the minimal size requirement. 353 */ maybeAddTarget(int position, int smallerSize)354 private void maybeAddTarget(int position, int smallerSize) { 355 if (smallerSize >= mMinimalSizeResizableTask) { 356 mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE)); 357 } 358 } 359 addMiddleTarget(boolean isHorizontalDivision)360 private void addMiddleTarget(boolean isHorizontalDivision) { 361 int position = DockedDividerUtils.calculateMiddlePosition(isHorizontalDivision, 362 mInsets, mDisplayWidth, mDisplayHeight, mDividerSize); 363 mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE)); 364 } 365 addMinimizedTarget(boolean isHorizontalDivision, int dockedSide)366 private void addMinimizedTarget(boolean isHorizontalDivision, int dockedSide) { 367 // In portrait offset the position by the statusbar height, in landscape add the statusbar 368 // height as well to match portrait offset 369 int position = mTaskHeightInMinimizedMode + mInsets.top; 370 if (!isHorizontalDivision) { 371 if (dockedSide == DOCKED_LEFT) { 372 position += mInsets.left; 373 } else if (dockedSide == DOCKED_RIGHT) { 374 position = mDisplayWidth - position - mInsets.right - mDividerSize; 375 } 376 } 377 mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE)); 378 } 379 getMiddleTarget()380 public SnapTarget getMiddleTarget() { 381 return mMiddleTarget; 382 } 383 getNextTarget(SnapTarget snapTarget)384 public SnapTarget getNextTarget(SnapTarget snapTarget) { 385 int index = mTargets.indexOf(snapTarget); 386 if (index != -1 && index < mTargets.size() - 1) { 387 return mTargets.get(index + 1); 388 } 389 return snapTarget; 390 } 391 getPreviousTarget(SnapTarget snapTarget)392 public SnapTarget getPreviousTarget(SnapTarget snapTarget) { 393 int index = mTargets.indexOf(snapTarget); 394 if (index != -1 && index > 0) { 395 return mTargets.get(index - 1); 396 } 397 return snapTarget; 398 } 399 400 /** 401 * @return whether or not there are more than 1 split targets that do not include the two 402 * dismiss targets, used in deciding to display the middle target for accessibility 403 */ showMiddleSplitTargetForAccessibility()404 public boolean showMiddleSplitTargetForAccessibility() { 405 return (mTargets.size() - 2) > 1; 406 } 407 isFirstSplitTargetAvailable()408 public boolean isFirstSplitTargetAvailable() { 409 return mFirstSplitTarget != mMiddleTarget; 410 } 411 isLastSplitTargetAvailable()412 public boolean isLastSplitTargetAvailable() { 413 return mLastSplitTarget != mMiddleTarget; 414 } 415 416 /** 417 * Cycles through all non-dismiss targets with a stepping of {@param increment}. It moves left 418 * if {@param increment} is negative and moves right otherwise. 419 */ cycleNonDismissTarget(SnapTarget snapTarget, int increment)420 public SnapTarget cycleNonDismissTarget(SnapTarget snapTarget, int increment) { 421 int index = mTargets.indexOf(snapTarget); 422 if (index != -1) { 423 SnapTarget newTarget = mTargets.get((index + mTargets.size() + increment) 424 % mTargets.size()); 425 if (newTarget == mDismissStartTarget) { 426 return mLastSplitTarget; 427 } else if (newTarget == mDismissEndTarget) { 428 return mFirstSplitTarget; 429 } else { 430 return newTarget; 431 } 432 } 433 return snapTarget; 434 } 435 436 /** 437 * Represents a snap target for the divider. 438 */ 439 public static class SnapTarget { 440 public static final int FLAG_NONE = 0; 441 442 /** If the divider reaches this value, the left/top task should be dismissed. */ 443 public static final int FLAG_DISMISS_START = 1; 444 445 /** If the divider reaches this value, the right/bottom task should be dismissed */ 446 public static final int FLAG_DISMISS_END = 2; 447 448 /** Position of this snap target. The right/bottom edge of the top/left task snaps here. */ 449 public final int position; 450 451 /** 452 * Like {@link #position}, but used to calculate the task bounds which might be different 453 * from the stack bounds. 454 */ 455 public final int taskPosition; 456 457 public final int flag; 458 459 public boolean isMiddleTarget; 460 461 /** 462 * Multiplier used to calculate distance to snap position. The lower this value, the harder 463 * it's to snap on this target 464 */ 465 private final float distanceMultiplier; 466 SnapTarget(int position, int taskPosition, int flag)467 public SnapTarget(int position, int taskPosition, int flag) { 468 this(position, taskPosition, flag, 1f); 469 } 470 SnapTarget(int position, int taskPosition, int flag, float distanceMultiplier)471 public SnapTarget(int position, int taskPosition, int flag, float distanceMultiplier) { 472 this.position = position; 473 this.taskPosition = taskPosition; 474 this.flag = flag; 475 this.distanceMultiplier = distanceMultiplier; 476 } 477 } 478 } 479