1 /* 2 * Copyright (C) 2020 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.wm.shell.common.split; 18 19 import static android.content.res.Configuration.SCREEN_HEIGHT_DP_UNDEFINED; 20 import static android.content.res.Configuration.SCREEN_WIDTH_DP_UNDEFINED; 21 import static android.view.WindowManager.DOCKED_BOTTOM; 22 import static android.view.WindowManager.DOCKED_INVALID; 23 import static android.view.WindowManager.DOCKED_LEFT; 24 import static android.view.WindowManager.DOCKED_RIGHT; 25 import static android.view.WindowManager.DOCKED_TOP; 26 27 import static com.android.internal.jank.InteractionJankMonitor.CUJ_SPLIT_SCREEN_DOUBLE_TAP_DIVIDER; 28 import static com.android.internal.jank.InteractionJankMonitor.CUJ_SPLIT_SCREEN_RESIZE; 29 import static com.android.wm.shell.animation.Interpolators.DIM_INTERPOLATOR; 30 import static com.android.wm.shell.animation.Interpolators.SLOWDOWN_INTERPOLATOR; 31 import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_END_AND_DISMISS; 32 import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_START_AND_DISMISS; 33 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; 34 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; 35 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; 36 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER; 37 38 import android.animation.Animator; 39 import android.animation.AnimatorListenerAdapter; 40 import android.animation.AnimatorSet; 41 import android.animation.ValueAnimator; 42 import android.annotation.NonNull; 43 import android.app.ActivityManager; 44 import android.content.Context; 45 import android.content.res.Configuration; 46 import android.content.res.Resources; 47 import android.graphics.Point; 48 import android.graphics.Rect; 49 import android.view.Display; 50 import android.view.InsetsSourceControl; 51 import android.view.InsetsState; 52 import android.view.RoundedCorner; 53 import android.view.SurfaceControl; 54 import android.view.WindowInsets; 55 import android.view.WindowManager; 56 import android.window.WindowContainerToken; 57 import android.window.WindowContainerTransaction; 58 59 import androidx.annotation.Nullable; 60 61 import com.android.internal.annotations.VisibleForTesting; 62 import com.android.internal.protolog.common.ProtoLog; 63 import com.android.wm.shell.R; 64 import com.android.wm.shell.ShellTaskOrganizer; 65 import com.android.wm.shell.animation.Interpolators; 66 import com.android.wm.shell.common.DisplayController; 67 import com.android.wm.shell.common.DisplayImeController; 68 import com.android.wm.shell.common.DisplayInsetsController; 69 import com.android.wm.shell.common.DisplayLayout; 70 import com.android.wm.shell.common.InteractionJankMonitorUtils; 71 import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition; 72 import com.android.wm.shell.common.split.SplitScreenConstants.SnapPosition; 73 import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; 74 import com.android.wm.shell.protolog.ShellProtoLogGroup; 75 76 import java.io.PrintWriter; 77 import java.util.function.Consumer; 78 79 /** 80 * Records and handles layout of splits. Helps to calculate proper bounds when configuration or 81 * divider position changes. 82 */ 83 public final class SplitLayout implements DisplayInsetsController.OnInsetsChangedListener { 84 private static final String TAG = "SplitLayout"; 85 public static final int PARALLAX_NONE = 0; 86 public static final int PARALLAX_DISMISSING = 1; 87 public static final int PARALLAX_ALIGN_CENTER = 2; 88 89 public static final int FLING_RESIZE_DURATION = 250; 90 private static final int FLING_SWITCH_DURATION = 350; 91 private static final int FLING_ENTER_DURATION = 450; 92 private static final int FLING_EXIT_DURATION = 450; 93 94 private int mDividerWindowWidth; 95 private int mDividerInsets; 96 private int mDividerSize; 97 98 private final Rect mTempRect = new Rect(); 99 private final Rect mRootBounds = new Rect(); 100 private final Rect mDividerBounds = new Rect(); 101 // Bounds1 final position should be always at top or left 102 private final Rect mBounds1 = new Rect(); 103 // Bounds2 final position should be always at bottom or right 104 private final Rect mBounds2 = new Rect(); 105 // The temp bounds outside of display bounds for side stage when split screen inactive to avoid 106 // flicker next time active split screen. 107 private final Rect mInvisibleBounds = new Rect(); 108 private final Rect mWinBounds1 = new Rect(); 109 private final Rect mWinBounds2 = new Rect(); 110 private final SplitLayoutHandler mSplitLayoutHandler; 111 private final SplitWindowManager mSplitWindowManager; 112 private final DisplayController mDisplayController; 113 private final DisplayImeController mDisplayImeController; 114 private final ImePositionProcessor mImePositionProcessor; 115 private final ResizingEffectPolicy mSurfaceEffectPolicy; 116 private final ShellTaskOrganizer mTaskOrganizer; 117 private final InsetsState mInsetsState = new InsetsState(); 118 119 private Context mContext; 120 @VisibleForTesting DividerSnapAlgorithm mDividerSnapAlgorithm; 121 private WindowContainerToken mWinToken1; 122 private WindowContainerToken mWinToken2; 123 private int mDividerPosition; 124 private boolean mInitialized = false; 125 private boolean mFreezeDividerWindow = false; 126 private boolean mIsLargeScreen = false; 127 private int mOrientation; 128 private int mRotation; 129 private int mDensity; 130 private int mUiMode; 131 132 private final boolean mDimNonImeSide; 133 private final boolean mAllowLeftRightSplitInPortrait; 134 private boolean mIsLeftRightSplit; 135 private ValueAnimator mDividerFlingAnimator; 136 SplitLayout(String windowName, Context context, Configuration configuration, SplitLayoutHandler splitLayoutHandler, SplitWindowManager.ParentContainerCallbacks parentContainerCallbacks, DisplayController displayController, DisplayImeController displayImeController, ShellTaskOrganizer taskOrganizer, int parallaxType)137 public SplitLayout(String windowName, Context context, Configuration configuration, 138 SplitLayoutHandler splitLayoutHandler, 139 SplitWindowManager.ParentContainerCallbacks parentContainerCallbacks, 140 DisplayController displayController, DisplayImeController displayImeController, 141 ShellTaskOrganizer taskOrganizer, int parallaxType) { 142 mContext = context.createConfigurationContext(configuration); 143 mOrientation = configuration.orientation; 144 mRotation = configuration.windowConfiguration.getRotation(); 145 mDensity = configuration.densityDpi; 146 mIsLargeScreen = configuration.smallestScreenWidthDp >= 600; 147 mSplitLayoutHandler = splitLayoutHandler; 148 mDisplayController = displayController; 149 mDisplayImeController = displayImeController; 150 mSplitWindowManager = new SplitWindowManager(windowName, mContext, configuration, 151 parentContainerCallbacks); 152 mTaskOrganizer = taskOrganizer; 153 mImePositionProcessor = new ImePositionProcessor(mContext.getDisplayId()); 154 mSurfaceEffectPolicy = new ResizingEffectPolicy(parallaxType); 155 156 final Resources res = mContext.getResources(); 157 mDimNonImeSide = res.getBoolean(R.bool.config_dimNonImeAttachedSide); 158 mAllowLeftRightSplitInPortrait = SplitScreenUtils.allowLeftRightSplitInPortrait(res); 159 mIsLeftRightSplit = SplitScreenUtils.isLeftRightSplit(mAllowLeftRightSplitInPortrait, 160 configuration); 161 162 updateDividerConfig(mContext); 163 164 mRootBounds.set(configuration.windowConfiguration.getBounds()); 165 mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds); 166 resetDividerPosition(); 167 updateInvisibleRect(); 168 } 169 updateDividerConfig(Context context)170 private void updateDividerConfig(Context context) { 171 final Resources resources = context.getResources(); 172 final Display display = context.getDisplay(); 173 final int dividerInset = resources.getDimensionPixelSize( 174 com.android.internal.R.dimen.docked_stack_divider_insets); 175 int radius = 0; 176 RoundedCorner corner = display.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT); 177 radius = corner != null ? Math.max(radius, corner.getRadius()) : radius; 178 corner = display.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT); 179 radius = corner != null ? Math.max(radius, corner.getRadius()) : radius; 180 corner = display.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT); 181 radius = corner != null ? Math.max(radius, corner.getRadius()) : radius; 182 corner = display.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT); 183 radius = corner != null ? Math.max(radius, corner.getRadius()) : radius; 184 185 mDividerInsets = Math.max(dividerInset, radius); 186 mDividerSize = resources.getDimensionPixelSize(R.dimen.split_divider_bar_width); 187 mDividerWindowWidth = mDividerSize + 2 * mDividerInsets; 188 } 189 190 /** Gets bounds of the primary split with screen based coordinate. */ getBounds1()191 public Rect getBounds1() { 192 return new Rect(mBounds1); 193 } 194 195 /** Gets bounds of the primary split with parent based coordinate. */ getRefBounds1()196 public Rect getRefBounds1() { 197 Rect outBounds = getBounds1(); 198 outBounds.offset(-mRootBounds.left, -mRootBounds.top); 199 return outBounds; 200 } 201 202 /** Gets bounds of the secondary split with screen based coordinate. */ getBounds2()203 public Rect getBounds2() { 204 return new Rect(mBounds2); 205 } 206 207 /** Gets bounds of the secondary split with parent based coordinate. */ getRefBounds2()208 public Rect getRefBounds2() { 209 final Rect outBounds = getBounds2(); 210 outBounds.offset(-mRootBounds.left, -mRootBounds.top); 211 return outBounds; 212 } 213 214 /** Gets root bounds of the whole split layout */ getRootBounds()215 public Rect getRootBounds() { 216 return new Rect(mRootBounds); 217 } 218 219 /** Gets bounds of divider window with screen based coordinate. */ getDividerBounds()220 public Rect getDividerBounds() { 221 return new Rect(mDividerBounds); 222 } 223 224 /** Gets bounds of divider window with parent based coordinate. */ getRefDividerBounds()225 public Rect getRefDividerBounds() { 226 final Rect outBounds = getDividerBounds(); 227 outBounds.offset(-mRootBounds.left, -mRootBounds.top); 228 return outBounds; 229 } 230 231 /** Gets bounds of the primary split with screen based coordinate on the param Rect. */ getBounds1(Rect rect)232 public void getBounds1(Rect rect) { 233 rect.set(mBounds1); 234 } 235 236 /** Gets bounds of the primary split with parent based coordinate on the param Rect. */ getRefBounds1(Rect rect)237 public void getRefBounds1(Rect rect) { 238 getBounds1(rect); 239 rect.offset(-mRootBounds.left, -mRootBounds.top); 240 } 241 242 /** Gets bounds of the secondary split with screen based coordinate on the param Rect. */ getBounds2(Rect rect)243 public void getBounds2(Rect rect) { 244 rect.set(mBounds2); 245 } 246 247 /** Gets bounds of the secondary split with parent based coordinate on the param Rect. */ getRefBounds2(Rect rect)248 public void getRefBounds2(Rect rect) { 249 getBounds2(rect); 250 rect.offset(-mRootBounds.left, -mRootBounds.top); 251 } 252 253 /** Gets root bounds of the whole split layout on the param Rect. */ getRootBounds(Rect rect)254 public void getRootBounds(Rect rect) { 255 rect.set(mRootBounds); 256 } 257 258 /** Gets bounds of divider window with screen based coordinate on the param Rect. */ getDividerBounds(Rect rect)259 public void getDividerBounds(Rect rect) { 260 rect.set(mDividerBounds); 261 } 262 263 /** Gets bounds of divider window with parent based coordinate on the param Rect. */ getRefDividerBounds(Rect rect)264 public void getRefDividerBounds(Rect rect) { 265 getDividerBounds(rect); 266 rect.offset(-mRootBounds.left, -mRootBounds.top); 267 } 268 269 /** Gets bounds size equal to root bounds but outside of screen, used for position side stage 270 * when split inactive to avoid flicker when next time active. */ getInvisibleBounds(Rect rect)271 public void getInvisibleBounds(Rect rect) { 272 rect.set(mInvisibleBounds); 273 } 274 275 /** Returns leash of the current divider bar. */ 276 @Nullable getDividerLeash()277 public SurfaceControl getDividerLeash() { 278 return mSplitWindowManager == null ? null : mSplitWindowManager.getSurfaceControl(); 279 } 280 getDividerPosition()281 int getDividerPosition() { 282 return mDividerPosition; 283 } 284 285 /** 286 * Finds the {@link SnapPosition} nearest to the current divider position. 287 */ calculateCurrentSnapPosition()288 public int calculateCurrentSnapPosition() { 289 return mDividerSnapAlgorithm.calculateNearestSnapPosition(mDividerPosition); 290 } 291 292 /** 293 * Returns the divider position as a fraction from 0 to 1. 294 */ getDividerPositionAsFraction()295 public float getDividerPositionAsFraction() { 296 return Math.min(1f, Math.max(0f, mIsLeftRightSplit 297 ? (float) ((mBounds1.right + mBounds2.left) / 2f) / mBounds2.right 298 : (float) ((mBounds1.bottom + mBounds2.top) / 2f) / mBounds2.bottom)); 299 } 300 updateInvisibleRect()301 private void updateInvisibleRect() { 302 mInvisibleBounds.set(mRootBounds.left, mRootBounds.top, 303 mIsLeftRightSplit ? mRootBounds.right / 2 : mRootBounds.right, 304 mIsLeftRightSplit ? mRootBounds.bottom : mRootBounds.bottom / 2); 305 mInvisibleBounds.offset(mIsLeftRightSplit ? mRootBounds.right : 0, 306 mIsLeftRightSplit ? 0 : mRootBounds.bottom); 307 } 308 309 /** Applies new configuration, returns {@code false} if there's no effect to the layout. */ updateConfiguration(Configuration configuration)310 public boolean updateConfiguration(Configuration configuration) { 311 // Update the split bounds when necessary. Besides root bounds changed, split bounds need to 312 // be updated when the rotation changed to cover the case that users rotated the screen 180 313 // degrees. 314 // Make sure to render the divider bar with proper resources that matching the screen 315 // orientation. 316 final int rotation = configuration.windowConfiguration.getRotation(); 317 final Rect rootBounds = configuration.windowConfiguration.getBounds(); 318 final int orientation = configuration.orientation; 319 final int density = configuration.densityDpi; 320 final int uiMode = configuration.uiMode; 321 final boolean wasLeftRightSplit = mIsLeftRightSplit; 322 323 if (mOrientation == orientation 324 && mRotation == rotation 325 && mDensity == density 326 && mUiMode == uiMode 327 && mRootBounds.equals(rootBounds)) { 328 return false; 329 } 330 331 mContext = mContext.createConfigurationContext(configuration); 332 mSplitWindowManager.setConfiguration(configuration); 333 mOrientation = orientation; 334 mTempRect.set(mRootBounds); 335 mRootBounds.set(rootBounds); 336 mRotation = rotation; 337 mDensity = density; 338 mUiMode = uiMode; 339 mIsLargeScreen = configuration.smallestScreenWidthDp >= 600; 340 mIsLeftRightSplit = SplitScreenUtils.isLeftRightSplit(mAllowLeftRightSplitInPortrait, 341 configuration); 342 mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds); 343 updateDividerConfig(mContext); 344 initDividerPosition(mTempRect, wasLeftRightSplit); 345 updateInvisibleRect(); 346 347 return true; 348 } 349 350 /** Rotate the layout to specific rotation and calculate new bounds. The stable insets value 351 * should be calculated by display layout. */ rotateTo(int newRotation)352 public void rotateTo(int newRotation) { 353 final int rotationDelta = (newRotation - mRotation + 4) % 4; 354 final boolean changeOrient = (rotationDelta % 2) != 0; 355 356 mRotation = newRotation; 357 Rect tmpRect = new Rect(mRootBounds); 358 if (changeOrient) { 359 tmpRect.set(mRootBounds.top, mRootBounds.left, mRootBounds.bottom, mRootBounds.right); 360 } 361 362 // We only need new bounds here, other configuration should be update later. 363 final boolean wasLeftRightSplit = SplitScreenUtils.isLeftRightSplit( 364 mAllowLeftRightSplitInPortrait, mIsLargeScreen, 365 mRootBounds.width() >= mRootBounds.height()); 366 mTempRect.set(mRootBounds); 367 mRootBounds.set(tmpRect); 368 mIsLeftRightSplit = SplitScreenUtils.isLeftRightSplit(mAllowLeftRightSplitInPortrait, 369 mIsLargeScreen, mRootBounds.width() >= mRootBounds.height()); 370 mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds); 371 initDividerPosition(mTempRect, wasLeftRightSplit); 372 } 373 374 /** 375 * Updates the divider position to the position in the current orientation and bounds using the 376 * snap fraction calculated based on the previous orientation and bounds. 377 */ initDividerPosition(Rect oldBounds, boolean wasLeftRightSplit)378 private void initDividerPosition(Rect oldBounds, boolean wasLeftRightSplit) { 379 final float snapRatio = (float) mDividerPosition 380 / (float) (wasLeftRightSplit ? oldBounds.width() : oldBounds.height()); 381 // Estimate position by previous ratio. 382 final float length = 383 (float) (mIsLeftRightSplit ? mRootBounds.width() : mRootBounds.height()); 384 final int estimatePosition = (int) (length * snapRatio); 385 // Init divider position by estimated position using current bounds snap algorithm. 386 mDividerPosition = mDividerSnapAlgorithm.calculateNonDismissingSnapTarget( 387 estimatePosition).position; 388 updateBounds(mDividerPosition); 389 } 390 updateBounds(int position)391 private void updateBounds(int position) { 392 updateBounds(position, mBounds1, mBounds2, mDividerBounds, true /* setEffectBounds */); 393 } 394 395 /** Updates recording bounds of divider window and both of the splits. */ updateBounds(int position, Rect bounds1, Rect bounds2, Rect dividerBounds, boolean setEffectBounds)396 private void updateBounds(int position, Rect bounds1, Rect bounds2, Rect dividerBounds, 397 boolean setEffectBounds) { 398 dividerBounds.set(mRootBounds); 399 bounds1.set(mRootBounds); 400 bounds2.set(mRootBounds); 401 if (mIsLeftRightSplit) { 402 position += mRootBounds.left; 403 dividerBounds.left = position - mDividerInsets; 404 dividerBounds.right = dividerBounds.left + mDividerWindowWidth; 405 bounds1.right = position; 406 bounds2.left = bounds1.right + mDividerSize; 407 } else { 408 position += mRootBounds.top; 409 dividerBounds.top = position - mDividerInsets; 410 dividerBounds.bottom = dividerBounds.top + mDividerWindowWidth; 411 bounds1.bottom = position; 412 bounds2.top = bounds1.bottom + mDividerSize; 413 } 414 DockedDividerUtils.sanitizeStackBounds(bounds1, true /** topLeft */); 415 DockedDividerUtils.sanitizeStackBounds(bounds2, false /** topLeft */); 416 if (setEffectBounds) { 417 mSurfaceEffectPolicy.applyDividerPosition(position, mIsLeftRightSplit); 418 } 419 } 420 421 /** Inflates {@link DividerView} on the root surface. */ init()422 public void init() { 423 if (mInitialized) return; 424 mInitialized = true; 425 mSplitWindowManager.init(this, mInsetsState, false /* isRestoring */); 426 mDisplayImeController.addPositionProcessor(mImePositionProcessor); 427 } 428 429 /** Releases the surface holding the current {@link DividerView}. */ release(SurfaceControl.Transaction t)430 public void release(SurfaceControl.Transaction t) { 431 if (!mInitialized) return; 432 mInitialized = false; 433 mSplitWindowManager.release(t); 434 mDisplayImeController.removePositionProcessor(mImePositionProcessor); 435 mImePositionProcessor.reset(); 436 if (mDividerFlingAnimator != null) { 437 mDividerFlingAnimator.cancel(); 438 } 439 resetDividerPosition(); 440 } 441 release()442 public void release() { 443 release(null /* t */); 444 } 445 446 /** Releases and re-inflates {@link DividerView} on the root surface. */ update(SurfaceControl.Transaction t, boolean resetImePosition)447 public void update(SurfaceControl.Transaction t, boolean resetImePosition) { 448 if (!mInitialized) { 449 init(); 450 return; 451 } 452 mSplitWindowManager.release(t); 453 if (resetImePosition) { 454 mImePositionProcessor.reset(); 455 } 456 mSplitWindowManager.init(this, mInsetsState, true /* isRestoring */); 457 // Update the surface positions again after recreating the divider in case nothing else 458 // triggers it 459 mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this); 460 } 461 462 @Override insetsChanged(InsetsState insetsState)463 public void insetsChanged(InsetsState insetsState) { 464 mInsetsState.set(insetsState); 465 if (!mInitialized) { 466 return; 467 } 468 if (mFreezeDividerWindow) { 469 // DO NOT change its layout before transition actually run because it might cause 470 // flicker. 471 return; 472 } 473 mSplitWindowManager.onInsetsChanged(insetsState); 474 } 475 476 @Override insetsControlChanged(InsetsState insetsState, InsetsSourceControl[] activeControls)477 public void insetsControlChanged(InsetsState insetsState, 478 InsetsSourceControl[] activeControls) { 479 if (!mInsetsState.equals(insetsState)) { 480 insetsChanged(insetsState); 481 } 482 } 483 setFreezeDividerWindow(boolean freezeDividerWindow)484 public void setFreezeDividerWindow(boolean freezeDividerWindow) { 485 mFreezeDividerWindow = freezeDividerWindow; 486 } 487 488 /** Update current layout as divider put on start or end position. */ setDividerAtBorder(boolean start)489 public void setDividerAtBorder(boolean start) { 490 final int pos = start ? mDividerSnapAlgorithm.getDismissStartTarget().position 491 : mDividerSnapAlgorithm.getDismissEndTarget().position; 492 setDividerPosition(pos, false /* applyLayoutChange */); 493 } 494 495 /** 496 * Updates bounds with the passing position. Usually used to update recording bounds while 497 * performing animation or dragging divider bar to resize the splits. 498 */ updateDividerBounds(int position, boolean shouldUseParallaxEffect)499 void updateDividerBounds(int position, boolean shouldUseParallaxEffect) { 500 updateBounds(position); 501 mSplitLayoutHandler.onLayoutSizeChanging(this, mSurfaceEffectPolicy.mParallaxOffset.x, 502 mSurfaceEffectPolicy.mParallaxOffset.y, shouldUseParallaxEffect); 503 } 504 setDividerPosition(int position, boolean applyLayoutChange)505 void setDividerPosition(int position, boolean applyLayoutChange) { 506 mDividerPosition = position; 507 updateBounds(mDividerPosition); 508 if (applyLayoutChange) { 509 mSplitLayoutHandler.onLayoutSizeChanged(this); 510 } 511 } 512 513 /** 514 * Updates divider position and split bounds base on the ratio within root bounds. Falls back 515 * to middle position if the provided SnapTarget is not supported. 516 */ setDivideRatio(@ersistentSnapPosition int snapPosition)517 public void setDivideRatio(@PersistentSnapPosition int snapPosition) { 518 final DividerSnapAlgorithm.SnapTarget snapTarget = mDividerSnapAlgorithm.findSnapTarget( 519 snapPosition); 520 521 setDividerPosition(snapTarget != null 522 ? snapTarget.position 523 : mDividerSnapAlgorithm.getMiddleTarget().position, 524 false /* applyLayoutChange */); 525 } 526 527 /** Resets divider position. */ resetDividerPosition()528 public void resetDividerPosition() { 529 mDividerPosition = mDividerSnapAlgorithm.getMiddleTarget().position; 530 updateBounds(mDividerPosition); 531 mWinToken1 = null; 532 mWinToken2 = null; 533 mWinBounds1.setEmpty(); 534 mWinBounds2.setEmpty(); 535 } 536 537 /** 538 * Set divider should interactive to user or not. 539 * 540 * @param interactive divider interactive. 541 * @param hideHandle divider handle hidden or not, only work when interactive is false. 542 * @param from caller from where. 543 */ setDividerInteractive(boolean interactive, boolean hideHandle, String from)544 public void setDividerInteractive(boolean interactive, boolean hideHandle, String from) { 545 mSplitWindowManager.setInteractive(interactive, hideHandle, from); 546 } 547 548 /** 549 * Sets new divider position and updates bounds correspondingly. Notifies listener if the new 550 * target indicates dismissing split. 551 */ snapToTarget(int currentPosition, DividerSnapAlgorithm.SnapTarget snapTarget)552 public void snapToTarget(int currentPosition, DividerSnapAlgorithm.SnapTarget snapTarget) { 553 switch (snapTarget.snapPosition) { 554 case SNAP_TO_START_AND_DISMISS: 555 flingDividerPosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION, 556 () -> mSplitLayoutHandler.onSnappedToDismiss(false /* bottomOrRight */, 557 EXIT_REASON_DRAG_DIVIDER)); 558 break; 559 case SNAP_TO_END_AND_DISMISS: 560 flingDividerPosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION, 561 () -> mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */, 562 EXIT_REASON_DRAG_DIVIDER)); 563 break; 564 default: 565 flingDividerPosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION, 566 () -> setDividerPosition(snapTarget.position, true /* applyLayoutChange */)); 567 break; 568 } 569 } 570 onStartDragging()571 void onStartDragging() { 572 InteractionJankMonitorUtils.beginTracing(CUJ_SPLIT_SCREEN_RESIZE, mContext, 573 getDividerLeash(), null /* tag */); 574 } 575 onDraggingCancelled()576 void onDraggingCancelled() { 577 InteractionJankMonitorUtils.cancelTracing(CUJ_SPLIT_SCREEN_RESIZE); 578 } 579 onDoubleTappedDivider()580 void onDoubleTappedDivider() { 581 mSplitLayoutHandler.onDoubleTappedDivider(); 582 } 583 584 /** 585 * Returns {@link DividerSnapAlgorithm.SnapTarget} which matches passing position and velocity. 586 * If hardDismiss is set to {@code true}, it will be harder to reach dismiss target. 587 */ findSnapTarget(int position, float velocity, boolean hardDismiss)588 public DividerSnapAlgorithm.SnapTarget findSnapTarget(int position, float velocity, 589 boolean hardDismiss) { 590 return mDividerSnapAlgorithm.calculateSnapTarget(position, velocity, hardDismiss); 591 } 592 getSnapAlgorithm(Context context, Rect rootBounds)593 private DividerSnapAlgorithm getSnapAlgorithm(Context context, Rect rootBounds) { 594 final Rect insets = getDisplayStableInsets(context); 595 596 // Make split axis insets value same as the larger one to avoid bounds1 and bounds2 597 // have difference for avoiding size-compat mode when switching unresizable apps in 598 // landscape while they are letterboxed. 599 if (!mIsLeftRightSplit) { 600 final int largerInsets = Math.max(insets.top, insets.bottom); 601 insets.set(insets.left, largerInsets, insets.right, largerInsets); 602 } 603 604 return new DividerSnapAlgorithm( 605 context.getResources(), 606 rootBounds.width(), 607 rootBounds.height(), 608 mDividerSize, 609 !mIsLeftRightSplit, 610 insets, 611 mIsLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP /* dockSide */); 612 } 613 614 /** Fling divider from current position to end or start position then exit */ flingDividerToDismiss(boolean toEnd, int reason)615 public void flingDividerToDismiss(boolean toEnd, int reason) { 616 final int target = toEnd ? mDividerSnapAlgorithm.getDismissEndTarget().position 617 : mDividerSnapAlgorithm.getDismissStartTarget().position; 618 flingDividerPosition(getDividerPosition(), target, FLING_EXIT_DURATION, 619 () -> mSplitLayoutHandler.onSnappedToDismiss(toEnd, reason)); 620 } 621 622 /** Fling divider from current position to center position. */ flingDividerToCenter(@ullable Runnable finishCallback)623 public void flingDividerToCenter(@Nullable Runnable finishCallback) { 624 final int pos = mDividerSnapAlgorithm.getMiddleTarget().position; 625 flingDividerPosition(getDividerPosition(), pos, FLING_ENTER_DURATION, 626 () -> { 627 setDividerPosition(pos, true /* applyLayoutChange */); 628 if (finishCallback != null) { 629 finishCallback.run(); 630 } 631 }); 632 } 633 634 @VisibleForTesting flingDividerPosition(int from, int to, int duration, @Nullable Runnable flingFinishedCallback)635 void flingDividerPosition(int from, int to, int duration, 636 @Nullable Runnable flingFinishedCallback) { 637 if (from == to) { 638 if (flingFinishedCallback != null) { 639 flingFinishedCallback.run(); 640 } 641 InteractionJankMonitorUtils.endTracing( 642 CUJ_SPLIT_SCREEN_RESIZE); 643 return; 644 } 645 646 if (mDividerFlingAnimator != null) { 647 mDividerFlingAnimator.cancel(); 648 } 649 650 mDividerFlingAnimator = ValueAnimator 651 .ofInt(from, to) 652 .setDuration(duration); 653 mDividerFlingAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 654 mDividerFlingAnimator.addUpdateListener( 655 animation -> updateDividerBounds( 656 (int) animation.getAnimatedValue(), false /* shouldUseParallaxEffect */) 657 ); 658 mDividerFlingAnimator.addListener(new AnimatorListenerAdapter() { 659 @Override 660 public void onAnimationEnd(Animator animation) { 661 if (flingFinishedCallback != null) { 662 flingFinishedCallback.run(); 663 } 664 InteractionJankMonitorUtils.endTracing( 665 CUJ_SPLIT_SCREEN_RESIZE); 666 mDividerFlingAnimator = null; 667 } 668 669 @Override 670 public void onAnimationCancel(Animator animation) { 671 mDividerFlingAnimator = null; 672 } 673 }); 674 mDividerFlingAnimator.start(); 675 } 676 677 /** Switch both surface position with animation. */ splitSwitching(SurfaceControl.Transaction t, SurfaceControl leash1, SurfaceControl leash2, Consumer<Rect> finishCallback)678 public void splitSwitching(SurfaceControl.Transaction t, SurfaceControl leash1, 679 SurfaceControl leash2, Consumer<Rect> finishCallback) { 680 final Rect insets = getDisplayStableInsets(mContext); 681 insets.set(mIsLeftRightSplit ? insets.left : 0, mIsLeftRightSplit ? 0 : insets.top, 682 mIsLeftRightSplit ? insets.right : 0, mIsLeftRightSplit ? 0 : insets.bottom); 683 684 final int dividerPos = mDividerSnapAlgorithm.calculateNonDismissingSnapTarget( 685 mIsLeftRightSplit ? mBounds2.width() : mBounds2.height()).position; 686 final Rect distBounds1 = new Rect(); 687 final Rect distBounds2 = new Rect(); 688 final Rect distDividerBounds = new Rect(); 689 // Compute dist bounds. 690 updateBounds(dividerPos, distBounds2, distBounds1, distDividerBounds, 691 false /* setEffectBounds */); 692 // Offset to real position under root container. 693 distBounds1.offset(-mRootBounds.left, -mRootBounds.top); 694 distBounds2.offset(-mRootBounds.left, -mRootBounds.top); 695 distDividerBounds.offset(-mRootBounds.left, -mRootBounds.top); 696 697 ValueAnimator animator1 = moveSurface(t, leash1, getRefBounds1(), distBounds1, 698 -insets.left, -insets.top); 699 ValueAnimator animator2 = moveSurface(t, leash2, getRefBounds2(), distBounds2, 700 insets.left, insets.top); 701 ValueAnimator animator3 = moveSurface(t, getDividerLeash(), getRefDividerBounds(), 702 distDividerBounds, 0 /* offsetX */, 0 /* offsetY */); 703 704 AnimatorSet set = new AnimatorSet(); 705 set.playTogether(animator1, animator2, animator3); 706 set.setDuration(FLING_SWITCH_DURATION); 707 set.addListener(new AnimatorListenerAdapter() { 708 @Override 709 public void onAnimationStart(Animator animation) { 710 InteractionJankMonitorUtils.beginTracing(CUJ_SPLIT_SCREEN_DOUBLE_TAP_DIVIDER, 711 mContext, getDividerLeash(), null /*tag*/); 712 } 713 714 @Override 715 public void onAnimationEnd(Animator animation) { 716 mDividerPosition = dividerPos; 717 updateBounds(mDividerPosition); 718 finishCallback.accept(insets); 719 InteractionJankMonitorUtils.endTracing(CUJ_SPLIT_SCREEN_DOUBLE_TAP_DIVIDER); 720 } 721 722 @Override 723 public void onAnimationCancel(Animator animation) { 724 InteractionJankMonitorUtils.cancelTracing(CUJ_SPLIT_SCREEN_DOUBLE_TAP_DIVIDER); 725 } 726 }); 727 set.start(); 728 } 729 moveSurface(SurfaceControl.Transaction t, SurfaceControl leash, Rect start, Rect end, float offsetX, float offsetY)730 private ValueAnimator moveSurface(SurfaceControl.Transaction t, SurfaceControl leash, 731 Rect start, Rect end, float offsetX, float offsetY) { 732 Rect tempStart = new Rect(start); 733 Rect tempEnd = new Rect(end); 734 final float diffX = tempEnd.left - tempStart.left; 735 final float diffY = tempEnd.top - tempStart.top; 736 final float diffWidth = tempEnd.width() - tempStart.width(); 737 final float diffHeight = tempEnd.height() - tempStart.height(); 738 ValueAnimator animator = ValueAnimator.ofFloat(0, 1); 739 animator.addUpdateListener(animation -> { 740 if (leash == null) return; 741 742 final float scale = (float) animation.getAnimatedValue(); 743 final float distX = tempStart.left + scale * diffX; 744 final float distY = tempStart.top + scale * diffY; 745 final int width = (int) (tempStart.width() + scale * diffWidth); 746 final int height = (int) (tempStart.height() + scale * diffHeight); 747 if (offsetX == 0 && offsetY == 0) { 748 t.setPosition(leash, distX, distY); 749 t.setWindowCrop(leash, width, height); 750 } else { 751 final int diffOffsetX = (int) (scale * offsetX); 752 final int diffOffsetY = (int) (scale * offsetY); 753 t.setPosition(leash, distX + diffOffsetX, distY + diffOffsetY); 754 mTempRect.set(0, 0, width, height); 755 mTempRect.offsetTo(-diffOffsetX, -diffOffsetY); 756 t.setCrop(leash, mTempRect); 757 } 758 t.apply(); 759 }); 760 return animator; 761 } 762 getDisplayStableInsets(Context context)763 private Rect getDisplayStableInsets(Context context) { 764 final DisplayLayout displayLayout = 765 mDisplayController.getDisplayLayout(context.getDisplayId()); 766 return displayLayout != null 767 ? displayLayout.stableInsets() 768 : context.getSystemService(WindowManager.class) 769 .getMaximumWindowMetrics() 770 .getWindowInsets() 771 .getInsetsIgnoringVisibility(WindowInsets.Type.systemBars() 772 | WindowInsets.Type.displayCutout()) 773 .toRect(); 774 } 775 776 /** 777 * @return {@code true} if we should create a left-right split, {@code false} if we should 778 * create a top-bottom split. 779 */ isLeftRightSplit()780 public boolean isLeftRightSplit() { 781 return mIsLeftRightSplit; 782 } 783 784 /** Apply recorded surface layout to the {@link SurfaceControl.Transaction}. */ applySurfaceChanges(SurfaceControl.Transaction t, SurfaceControl leash1, SurfaceControl leash2, SurfaceControl dimLayer1, SurfaceControl dimLayer2, boolean applyResizingOffset)785 public void applySurfaceChanges(SurfaceControl.Transaction t, SurfaceControl leash1, 786 SurfaceControl leash2, SurfaceControl dimLayer1, SurfaceControl dimLayer2, 787 boolean applyResizingOffset) { 788 final SurfaceControl dividerLeash = getDividerLeash(); 789 if (dividerLeash != null) { 790 getRefDividerBounds(mTempRect); 791 t.setPosition(dividerLeash, mTempRect.left, mTempRect.top); 792 // Resets layer of divider bar to make sure it is always on top. 793 t.setLayer(dividerLeash, Integer.MAX_VALUE); 794 } 795 getRefBounds1(mTempRect); 796 t.setPosition(leash1, mTempRect.left, mTempRect.top) 797 .setWindowCrop(leash1, mTempRect.width(), mTempRect.height()); 798 getRefBounds2(mTempRect); 799 t.setPosition(leash2, mTempRect.left, mTempRect.top) 800 .setWindowCrop(leash2, mTempRect.width(), mTempRect.height()); 801 802 if (mImePositionProcessor.adjustSurfaceLayoutForIme( 803 t, dividerLeash, leash1, leash2, dimLayer1, dimLayer2)) { 804 return; 805 } 806 807 mSurfaceEffectPolicy.adjustDimSurface(t, dimLayer1, dimLayer2); 808 if (applyResizingOffset) { 809 mSurfaceEffectPolicy.adjustRootSurface(t, leash1, leash2); 810 } 811 } 812 813 /** Apply recorded task layout to the {@link WindowContainerTransaction}. 814 * 815 * @return true if stage bounds actually update. 816 */ applyTaskChanges(WindowContainerTransaction wct, ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2)817 public boolean applyTaskChanges(WindowContainerTransaction wct, 818 ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2) { 819 boolean boundsChanged = false; 820 if (!mBounds1.equals(mWinBounds1) || !task1.token.equals(mWinToken1)) { 821 setTaskBounds(wct, task1, mBounds1); 822 mWinBounds1.set(mBounds1); 823 mWinToken1 = task1.token; 824 boundsChanged = true; 825 } 826 if (!mBounds2.equals(mWinBounds2) || !task2.token.equals(mWinToken2)) { 827 setTaskBounds(wct, task2, mBounds2); 828 mWinBounds2.set(mBounds2); 829 mWinToken2 = task2.token; 830 boundsChanged = true; 831 } 832 return boundsChanged; 833 } 834 835 /** Set bounds to the {@link WindowContainerTransaction} for single task. */ setTaskBounds(WindowContainerTransaction wct, ActivityManager.RunningTaskInfo task, Rect bounds)836 public void setTaskBounds(WindowContainerTransaction wct, 837 ActivityManager.RunningTaskInfo task, Rect bounds) { 838 wct.setBounds(task.token, bounds); 839 wct.setSmallestScreenWidthDp(task.token, getSmallestWidthDp(bounds)); 840 } 841 getSmallestWidthDp(Rect bounds)842 private int getSmallestWidthDp(Rect bounds) { 843 mTempRect.set(bounds); 844 mTempRect.inset(getDisplayStableInsets(mContext)); 845 final int minWidth = Math.min(mTempRect.width(), mTempRect.height()); 846 final float density = mContext.getResources().getDisplayMetrics().density; 847 return (int) (minWidth / density); 848 } 849 850 /** 851 * Shift configuration bounds to prevent client apps get configuration changed or relaunch. And 852 * restore shifted configuration bounds if it's no longer shifted. 853 */ applyLayoutOffsetTarget(WindowContainerTransaction wct, int offsetX, int offsetY, ActivityManager.RunningTaskInfo taskInfo1, ActivityManager.RunningTaskInfo taskInfo2)854 public void applyLayoutOffsetTarget(WindowContainerTransaction wct, int offsetX, int offsetY, 855 ActivityManager.RunningTaskInfo taskInfo1, ActivityManager.RunningTaskInfo taskInfo2) { 856 if (offsetX == 0 && offsetY == 0) { 857 wct.setBounds(taskInfo1.token, mBounds1); 858 wct.setScreenSizeDp(taskInfo1.token, 859 SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED); 860 861 wct.setBounds(taskInfo2.token, mBounds2); 862 wct.setScreenSizeDp(taskInfo2.token, 863 SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED); 864 } else { 865 getBounds1(mTempRect); 866 mTempRect.offset(offsetX, offsetY); 867 wct.setBounds(taskInfo1.token, mTempRect); 868 wct.setScreenSizeDp(taskInfo1.token, 869 taskInfo1.configuration.screenWidthDp, 870 taskInfo1.configuration.screenHeightDp); 871 872 getBounds2(mTempRect); 873 mTempRect.offset(offsetX, offsetY); 874 wct.setBounds(taskInfo2.token, mTempRect); 875 wct.setScreenSizeDp(taskInfo2.token, 876 taskInfo2.configuration.screenWidthDp, 877 taskInfo2.configuration.screenHeightDp); 878 } 879 } 880 881 /** Dumps the current split bounds recorded in this layout. */ dump(@onNull PrintWriter pw, String prefix)882 public void dump(@NonNull PrintWriter pw, String prefix) { 883 final String innerPrefix = prefix + "\t"; 884 pw.println(prefix + TAG + ":"); 885 pw.println(innerPrefix + "mAllowLeftRightSplitInPortrait=" + mAllowLeftRightSplitInPortrait); 886 pw.println(innerPrefix + "mIsLeftRightSplit=" + mIsLeftRightSplit); 887 pw.println(innerPrefix + "mFreezeDividerWindow=" + mFreezeDividerWindow); 888 pw.println(innerPrefix + "mDimNonImeSide=" + mDimNonImeSide); 889 pw.println(innerPrefix + "mDividerPosition=" + mDividerPosition); 890 pw.println(innerPrefix + "bounds1=" + mBounds1.toShortString()); 891 pw.println(innerPrefix + "dividerBounds=" + mDividerBounds.toShortString()); 892 pw.println(innerPrefix + "bounds2=" + mBounds2.toShortString()); 893 } 894 895 /** Handles layout change event. */ 896 public interface SplitLayoutHandler { 897 898 /** Calls when dismissing split. */ onSnappedToDismiss(boolean snappedToEnd, int reason)899 void onSnappedToDismiss(boolean snappedToEnd, int reason); 900 901 /** 902 * Calls when resizing the split bounds. 903 * 904 * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl, 905 * SurfaceControl, SurfaceControl, boolean) 906 */ onLayoutSizeChanging(SplitLayout layout, int offsetX, int offsetY, boolean shouldUseParallaxEffect)907 void onLayoutSizeChanging(SplitLayout layout, int offsetX, int offsetY, 908 boolean shouldUseParallaxEffect); 909 910 /** 911 * Calls when finish resizing the split bounds. 912 * 913 * @see #applyTaskChanges(WindowContainerTransaction, ActivityManager.RunningTaskInfo, 914 * ActivityManager.RunningTaskInfo) 915 * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl, 916 * SurfaceControl, SurfaceControl, boolean) 917 */ onLayoutSizeChanged(SplitLayout layout)918 void onLayoutSizeChanged(SplitLayout layout); 919 920 /** 921 * Calls when re-positioning the split bounds. Like moving split bounds while showing IME 922 * panel. 923 * 924 * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl, 925 * SurfaceControl, SurfaceControl, boolean) 926 */ onLayoutPositionChanging(SplitLayout layout)927 void onLayoutPositionChanging(SplitLayout layout); 928 929 /** 930 * Notifies the target offset for shifting layout. So layout handler can shift configuration 931 * bounds correspondingly to make sure client apps won't get configuration changed or 932 * relaunched. If the layout is no longer shifted, layout handler should restore shifted 933 * configuration bounds. 934 * 935 * @see #applyLayoutOffsetTarget(WindowContainerTransaction, int, int, 936 * ActivityManager.RunningTaskInfo, ActivityManager.RunningTaskInfo) 937 */ setLayoutOffsetTarget(int offsetX, int offsetY, SplitLayout layout)938 void setLayoutOffsetTarget(int offsetX, int offsetY, SplitLayout layout); 939 940 /** Calls when user double tapped on the divider bar. */ onDoubleTappedDivider()941 default void onDoubleTappedDivider() { 942 } 943 944 /** Returns split position of the token. */ 945 @SplitPosition getSplitItemPosition(WindowContainerToken token)946 int getSplitItemPosition(WindowContainerToken token); 947 } 948 949 /** 950 * Calculates and applies proper dismissing parallax offset and dimming value to hint users 951 * dismissing gesture. 952 */ 953 private class ResizingEffectPolicy { 954 /** Indicates whether to offset splitting bounds to hint dismissing progress or not. */ 955 private final int mParallaxType; 956 957 int mShrinkSide = DOCKED_INVALID; 958 959 // The current dismissing side. 960 int mDismissingSide = DOCKED_INVALID; 961 962 // The parallax offset to hint the dismissing side and progress. 963 final Point mParallaxOffset = new Point(); 964 965 // The dimming value to hint the dismissing side and progress. 966 float mDismissingDimValue = 0.0f; 967 final Rect mContentBounds = new Rect(); 968 final Rect mSurfaceBounds = new Rect(); 969 ResizingEffectPolicy(int parallaxType)970 ResizingEffectPolicy(int parallaxType) { 971 mParallaxType = parallaxType; 972 } 973 974 /** 975 * Applies a parallax to the task to hint dismissing progress. 976 * 977 * @param position the split position to apply dismissing parallax effect 978 * @param isLeftRightSplit indicates whether it's splitting horizontally or vertically 979 */ applyDividerPosition(int position, boolean isLeftRightSplit)980 void applyDividerPosition(int position, boolean isLeftRightSplit) { 981 mDismissingSide = DOCKED_INVALID; 982 mParallaxOffset.set(0, 0); 983 mDismissingDimValue = 0; 984 985 int totalDismissingDistance = 0; 986 if (position < mDividerSnapAlgorithm.getFirstSplitTarget().position) { 987 mDismissingSide = isLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP; 988 totalDismissingDistance = mDividerSnapAlgorithm.getDismissStartTarget().position 989 - mDividerSnapAlgorithm.getFirstSplitTarget().position; 990 } else if (position > mDividerSnapAlgorithm.getLastSplitTarget().position) { 991 mDismissingSide = isLeftRightSplit ? DOCKED_RIGHT : DOCKED_BOTTOM; 992 totalDismissingDistance = mDividerSnapAlgorithm.getLastSplitTarget().position 993 - mDividerSnapAlgorithm.getDismissEndTarget().position; 994 } 995 996 final boolean topLeftShrink = isLeftRightSplit 997 ? position < mWinBounds1.right : position < mWinBounds1.bottom; 998 if (topLeftShrink) { 999 mShrinkSide = isLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP; 1000 mContentBounds.set(mWinBounds1); 1001 mSurfaceBounds.set(mBounds1); 1002 } else { 1003 mShrinkSide = isLeftRightSplit ? DOCKED_RIGHT : DOCKED_BOTTOM; 1004 mContentBounds.set(mWinBounds2); 1005 mSurfaceBounds.set(mBounds2); 1006 } 1007 1008 if (mDismissingSide != DOCKED_INVALID) { 1009 float fraction = Math.max(0, 1010 Math.min(mDividerSnapAlgorithm.calculateDismissingFraction(position), 1f)); 1011 mDismissingDimValue = DIM_INTERPOLATOR.getInterpolation(fraction); 1012 if (mParallaxType == PARALLAX_DISMISSING) { 1013 fraction = calculateParallaxDismissingFraction(fraction, mDismissingSide); 1014 if (isLeftRightSplit) { 1015 mParallaxOffset.x = (int) (fraction * totalDismissingDistance); 1016 } else { 1017 mParallaxOffset.y = (int) (fraction * totalDismissingDistance); 1018 } 1019 } 1020 } 1021 1022 if (mParallaxType == PARALLAX_ALIGN_CENTER) { 1023 if (isLeftRightSplit) { 1024 mParallaxOffset.x = 1025 (mSurfaceBounds.width() - mContentBounds.width()) / 2; 1026 } else { 1027 mParallaxOffset.y = 1028 (mSurfaceBounds.height() - mContentBounds.height()) / 2; 1029 } 1030 } 1031 } 1032 1033 /** 1034 * @return for a specified {@code fraction}, this returns an adjusted value that simulates a 1035 * slowing down parallax effect 1036 */ 1037 private float calculateParallaxDismissingFraction(float fraction, int dockSide) { 1038 float result = SLOWDOWN_INTERPOLATOR.getInterpolation(fraction) / 3.5f; 1039 1040 // Less parallax at the top, just because. 1041 if (dockSide == WindowManager.DOCKED_TOP) { 1042 result /= 2f; 1043 } 1044 return result; 1045 } 1046 1047 /** Applies parallax offset and dimming value to the root surface at the dismissing side. */ 1048 void adjustRootSurface(SurfaceControl.Transaction t, 1049 SurfaceControl leash1, SurfaceControl leash2) { 1050 SurfaceControl targetLeash = null; 1051 1052 if (mParallaxType == PARALLAX_DISMISSING) { 1053 switch (mDismissingSide) { 1054 case DOCKED_TOP: 1055 case DOCKED_LEFT: 1056 targetLeash = leash1; 1057 mTempRect.set(mBounds1); 1058 break; 1059 case DOCKED_BOTTOM: 1060 case DOCKED_RIGHT: 1061 targetLeash = leash2; 1062 mTempRect.set(mBounds2); 1063 break; 1064 } 1065 } else if (mParallaxType == PARALLAX_ALIGN_CENTER) { 1066 switch (mShrinkSide) { 1067 case DOCKED_TOP: 1068 case DOCKED_LEFT: 1069 targetLeash = leash1; 1070 mTempRect.set(mBounds1); 1071 break; 1072 case DOCKED_BOTTOM: 1073 case DOCKED_RIGHT: 1074 targetLeash = leash2; 1075 mTempRect.set(mBounds2); 1076 break; 1077 } 1078 } 1079 if (mParallaxType != PARALLAX_NONE && targetLeash != null) { 1080 t.setPosition(targetLeash, 1081 mTempRect.left + mParallaxOffset.x, mTempRect.top + mParallaxOffset.y); 1082 // Transform the screen-based split bounds to surface-based crop bounds. 1083 mTempRect.offsetTo(-mParallaxOffset.x, -mParallaxOffset.y); 1084 t.setWindowCrop(targetLeash, mTempRect); 1085 } 1086 } 1087 1088 void adjustDimSurface(SurfaceControl.Transaction t, 1089 SurfaceControl dimLayer1, SurfaceControl dimLayer2) { 1090 SurfaceControl targetDimLayer; 1091 switch (mDismissingSide) { 1092 case DOCKED_TOP: 1093 case DOCKED_LEFT: 1094 targetDimLayer = dimLayer1; 1095 break; 1096 case DOCKED_BOTTOM: 1097 case DOCKED_RIGHT: 1098 targetDimLayer = dimLayer2; 1099 break; 1100 case DOCKED_INVALID: 1101 default: 1102 t.setAlpha(dimLayer1, 0).hide(dimLayer1); 1103 t.setAlpha(dimLayer2, 0).hide(dimLayer2); 1104 return; 1105 } 1106 t.setAlpha(targetDimLayer, mDismissingDimValue) 1107 .setVisibility(targetDimLayer, mDismissingDimValue > 0.001f); 1108 } 1109 } 1110 1111 /** Records IME top offset changes and updates SplitLayout correspondingly. */ 1112 private class ImePositionProcessor implements DisplayImeController.ImePositionProcessor { 1113 /** 1114 * Maximum size of an adjusted split bounds relative to original stack bounds. Used to 1115 * restrict IME adjustment so that a min portion of top split remains visible. 1116 */ 1117 private static final float ADJUSTED_SPLIT_FRACTION_MAX = 0.7f; 1118 private static final float ADJUSTED_NONFOCUS_DIM = 0.3f; 1119 1120 private final int mDisplayId; 1121 1122 private boolean mHasImeFocus; 1123 private boolean mImeShown; 1124 private int mYOffsetForIme; 1125 private float mDimValue1; 1126 private float mDimValue2; 1127 1128 private int mStartImeTop; 1129 private int mEndImeTop; 1130 1131 private int mTargetYOffset; 1132 private int mLastYOffset; 1133 private float mTargetDim1; 1134 private float mTargetDim2; 1135 private float mLastDim1; 1136 private float mLastDim2; 1137 1138 private ImePositionProcessor(int displayId) { 1139 mDisplayId = displayId; 1140 } 1141 1142 @Override 1143 public int onImeStartPositioning(int displayId, int hiddenTop, int shownTop, 1144 boolean showing, boolean isFloating, SurfaceControl.Transaction t) { 1145 if (displayId != mDisplayId || !mInitialized) { 1146 return 0; 1147 } 1148 1149 final int imeTargetPosition = getImeTargetPosition(); 1150 mHasImeFocus = imeTargetPosition != SPLIT_POSITION_UNDEFINED; 1151 if (!mHasImeFocus) { 1152 return 0; 1153 } 1154 1155 mStartImeTop = showing ? hiddenTop : shownTop; 1156 mEndImeTop = showing ? shownTop : hiddenTop; 1157 mImeShown = showing; 1158 1159 // Update target dim values 1160 mLastDim1 = mDimValue1; 1161 mTargetDim1 = imeTargetPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT && mImeShown 1162 && mDimNonImeSide ? ADJUSTED_NONFOCUS_DIM : 0.0f; 1163 mLastDim2 = mDimValue2; 1164 mTargetDim2 = imeTargetPosition == SPLIT_POSITION_TOP_OR_LEFT && mImeShown 1165 && mDimNonImeSide ? ADJUSTED_NONFOCUS_DIM : 0.0f; 1166 1167 // Calculate target bounds offset for IME 1168 mLastYOffset = mYOffsetForIme; 1169 final boolean needOffset = imeTargetPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT 1170 && !isFloating && !mIsLeftRightSplit && mImeShown; 1171 mTargetYOffset = needOffset ? getTargetYOffset() : 0; 1172 1173 if (mTargetYOffset != mLastYOffset) { 1174 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, 1175 "Split IME animation starting, fromY=%d toY=%d", 1176 mLastYOffset, mTargetYOffset); 1177 // Freeze the configuration size with offset to prevent app get a configuration 1178 // changed or relaunch. This is required to make sure client apps will calculate 1179 // insets properly after layout shifted. 1180 if (mTargetYOffset == 0) { 1181 mSplitLayoutHandler.setLayoutOffsetTarget(0, 0, SplitLayout.this); 1182 } else { 1183 mSplitLayoutHandler.setLayoutOffsetTarget(0, mTargetYOffset, SplitLayout.this); 1184 } 1185 } 1186 1187 // Make {@link DividerView} non-interactive while IME showing in split mode. Listen to 1188 // ImePositionProcessor#onImeVisibilityChanged directly in DividerView is not enough 1189 // because DividerView won't receive onImeVisibilityChanged callback after it being 1190 // re-inflated. 1191 setDividerInteractive(!mImeShown || !mHasImeFocus || isFloating, true, 1192 "onImeStartPositioning"); 1193 1194 return mTargetYOffset != mLastYOffset ? IME_ANIMATION_NO_ALPHA : 0; 1195 } 1196 1197 @Override 1198 public void onImePositionChanged(int displayId, int imeTop, SurfaceControl.Transaction t) { 1199 if (displayId != mDisplayId || !mHasImeFocus) return; 1200 onProgress(getProgress(imeTop)); 1201 mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this); 1202 } 1203 1204 @Override 1205 public void onImeEndPositioning(int displayId, boolean cancel, 1206 SurfaceControl.Transaction t) { 1207 if (displayId != mDisplayId || !mHasImeFocus || cancel) return; 1208 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, 1209 "Split IME animation ending, canceled=%b", cancel); 1210 onProgress(1.0f); 1211 mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this); 1212 } 1213 1214 @Override 1215 public void onImeControlTargetChanged(int displayId, boolean controlling) { 1216 if (displayId != mDisplayId) return; 1217 // Restore the split layout when wm-shell is not controlling IME insets anymore. 1218 if (!controlling && mImeShown) { 1219 reset(); 1220 setDividerInteractive(true, true, "onImeControlTargetChanged"); 1221 mSplitLayoutHandler.setLayoutOffsetTarget(0, 0, SplitLayout.this); 1222 mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this); 1223 } 1224 } 1225 1226 private int getTargetYOffset() { 1227 final int desireOffset = Math.abs(mEndImeTop - mStartImeTop); 1228 // Make sure to keep at least 30% visible for the top split. 1229 final int maxOffset = (int) (mBounds1.height() * ADJUSTED_SPLIT_FRACTION_MAX); 1230 return -Math.min(desireOffset, maxOffset); 1231 } 1232 1233 @SplitPosition 1234 private int getImeTargetPosition() { 1235 final WindowContainerToken token = mTaskOrganizer.getImeTarget(mDisplayId); 1236 return mSplitLayoutHandler.getSplitItemPosition(token); 1237 } 1238 1239 private float getProgress(int currImeTop) { 1240 return ((float) currImeTop - mStartImeTop) / (mEndImeTop - mStartImeTop); 1241 } 1242 1243 private void onProgress(float progress) { 1244 mDimValue1 = getProgressValue(mLastDim1, mTargetDim1, progress); 1245 mDimValue2 = getProgressValue(mLastDim2, mTargetDim2, progress); 1246 mYOffsetForIme = 1247 (int) getProgressValue((float) mLastYOffset, (float) mTargetYOffset, progress); 1248 } 1249 1250 private float getProgressValue(float start, float end, float progress) { 1251 return start + (end - start) * progress; 1252 } 1253 1254 void reset() { 1255 mHasImeFocus = false; 1256 mImeShown = false; 1257 mYOffsetForIme = mLastYOffset = mTargetYOffset = 0; 1258 mDimValue1 = mLastDim1 = mTargetDim1 = 0.0f; 1259 mDimValue2 = mLastDim2 = mTargetDim2 = 0.0f; 1260 } 1261 1262 /** 1263 * Adjusts surface layout while showing IME. 1264 * 1265 * @return {@code false} if there's no need to adjust, otherwise {@code true} 1266 */ 1267 boolean adjustSurfaceLayoutForIme(SurfaceControl.Transaction t, 1268 SurfaceControl dividerLeash, SurfaceControl leash1, SurfaceControl leash2, 1269 SurfaceControl dimLayer1, SurfaceControl dimLayer2) { 1270 final boolean showDim = mDimValue1 > 0.001f || mDimValue2 > 0.001f; 1271 boolean adjusted = false; 1272 if (mYOffsetForIme != 0) { 1273 if (dividerLeash != null) { 1274 getRefDividerBounds(mTempRect); 1275 mTempRect.offset(0, mYOffsetForIme); 1276 t.setPosition(dividerLeash, mTempRect.left, mTempRect.top); 1277 } 1278 1279 getRefBounds1(mTempRect); 1280 mTempRect.offset(0, mYOffsetForIme); 1281 t.setPosition(leash1, mTempRect.left, mTempRect.top); 1282 1283 getRefBounds2(mTempRect); 1284 mTempRect.offset(0, mYOffsetForIme); 1285 t.setPosition(leash2, mTempRect.left, mTempRect.top); 1286 adjusted = true; 1287 } 1288 1289 if (showDim) { 1290 t.setAlpha(dimLayer1, mDimValue1).setVisibility(dimLayer1, mDimValue1 > 0.001f); 1291 t.setAlpha(dimLayer2, mDimValue2).setVisibility(dimLayer2, mDimValue2 > 0.001f); 1292 adjusted = true; 1293 } 1294 return adjusted; 1295 } 1296 } 1297 } 1298