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 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 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 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 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 */ 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 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 */ 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 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 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 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 217 public SnapTarget getFirstSplitTarget() { 218 return mFirstSplitTarget; 219 } 220 221 public SnapTarget getLastSplitTarget() { 222 return mLastSplitTarget; 223 } 224 225 public SnapTarget getDismissStartTarget() { 226 return mDismissStartTarget; 227 } 228 229 public SnapTarget getDismissEndTarget() { 230 return mDismissEndTarget; 231 } 232 233 private int getStartInset() { 234 if (mIsHorizontalDivision) { 235 return mInsets.top; 236 } else { 237 return mInsets.left; 238 } 239 } 240 241 private int getEndInset() { 242 if (mIsHorizontalDivision) { 243 return mInsets.bottom; 244 } else { 245 return mInsets.right; 246 } 247 } 248 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 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 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 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 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 */ 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 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 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 362 public SnapTarget getMiddleTarget() { 363 return mMiddleTarget; 364 } 365 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 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 */ 386 public boolean showMiddleSplitTargetForAccessibility() { 387 return (mTargets.size() - 2) > 1; 388 } 389 390 public boolean isFirstSplitTargetAvailable() { 391 return mFirstSplitTarget != mMiddleTarget; 392 } 393 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 */ 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 447 public SnapTarget(int position, int taskPosition, int flag) { 448 this(position, taskPosition, flag, 1f); 449 } 450 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