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