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