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