DividerSnapAlgorithm.java revision 62c7846bf89c170f1f41624866d3249a4e30a33a
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 17package com.android.internal.policy; 18 19import static android.view.WindowManager.DOCKED_INVALID; 20import static android.view.WindowManager.DOCKED_LEFT; 21import static android.view.WindowManager.DOCKED_RIGHT; 22 23import android.content.Context; 24import android.content.res.Configuration; 25import android.content.res.Resources; 26import android.graphics.Rect; 27import android.hardware.display.DisplayManager; 28import android.util.Log; 29import android.view.Display; 30import android.view.DisplayInfo; 31 32import java.util.ArrayList; 33 34/** 35 * Calculates the snap targets and the snap position given a position and a velocity. All positions 36 * here are to be interpreted as the left/top edge of the divider rectangle. 37 * 38 * @hide 39 */ 40public class DividerSnapAlgorithm { 41 42 private static final int MIN_FLING_VELOCITY_DP_PER_SECOND = 400; 43 private static final int MIN_DISMISS_VELOCITY_DP_PER_SECOND = 600; 44 45 /** 46 * 3 snap targets: left/top has 16:9 ratio (for videos), 1:1, and right/bottom has 16:9 ratio 47 */ 48 private static final int SNAP_MODE_16_9 = 0; 49 50 /** 51 * 3 snap targets: fixed ratio, 1:1, (1 - fixed ratio) 52 */ 53 private static final int SNAP_FIXED_RATIO = 1; 54 55 /** 56 * 1 snap target: 1:1 57 */ 58 private static final int SNAP_ONLY_1_1 = 2; 59 60 /** 61 * 1 snap target: minimized height, (1 - minimized height) 62 */ 63 private static final int SNAP_MODE_MINIMIZED = 3; 64 65 private final float mMinFlingVelocityPxPerSecond; 66 private final float mMinDismissVelocityPxPerSecond; 67 private final int mDisplayWidth; 68 private final int mDisplayHeight; 69 private final int mDividerSize; 70 private final ArrayList<SnapTarget> mTargets = new ArrayList<>(); 71 private final Rect mInsets = new Rect(); 72 private final int mSnapMode; 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 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 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); 108 } 109 110 public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize, 111 boolean isHorizontalDivision, Rect insets, int dockSide, boolean isMinimizedMode) { 112 mMinFlingVelocityPxPerSecond = 113 MIN_FLING_VELOCITY_DP_PER_SECOND * res.getDisplayMetrics().density; 114 mMinDismissVelocityPxPerSecond = 115 MIN_DISMISS_VELOCITY_DP_PER_SECOND * res.getDisplayMetrics().density; 116 mDividerSize = dividerSize; 117 mDisplayWidth = displayWidth; 118 mDisplayHeight = displayHeight; 119 mIsHorizontalDivision = isHorizontalDivision; 120 mInsets.set(insets); 121 mSnapMode = isMinimizedMode ? SNAP_MODE_MINIMIZED : 122 res.getInteger(com.android.internal.R.integer.config_dockedStackDividerSnapMode); 123 mFixedRatio = res.getFraction( 124 com.android.internal.R.fraction.docked_stack_divider_fixed_ratio, 1, 1); 125 mMinimalSizeResizableTask = res.getDimensionPixelSize( 126 com.android.internal.R.dimen.default_minimal_size_resizable_task); 127 mTaskHeightInMinimizedMode = res.getDimensionPixelSize( 128 com.android.internal.R.dimen.task_height_of_minimized_mode); 129 calculateTargets(isHorizontalDivision, dockSide); 130 mFirstSplitTarget = mTargets.get(1); 131 mLastSplitTarget = mTargets.get(mTargets.size() - 2); 132 mDismissStartTarget = mTargets.get(0); 133 mDismissEndTarget = mTargets.get(mTargets.size() - 1); 134 mMiddleTarget = mTargets.get(mTargets.size() / 2); 135 } 136 137 /** 138 * @return whether it's feasible to enable split screen in the current configuration, i.e. when 139 * snapping in the middle both tasks are larger than the minimal task size. 140 */ 141 public boolean isSplitScreenFeasible() { 142 int statusBarSize = mInsets.top; 143 int navBarSize = mIsHorizontalDivision ? mInsets.bottom : mInsets.right; 144 int size = mIsHorizontalDivision 145 ? mDisplayHeight 146 : mDisplayWidth; 147 int availableSpace = size - navBarSize - statusBarSize - mDividerSize; 148 return availableSpace / 2 >= mMinimalSizeResizableTask; 149 } 150 151 public SnapTarget calculateSnapTarget(int position, float velocity) { 152 return calculateSnapTarget(position, velocity, true /* hardDismiss */); 153 } 154 155 /** 156 * @param position the top/left position of the divider 157 * @param velocity current dragging velocity 158 * @param hardDismiss if set, make it a bit harder to get reach the dismiss targets 159 */ 160 public SnapTarget calculateSnapTarget(int position, float velocity, boolean hardDismiss) { 161 if (position < mFirstSplitTarget.position && velocity < -mMinDismissVelocityPxPerSecond) { 162 return mDismissStartTarget; 163 } 164 if (position > mLastSplitTarget.position && velocity > mMinDismissVelocityPxPerSecond) { 165 return mDismissEndTarget; 166 } 167 if (Math.abs(velocity) < mMinFlingVelocityPxPerSecond) { 168 return snap(position, hardDismiss); 169 } 170 if (velocity < 0) { 171 return mFirstSplitTarget; 172 } else { 173 return mLastSplitTarget; 174 } 175 } 176 177 public SnapTarget calculateNonDismissingSnapTarget(int position) { 178 SnapTarget target = snap(position, false /* hardDismiss */); 179 if (target == mDismissStartTarget) { 180 return mFirstSplitTarget; 181 } else if (target == mDismissEndTarget) { 182 return mLastSplitTarget; 183 } else { 184 return target; 185 } 186 } 187 188 public float calculateDismissingFraction(int position) { 189 if (position < mFirstSplitTarget.position) { 190 return 1f - (float) (position - getStartInset()) 191 / (mFirstSplitTarget.position - getStartInset()); 192 } else if (position > mLastSplitTarget.position) { 193 return (float) (position - mLastSplitTarget.position) 194 / (mDismissEndTarget.position - mLastSplitTarget.position - mDividerSize); 195 } 196 return 0f; 197 } 198 199 public SnapTarget getClosestDismissTarget(int position) { 200 if (position < mFirstSplitTarget.position) { 201 return mDismissStartTarget; 202 } else if (position > mLastSplitTarget.position) { 203 return mDismissEndTarget; 204 } else if (position - mDismissStartTarget.position 205 < mDismissEndTarget.position - position) { 206 return mDismissStartTarget; 207 } else { 208 return mDismissEndTarget; 209 } 210 } 211 212 public SnapTarget getFirstSplitTarget() { 213 return mFirstSplitTarget; 214 } 215 216 public SnapTarget getLastSplitTarget() { 217 return mLastSplitTarget; 218 } 219 220 public SnapTarget getDismissStartTarget() { 221 return mDismissStartTarget; 222 } 223 224 public SnapTarget getDismissEndTarget() { 225 return mDismissEndTarget; 226 } 227 228 private int getStartInset() { 229 if (mIsHorizontalDivision) { 230 return mInsets.top; 231 } else { 232 return mInsets.left; 233 } 234 } 235 236 private int getEndInset() { 237 if (mIsHorizontalDivision) { 238 return mInsets.bottom; 239 } else { 240 return mInsets.right; 241 } 242 } 243 244 private SnapTarget snap(int position, boolean hardDismiss) { 245 int minIndex = -1; 246 float minDistance = Float.MAX_VALUE; 247 int size = mTargets.size(); 248 for (int i = 0; i < size; i++) { 249 SnapTarget target = mTargets.get(i); 250 float distance = Math.abs(position - target.position); 251 if (hardDismiss) { 252 distance /= target.distanceMultiplier; 253 } 254 if (distance < minDistance) { 255 minIndex = i; 256 minDistance = distance; 257 } 258 } 259 return mTargets.get(minIndex); 260 } 261 262 private void calculateTargets(boolean isHorizontalDivision, int dockedSide) { 263 mTargets.clear(); 264 int dividerMax = isHorizontalDivision 265 ? mDisplayHeight 266 : mDisplayWidth; 267 int navBarSize = isHorizontalDivision ? mInsets.bottom : mInsets.right; 268 mTargets.add(new SnapTarget(-mDividerSize, -mDividerSize, SnapTarget.FLAG_DISMISS_START, 269 0.35f)); 270 switch (mSnapMode) { 271 case SNAP_MODE_16_9: 272 addRatio16_9Targets(isHorizontalDivision, dividerMax); 273 break; 274 case SNAP_FIXED_RATIO: 275 addFixedDivisionTargets(isHorizontalDivision, dividerMax); 276 break; 277 case SNAP_ONLY_1_1: 278 addMiddleTarget(isHorizontalDivision); 279 break; 280 case SNAP_MODE_MINIMIZED: 281 addMinimizedTarget(isHorizontalDivision, dockedSide); 282 break; 283 } 284 mTargets.add(new SnapTarget(dividerMax - navBarSize, dividerMax, 285 SnapTarget.FLAG_DISMISS_END, 0.35f)); 286 } 287 288 private void addNonDismissingTargets(boolean isHorizontalDivision, int topPosition, 289 int bottomPosition, int dividerMax) { 290 maybeAddTarget(topPosition, topPosition - mInsets.top); 291 addMiddleTarget(isHorizontalDivision); 292 maybeAddTarget(bottomPosition, dividerMax - mInsets.bottom 293 - (bottomPosition + mDividerSize)); 294 } 295 296 private void addFixedDivisionTargets(boolean isHorizontalDivision, int dividerMax) { 297 int start = isHorizontalDivision ? mInsets.top : mInsets.left; 298 int end = isHorizontalDivision 299 ? mDisplayHeight - mInsets.bottom 300 : mDisplayWidth - mInsets.right; 301 int size = (int) (mFixedRatio * (end - start)) - mDividerSize / 2; 302 int topPosition = start + size; 303 int bottomPosition = end - size - mDividerSize; 304 addNonDismissingTargets(isHorizontalDivision, topPosition, bottomPosition, dividerMax); 305 } 306 307 private void addRatio16_9Targets(boolean isHorizontalDivision, int dividerMax) { 308 int start = isHorizontalDivision ? mInsets.top : mInsets.left; 309 int end = isHorizontalDivision 310 ? mDisplayHeight - mInsets.bottom 311 : mDisplayWidth - mInsets.right; 312 int startOther = isHorizontalDivision ? mInsets.left : mInsets.top; 313 int endOther = isHorizontalDivision 314 ? mDisplayWidth - mInsets.right 315 : mDisplayHeight - mInsets.bottom; 316 float size = 9.0f / 16.0f * (endOther - startOther); 317 int sizeInt = (int) Math.floor(size); 318 int topPosition = start + sizeInt; 319 int bottomPosition = end - sizeInt - mDividerSize; 320 addNonDismissingTargets(isHorizontalDivision, topPosition, bottomPosition, dividerMax); 321 } 322 323 /** 324 * Adds a target at {@param position} but only if the area with size of {@param smallerSize} 325 * meets the minimal size requirement. 326 */ 327 private void maybeAddTarget(int position, int smallerSize) { 328 if (smallerSize >= mMinimalSizeResizableTask) { 329 mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE)); 330 } 331 } 332 333 private void addMiddleTarget(boolean isHorizontalDivision) { 334 int position = DockedDividerUtils.calculateMiddlePosition(isHorizontalDivision, 335 mInsets, mDisplayWidth, mDisplayHeight, mDividerSize); 336 mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE)); 337 } 338 339 private void addMinimizedTarget(boolean isHorizontalDivision, int dockedSide) { 340 // In portrait offset the position by the statusbar height, in landscape add the statusbar 341 // height as well to match portrait offset 342 int position = mTaskHeightInMinimizedMode + mInsets.top; 343 if (!isHorizontalDivision) { 344 if (dockedSide == DOCKED_LEFT) { 345 position += mInsets.left; 346 } else if (dockedSide == DOCKED_RIGHT) { 347 position = mDisplayWidth - position - mInsets.right - mDividerSize; 348 } 349 } 350 mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE)); 351 } 352 353 public SnapTarget getMiddleTarget() { 354 return mMiddleTarget; 355 } 356 357 public SnapTarget getNextTarget(SnapTarget snapTarget) { 358 int index = mTargets.indexOf(snapTarget); 359 if (index != -1 && index < mTargets.size() - 1) { 360 return mTargets.get(index + 1); 361 } 362 return snapTarget; 363 } 364 365 public SnapTarget getPreviousTarget(SnapTarget snapTarget) { 366 int index = mTargets.indexOf(snapTarget); 367 if (index != -1 && index > 0) { 368 return mTargets.get(index - 1); 369 } 370 return snapTarget; 371 } 372 373 public boolean isFirstSplitTargetAvailable() { 374 return mFirstSplitTarget != mMiddleTarget; 375 } 376 377 public boolean isLastSplitTargetAvailable() { 378 return mLastSplitTarget != mMiddleTarget; 379 } 380 381 /** 382 * Cycles through all non-dismiss targets with a stepping of {@param increment}. It moves left 383 * if {@param increment} is negative and moves right otherwise. 384 */ 385 public SnapTarget cycleNonDismissTarget(SnapTarget snapTarget, int increment) { 386 int index = mTargets.indexOf(snapTarget); 387 if (index != -1) { 388 SnapTarget newTarget = mTargets.get((index + mTargets.size() + increment) 389 % mTargets.size()); 390 if (newTarget == mDismissStartTarget) { 391 return mLastSplitTarget; 392 } else if (newTarget == mDismissEndTarget) { 393 return mFirstSplitTarget; 394 } else { 395 return newTarget; 396 } 397 } 398 return snapTarget; 399 } 400 401 /** 402 * Represents a snap target for the divider. 403 */ 404 public static class SnapTarget { 405 public static final int FLAG_NONE = 0; 406 407 /** If the divider reaches this value, the left/top task should be dismissed. */ 408 public static final int FLAG_DISMISS_START = 1; 409 410 /** If the divider reaches this value, the right/bottom task should be dismissed */ 411 public static final int FLAG_DISMISS_END = 2; 412 413 /** Position of this snap target. The right/bottom edge of the top/left task snaps here. */ 414 public final int position; 415 416 /** 417 * Like {@link #position}, but used to calculate the task bounds which might be different 418 * from the stack bounds. 419 */ 420 public final int taskPosition; 421 422 public final int flag; 423 424 /** 425 * Multiplier used to calculate distance to snap position. The lower this value, the harder 426 * it's to snap on this target 427 */ 428 private final float distanceMultiplier; 429 430 public SnapTarget(int position, int taskPosition, int flag) { 431 this(position, taskPosition, flag, 1f); 432 } 433 434 public SnapTarget(int position, int taskPosition, int flag, float distanceMultiplier) { 435 this.position = position; 436 this.taskPosition = taskPosition; 437 this.flag = flag; 438 this.distanceMultiplier = distanceMultiplier; 439 } 440 } 441} 442