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