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