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 android.content.Context; 20 import android.content.res.Configuration; 21 import android.content.res.Resources; 22 import android.graphics.Rect; 23 import android.hardware.display.DisplayManager; 24 import android.util.Log; 25 import android.view.Display; 26 import android.view.DisplayInfo; 27 28 import 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 */ 36 public 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 create(Context ctx, Rect insets)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 DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize, boolean isHorizontalDivision, Rect insets)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 DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize, boolean isHorizontalDivision, Rect insets, boolean isMinimizedMode)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 */ isSplitScreenFeasible()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 calculateSnapTarget(int position, float velocity)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 */ calculateSnapTarget(int position, float velocity, boolean hardDismiss)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 calculateNonDismissingSnapTarget(int position)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 calculateDismissingFraction(int position)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 getClosestDismissTarget(int position)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 getFirstSplitTarget()207 public SnapTarget getFirstSplitTarget() { 208 return mFirstSplitTarget; 209 } 210 getLastSplitTarget()211 public SnapTarget getLastSplitTarget() { 212 return mLastSplitTarget; 213 } 214 getDismissStartTarget()215 public SnapTarget getDismissStartTarget() { 216 return mDismissStartTarget; 217 } 218 getDismissEndTarget()219 public SnapTarget getDismissEndTarget() { 220 return mDismissEndTarget; 221 } 222 getStartInset()223 private int getStartInset() { 224 if (mIsHorizontalDivision) { 225 return mInsets.top; 226 } else { 227 return mInsets.left; 228 } 229 } 230 getEndInset()231 private int getEndInset() { 232 if (mIsHorizontalDivision) { 233 return mInsets.bottom; 234 } else { 235 return mInsets.right; 236 } 237 } 238 snap(int position, boolean hardDismiss)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 calculateTargets(boolean isHorizontalDivision)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 addNonDismissingTargets(boolean isHorizontalDivision, int topPosition, int bottomPosition, int dividerMax)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 addFixedDivisionTargets(boolean isHorizontalDivision, int dividerMax)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 addRatio16_9Targets(boolean isHorizontalDivision, int dividerMax)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 */ maybeAddTarget(int position, int smallerSize)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 addMiddleTarget(boolean isHorizontalDivision)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 addMinimizedTarget(boolean isHorizontalDivision)334 private void addMinimizedTarget(boolean isHorizontalDivision) { 335 // In portrait offset the position by the statusbar height, in landscape add the statusbar 336 // height as well to match portrait offset 337 int position = mTaskHeightInMinimizedMode + mInsets.top; 338 if (!isHorizontalDivision) { 339 position += mInsets.left; 340 } 341 mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE)); 342 } 343 getMiddleTarget()344 public SnapTarget getMiddleTarget() { 345 return mMiddleTarget; 346 } 347 getNextTarget(SnapTarget snapTarget)348 public SnapTarget getNextTarget(SnapTarget snapTarget) { 349 int index = mTargets.indexOf(snapTarget); 350 if (index != -1 && index < mTargets.size() - 1) { 351 return mTargets.get(index + 1); 352 } 353 return snapTarget; 354 } 355 getPreviousTarget(SnapTarget snapTarget)356 public SnapTarget getPreviousTarget(SnapTarget snapTarget) { 357 int index = mTargets.indexOf(snapTarget); 358 if (index != -1 && index > 0) { 359 return mTargets.get(index - 1); 360 } 361 return snapTarget; 362 } 363 isFirstSplitTargetAvailable()364 public boolean isFirstSplitTargetAvailable() { 365 return mFirstSplitTarget != mMiddleTarget; 366 } 367 isLastSplitTargetAvailable()368 public boolean isLastSplitTargetAvailable() { 369 return mLastSplitTarget != mMiddleTarget; 370 } 371 372 /** 373 * Cycles through all non-dismiss targets with a stepping of {@param increment}. It moves left 374 * if {@param increment} is negative and moves right otherwise. 375 */ cycleNonDismissTarget(SnapTarget snapTarget, int increment)376 public SnapTarget cycleNonDismissTarget(SnapTarget snapTarget, int increment) { 377 int index = mTargets.indexOf(snapTarget); 378 if (index != -1) { 379 SnapTarget newTarget = mTargets.get((index + mTargets.size() + increment) 380 % mTargets.size()); 381 if (newTarget == mDismissStartTarget) { 382 return mLastSplitTarget; 383 } else if (newTarget == mDismissEndTarget) { 384 return mFirstSplitTarget; 385 } else { 386 return newTarget; 387 } 388 } 389 return snapTarget; 390 } 391 392 /** 393 * Represents a snap target for the divider. 394 */ 395 public static class SnapTarget { 396 public static final int FLAG_NONE = 0; 397 398 /** If the divider reaches this value, the left/top task should be dismissed. */ 399 public static final int FLAG_DISMISS_START = 1; 400 401 /** If the divider reaches this value, the right/bottom task should be dismissed */ 402 public static final int FLAG_DISMISS_END = 2; 403 404 /** Position of this snap target. The right/bottom edge of the top/left task snaps here. */ 405 public final int position; 406 407 /** 408 * Like {@link #position}, but used to calculate the task bounds which might be different 409 * from the stack bounds. 410 */ 411 public final int taskPosition; 412 413 public final int flag; 414 415 /** 416 * Multiplier used to calculate distance to snap position. The lower this value, the harder 417 * it's to snap on this target 418 */ 419 private final float distanceMultiplier; 420 SnapTarget(int position, int taskPosition, int flag)421 public SnapTarget(int position, int taskPosition, int flag) { 422 this(position, taskPosition, flag, 1f); 423 } 424 SnapTarget(int position, int taskPosition, int flag, float distanceMultiplier)425 public SnapTarget(int position, int taskPosition, int flag, float distanceMultiplier) { 426 this.position = position; 427 this.taskPosition = taskPosition; 428 this.flag = flag; 429 this.distanceMultiplier = distanceMultiplier; 430 } 431 } 432 } 433