1 /* 2 * Copyright (C) 2023 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.wm.shell.common.split; 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 startPos = -mDividerSize; 291 if (dockedSide == DOCKED_RIGHT) { 292 startPos += mInsets.left; 293 } 294 mTargets.add(new SnapTarget(startPos, startPos, SnapTarget.FLAG_DISMISS_START, 295 0.35f)); 296 switch (mSnapMode) { 297 case SNAP_MODE_16_9: 298 addRatio16_9Targets(isHorizontalDivision, dividerMax); 299 break; 300 case SNAP_FIXED_RATIO: 301 addFixedDivisionTargets(isHorizontalDivision, dividerMax); 302 break; 303 case SNAP_ONLY_1_1: 304 addMiddleTarget(isHorizontalDivision); 305 break; 306 case SNAP_MODE_MINIMIZED: 307 addMinimizedTarget(isHorizontalDivision, dockedSide); 308 break; 309 } 310 mTargets.add(new SnapTarget(dividerMax, dividerMax, SnapTarget.FLAG_DISMISS_END, 0.35f)); 311 } 312 addNonDismissingTargets(boolean isHorizontalDivision, int topPosition, int bottomPosition, int dividerMax)313 private void addNonDismissingTargets(boolean isHorizontalDivision, int topPosition, 314 int bottomPosition, int dividerMax) { 315 maybeAddTarget(topPosition, topPosition - getStartInset()); 316 addMiddleTarget(isHorizontalDivision); 317 maybeAddTarget(bottomPosition, 318 dividerMax - getEndInset() - (bottomPosition + mDividerSize)); 319 } 320 addFixedDivisionTargets(boolean isHorizontalDivision, int dividerMax)321 private void addFixedDivisionTargets(boolean isHorizontalDivision, int dividerMax) { 322 int start = isHorizontalDivision ? mInsets.top : mInsets.left; 323 int end = isHorizontalDivision 324 ? mDisplayHeight - mInsets.bottom 325 : mDisplayWidth - mInsets.right; 326 int size = (int) (mFixedRatio * (end - start)) - mDividerSize / 2; 327 int topPosition = start + size; 328 int bottomPosition = end - size - mDividerSize; 329 addNonDismissingTargets(isHorizontalDivision, topPosition, bottomPosition, dividerMax); 330 } 331 addRatio16_9Targets(boolean isHorizontalDivision, int dividerMax)332 private void addRatio16_9Targets(boolean isHorizontalDivision, int dividerMax) { 333 int start = isHorizontalDivision ? mInsets.top : mInsets.left; 334 int end = isHorizontalDivision 335 ? mDisplayHeight - mInsets.bottom 336 : mDisplayWidth - mInsets.right; 337 int startOther = isHorizontalDivision ? mInsets.left : mInsets.top; 338 int endOther = isHorizontalDivision 339 ? mDisplayWidth - mInsets.right 340 : mDisplayHeight - mInsets.bottom; 341 float size = 9.0f / 16.0f * (endOther - startOther); 342 int sizeInt = (int) Math.floor(size); 343 int topPosition = start + sizeInt; 344 int bottomPosition = end - sizeInt - mDividerSize; 345 addNonDismissingTargets(isHorizontalDivision, topPosition, bottomPosition, dividerMax); 346 } 347 348 /** 349 * Adds a target at {@param position} but only if the area with size of {@param smallerSize} 350 * meets the minimal size requirement. 351 */ maybeAddTarget(int position, int smallerSize)352 private void maybeAddTarget(int position, int smallerSize) { 353 if (smallerSize >= mMinimalSizeResizableTask) { 354 mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE)); 355 } 356 } 357 addMiddleTarget(boolean isHorizontalDivision)358 private void addMiddleTarget(boolean isHorizontalDivision) { 359 int position = DockedDividerUtils.calculateMiddlePosition(isHorizontalDivision, 360 mInsets, mDisplayWidth, mDisplayHeight, mDividerSize); 361 mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE)); 362 } 363 addMinimizedTarget(boolean isHorizontalDivision, int dockedSide)364 private void addMinimizedTarget(boolean isHorizontalDivision, int dockedSide) { 365 // In portrait offset the position by the statusbar height, in landscape add the statusbar 366 // height as well to match portrait offset 367 int position = mTaskHeightInMinimizedMode + mInsets.top; 368 if (!isHorizontalDivision) { 369 if (dockedSide == DOCKED_LEFT) { 370 position += mInsets.left; 371 } else if (dockedSide == DOCKED_RIGHT) { 372 position = mDisplayWidth - position - mInsets.right - mDividerSize; 373 } 374 } 375 mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE)); 376 } 377 getMiddleTarget()378 public SnapTarget getMiddleTarget() { 379 return mMiddleTarget; 380 } 381 getNextTarget(SnapTarget snapTarget)382 public SnapTarget getNextTarget(SnapTarget snapTarget) { 383 int index = mTargets.indexOf(snapTarget); 384 if (index != -1 && index < mTargets.size() - 1) { 385 return mTargets.get(index + 1); 386 } 387 return snapTarget; 388 } 389 getPreviousTarget(SnapTarget snapTarget)390 public SnapTarget getPreviousTarget(SnapTarget snapTarget) { 391 int index = mTargets.indexOf(snapTarget); 392 if (index != -1 && index > 0) { 393 return mTargets.get(index - 1); 394 } 395 return snapTarget; 396 } 397 398 /** 399 * @return whether or not there are more than 1 split targets that do not include the two 400 * dismiss targets, used in deciding to display the middle target for accessibility 401 */ showMiddleSplitTargetForAccessibility()402 public boolean showMiddleSplitTargetForAccessibility() { 403 return (mTargets.size() - 2) > 1; 404 } 405 isFirstSplitTargetAvailable()406 public boolean isFirstSplitTargetAvailable() { 407 return mFirstSplitTarget != mMiddleTarget; 408 } 409 isLastSplitTargetAvailable()410 public boolean isLastSplitTargetAvailable() { 411 return mLastSplitTarget != mMiddleTarget; 412 } 413 414 /** 415 * Cycles through all non-dismiss targets with a stepping of {@param increment}. It moves left 416 * if {@param increment} is negative and moves right otherwise. 417 */ cycleNonDismissTarget(SnapTarget snapTarget, int increment)418 public SnapTarget cycleNonDismissTarget(SnapTarget snapTarget, int increment) { 419 int index = mTargets.indexOf(snapTarget); 420 if (index != -1) { 421 SnapTarget newTarget = mTargets.get((index + mTargets.size() + increment) 422 % mTargets.size()); 423 if (newTarget == mDismissStartTarget) { 424 return mLastSplitTarget; 425 } else if (newTarget == mDismissEndTarget) { 426 return mFirstSplitTarget; 427 } else { 428 return newTarget; 429 } 430 } 431 return snapTarget; 432 } 433 434 /** 435 * Represents a snap target for the divider. 436 */ 437 public static class SnapTarget { 438 public static final int FLAG_NONE = 0; 439 440 /** If the divider reaches this value, the left/top task should be dismissed. */ 441 public static final int FLAG_DISMISS_START = 1; 442 443 /** If the divider reaches this value, the right/bottom task should be dismissed */ 444 public static final int FLAG_DISMISS_END = 2; 445 446 /** Position of this snap target. The right/bottom edge of the top/left task snaps here. */ 447 public final int position; 448 449 /** 450 * Like {@link #position}, but used to calculate the task bounds which might be different 451 * from the stack bounds. 452 */ 453 public final int taskPosition; 454 455 public final int flag; 456 457 public boolean isMiddleTarget; 458 459 /** 460 * Multiplier used to calculate distance to snap position. The lower this value, the harder 461 * it's to snap on this target 462 */ 463 private final float distanceMultiplier; 464 SnapTarget(int position, int taskPosition, int flag)465 public SnapTarget(int position, int taskPosition, int flag) { 466 this(position, taskPosition, flag, 1f); 467 } 468 SnapTarget(int position, int taskPosition, int flag, float distanceMultiplier)469 public SnapTarget(int position, int taskPosition, int flag, float distanceMultiplier) { 470 this.position = position; 471 this.taskPosition = taskPosition; 472 this.flag = flag; 473 this.distanceMultiplier = distanceMultiplier; 474 } 475 } 476 } 477