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