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_LEFT; 22 import static android.view.WindowManager.DOCKED_TOP; 23 24 import static com.android.internal.jank.InteractionJankMonitor.CUJ_SPLIT_SCREEN_DOUBLE_TAP_DIVIDER; 25 import static com.android.internal.jank.InteractionJankMonitor.CUJ_SPLIT_SCREEN_RESIZE; 26 import static com.android.wm.shell.shared.animation.Interpolators.EMPHASIZED; 27 import static com.android.wm.shell.shared.animation.Interpolators.FAST_OUT_SLOW_IN; 28 import static com.android.wm.shell.shared.animation.Interpolators.LINEAR; 29 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_10_90; 30 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_90_10; 31 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_3_10_45_45; 32 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_3_45_45_10; 33 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_END_AND_DISMISS; 34 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_START_AND_DISMISS; 35 import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; 36 import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; 37 import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; 38 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER; 39 40 import android.animation.Animator; 41 import android.animation.AnimatorListenerAdapter; 42 import android.animation.AnimatorSet; 43 import android.animation.ValueAnimator; 44 import android.annotation.NonNull; 45 import android.app.ActivityManager; 46 import android.content.Context; 47 import android.content.res.Configuration; 48 import android.content.res.Resources; 49 import android.graphics.Insets; 50 import android.graphics.Rect; 51 import android.os.Handler; 52 import android.util.Log; 53 import android.view.Display; 54 import android.view.InsetsController; 55 import android.view.InsetsSource; 56 import android.view.InsetsSourceControl; 57 import android.view.InsetsState; 58 import android.view.RoundedCorner; 59 import android.view.SurfaceControl; 60 import android.view.WindowInsets; 61 import android.view.WindowManager; 62 import android.view.animation.Interpolator; 63 import android.view.animation.PathInterpolator; 64 import android.window.WindowContainerToken; 65 import android.window.WindowContainerTransaction; 66 67 import androidx.annotation.Nullable; 68 69 import com.android.internal.annotations.VisibleForTesting; 70 import com.android.internal.jank.InteractionJankMonitor; 71 import com.android.internal.protolog.ProtoLog; 72 import com.android.wm.shell.Flags; 73 import com.android.wm.shell.R; 74 import com.android.wm.shell.ShellTaskOrganizer; 75 import com.android.wm.shell.common.DisplayController; 76 import com.android.wm.shell.common.DisplayImeController; 77 import com.android.wm.shell.common.DisplayInsetsController; 78 import com.android.wm.shell.common.DisplayLayout; 79 import com.android.wm.shell.common.pip.PipUtils; 80 import com.android.wm.shell.common.split.DividerSnapAlgorithm.SnapTarget; 81 import com.android.wm.shell.common.split.SplitWindowManager.ParentContainerCallbacks; 82 import com.android.wm.shell.protolog.ShellProtoLogGroup; 83 import com.android.wm.shell.shared.annotations.ShellMainThread; 84 import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition; 85 import com.android.wm.shell.shared.split.SplitScreenConstants.SnapPosition; 86 import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition; 87 import com.android.wm.shell.splitscreen.StageTaskListener; 88 89 import java.io.PrintWriter; 90 import java.util.ArrayList; 91 import java.util.List; 92 import java.util.function.Consumer; 93 94 /** 95 * Records and handles layout of splits. Helps to calculate proper bounds when configuration or 96 * divider position changes. 97 */ 98 public final class SplitLayout implements DisplayInsetsController.OnInsetsChangedListener { 99 private static final String TAG = "SplitLayout"; 100 /** No parallax effect when the user is dragging the divider */ 101 public static final int PARALLAX_NONE = 0; 102 public static final int PARALLAX_DISMISSING = 1; 103 /** Parallax effect (center-aligned) when the user is dragging the divider */ 104 public static final int PARALLAX_ALIGN_CENTER = 2; 105 /** 106 * A custom parallax effect for flexible split. When an app is being pushed/pulled offscreen, 107 * we use a specific parallax to give the impression that it is stuck to the divider. 108 * Otherwise, we fall back to PARALLAX_ALIGN_CENTER behavior. 109 */ 110 public static final int PARALLAX_FLEX = 3; 111 112 public static final int FLING_RESIZE_DURATION = 250; 113 private static final int FLING_ENTER_DURATION = 450; 114 private static final int FLING_EXIT_DURATION = 450; 115 private static final int FLING_OFFSCREEN_DURATION = 500; 116 117 // Here are some (arbitrarily decided) layer definitions used during animations to make sure the 118 // layers stay in order. (During transitions, everything is reparented onto a transition root 119 // and can be freely relayered.) 120 public static final int ANIMATING_DIVIDER_LAYER = 0; 121 public static final int ANIMATING_FRONT_APP_VEIL_LAYER = ANIMATING_DIVIDER_LAYER + 20; 122 public static final int ANIMATING_FRONT_APP_LAYER = ANIMATING_DIVIDER_LAYER + 10; 123 public static final int ANIMATING_BACK_APP_VEIL_LAYER = ANIMATING_DIVIDER_LAYER - 10; 124 public static final int ANIMATING_BACK_APP_LAYER = ANIMATING_DIVIDER_LAYER - 20; 125 // The divider is on the split root, and is sibling with the stage roots. We want to keep it 126 // above the app stages. 127 public static final int RESTING_DIVIDER_LAYER = Integer.MAX_VALUE; 128 // The touch layer is on a stage root, and is sibling with things like the app activity itself 129 // and the app veil. We want it to be above all those. 130 public static final int RESTING_TOUCH_LAYER = Integer.MAX_VALUE; 131 // The dim layer is also on the stage root, and stays under the touch layer. 132 public static final int RESTING_DIM_LAYER = RESTING_TOUCH_LAYER - 1; 133 134 // Animation specs for the swap animation 135 private static final int SWAP_ANIMATION_TOTAL_DURATION = 500; 136 private static final float SWAP_ANIMATION_SHRINK_DURATION = 83; 137 private static final float SWAP_ANIMATION_SHRINK_MARGIN_DP = 14; 138 private static final Interpolator SHRINK_INTERPOLATOR = 139 new PathInterpolator(0.2f, 0f, 0f, 1f); 140 private static final Interpolator GROW_INTERPOLATOR = 141 new PathInterpolator(0.45f, 0f, 0.5f, 1f); 142 @ShellMainThread 143 private final Handler mHandler; 144 145 /** Singleton source of truth for the current state of split screen on this device. */ 146 private final SplitState mSplitState; 147 148 private int mDividerWindowWidth; 149 private int mDividerInsets; 150 private int mDividerSize; 151 152 private final Rect mTempRect = new Rect(); 153 private final Rect mTempRect2 = new Rect(); 154 private final Rect mRootBounds = new Rect(); 155 private final Rect mDividerBounds = new Rect(); 156 /** 157 * A list of stage bounds, kept in order from top/left to bottom/right. These are the sizes of 158 * the app surfaces, not necessarily the same as the size of the rendered content. 159 * See {@link #mContentBounds}. 160 */ 161 private final List<Rect> mStageBounds = List.of(new Rect(), new Rect()); 162 /** 163 * A list of app content bounds, kept in order from top/left to bottom/right. These are the 164 * sizes of the rendered app contents, not necessarily the same as the size of the drawn app 165 * surfaces. See {@link #mStageBounds}. 166 */ 167 private final List<Rect> mContentBounds = List.of(new Rect(), new Rect()); 168 // The temp bounds outside of display bounds for side stage when split screen inactive to avoid 169 // flicker next time active split screen. 170 private final Rect mInvisibleBounds = new Rect(); 171 /** 172 * Areas on the screen that the user can touch to shift the layout, bringing offscreen apps 173 * onscreen. If n apps are offscreen, there should be n such areas. Empty otherwise. 174 */ 175 private final List<OffscreenTouchZone> mOffscreenTouchZones = new ArrayList<>(); 176 private final SplitLayoutHandler mSplitLayoutHandler; 177 private final SplitWindowManager mSplitWindowManager; 178 private final DisplayController mDisplayController; 179 private final DisplayImeController mDisplayImeController; 180 private final ParentContainerCallbacks mParentContainerCallbacks; 181 private final ImePositionProcessor mImePositionProcessor; 182 private final ResizingEffectPolicy mSurfaceEffectPolicy; 183 private final ShellTaskOrganizer mTaskOrganizer; 184 private final InsetsState mInsetsState = new InsetsState(); 185 private Insets mPinnedTaskbarInsets = Insets.NONE; 186 187 private Context mContext; 188 @VisibleForTesting DividerSnapAlgorithm mDividerSnapAlgorithm; 189 private WindowContainerToken mWinToken1; 190 private WindowContainerToken mWinToken2; 191 private int mDividerPosition; 192 private boolean mInitialized = false; 193 private boolean mFreezeDividerWindow = false; 194 private boolean mIsLargeScreen = false; 195 private int mOrientation; 196 private int mRotation; 197 private int mDensity; 198 private int mUiMode; 199 200 private final boolean mDimNonImeSide; 201 private final boolean mAllowLeftRightSplitInPortrait; 202 private final InteractionJankMonitor mInteractionJankMonitor; 203 private boolean mIsLeftRightSplit; 204 private ValueAnimator mDividerFlingAnimator; 205 private AnimatorSet mSwapAnimator; 206 SplitLayout(String windowName, Context context, Configuration configuration, SplitLayoutHandler splitLayoutHandler, SplitWindowManager.ParentContainerCallbacks parentContainerCallbacks, DisplayController displayController, DisplayImeController displayImeController, ShellTaskOrganizer taskOrganizer, int parallaxType, SplitState splitState, @ShellMainThread Handler handler)207 public SplitLayout(String windowName, Context context, Configuration configuration, 208 SplitLayoutHandler splitLayoutHandler, 209 SplitWindowManager.ParentContainerCallbacks parentContainerCallbacks, 210 DisplayController displayController, DisplayImeController displayImeController, 211 ShellTaskOrganizer taskOrganizer, int parallaxType, SplitState splitState, 212 @ShellMainThread Handler handler) { 213 mHandler = handler; 214 mContext = context.createConfigurationContext(configuration); 215 mOrientation = configuration.orientation; 216 mRotation = configuration.windowConfiguration.getRotation(); 217 mDensity = configuration.densityDpi; 218 mIsLargeScreen = configuration.smallestScreenWidthDp >= 600; 219 mSplitLayoutHandler = splitLayoutHandler; 220 mDisplayController = displayController; 221 mDisplayImeController = displayImeController; 222 mParentContainerCallbacks = parentContainerCallbacks; 223 mSplitWindowManager = new SplitWindowManager(windowName, mContext, configuration, 224 parentContainerCallbacks); 225 mTaskOrganizer = taskOrganizer; 226 mImePositionProcessor = new ImePositionProcessor(mContext.getDisplayId()); 227 mSurfaceEffectPolicy = new ResizingEffectPolicy(parallaxType, this); 228 mSplitState = splitState; 229 230 final Resources res = mContext.getResources(); 231 mDimNonImeSide = res.getBoolean(R.bool.config_dimNonImeAttachedSide); 232 mAllowLeftRightSplitInPortrait = SplitScreenUtils.allowLeftRightSplitInPortrait(res); 233 mIsLeftRightSplit = SplitScreenUtils.isLeftRightSplit(mAllowLeftRightSplitInPortrait, 234 configuration); 235 236 updateDividerConfig(mContext); 237 238 mRootBounds.set(configuration.windowConfiguration.getBounds()); 239 updateLayouts(); 240 mInteractionJankMonitor = InteractionJankMonitor.getInstance(); 241 resetDividerPosition(); 242 updateInvisibleRect(); 243 } 244 updateDividerConfig(Context context)245 private void updateDividerConfig(Context context) { 246 final Resources resources = context.getResources(); 247 final Display display = context.getDisplay(); 248 final int dividerInset = resources.getDimensionPixelSize( 249 com.android.internal.R.dimen.docked_stack_divider_insets); 250 int radius = 0; 251 RoundedCorner corner = display.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT); 252 radius = corner != null ? Math.max(radius, corner.getRadius()) : radius; 253 corner = display.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT); 254 radius = corner != null ? Math.max(radius, corner.getRadius()) : radius; 255 corner = display.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT); 256 radius = corner != null ? Math.max(radius, corner.getRadius()) : radius; 257 corner = display.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT); 258 radius = corner != null ? Math.max(radius, corner.getRadius()) : radius; 259 260 mDividerInsets = Math.max(dividerInset, radius); 261 mDividerSize = resources.getDimensionPixelSize(R.dimen.split_divider_bar_width); 262 mDividerWindowWidth = mDividerSize + 2 * mDividerInsets; 263 } 264 265 /** Gets the bounds of the top/left app in screen-based coordinates. */ getTopLeftBounds()266 public Rect getTopLeftBounds() { 267 return mStageBounds.getFirst(); 268 } 269 270 /** Gets the bounds of the bottom/right app in screen-based coordinates. */ getBottomRightBounds()271 public Rect getBottomRightBounds() { 272 return mStageBounds.getLast(); 273 } 274 275 /** Gets the bounds of the top/left app in parent-based coordinates. */ getTopLeftRefBounds()276 public Rect getTopLeftRefBounds() { 277 Rect outBounds = getTopLeftBounds(); 278 outBounds.offset(-mRootBounds.left, -mRootBounds.top); 279 return outBounds; 280 } 281 282 /** Gets the bounds of the bottom/right app in parent-based coordinates. */ getBottomRightRefBounds()283 public Rect getBottomRightRefBounds() { 284 Rect outBounds = getBottomRightBounds(); 285 outBounds.offset(-mRootBounds.left, -mRootBounds.top); 286 return outBounds; 287 } 288 289 /** Gets root bounds of the whole split layout */ getRootBounds()290 public Rect getRootBounds() { 291 return new Rect(mRootBounds); 292 } 293 294 /** Copies the top/left bounds to the provided Rect (screen-based coordinates). */ copyTopLeftBounds(Rect rect)295 public void copyTopLeftBounds(Rect rect) { 296 rect.set(getTopLeftBounds()); 297 } 298 299 /** Copies the top/left bounds to the provided Rect (parent-based coordinates). */ copyTopLeftRefBounds(Rect rect)300 public void copyTopLeftRefBounds(Rect rect) { 301 copyTopLeftBounds(rect); 302 rect.offset(-mRootBounds.left, -mRootBounds.top); 303 } 304 305 /** Copies the bottom/right bounds to the provided Rect (screen-based coordinates). */ copyBottomRightBounds(Rect rect)306 public void copyBottomRightBounds(Rect rect) { 307 rect.set(getBottomRightBounds()); 308 } 309 310 /** Copies the bottom/right bounds to the provided Rect (parent-based coordinates). */ copyBottomRightRefBounds(Rect rect)311 public void copyBottomRightRefBounds(Rect rect) { 312 copyBottomRightBounds(rect); 313 rect.offset(-mRootBounds.left, -mRootBounds.top); 314 } 315 316 /** 317 * Gets the content bounds of the top/left app (the bounds of where the app contents would be 318 * drawn). Might be larger than the available surface space. 319 */ getTopLeftContentBounds()320 public Rect getTopLeftContentBounds() { 321 return mContentBounds.getFirst(); 322 } 323 324 /** 325 * Gets the content bounds of the bottom/right app (the bounds of where the app contents would 326 * be drawn). Might be larger than the available surface space. 327 */ getBottomRightContentBounds()328 public Rect getBottomRightContentBounds() { 329 return mContentBounds.getLast(); 330 } 331 332 /** 333 * Gets the bounds of divider window, in screen-based coordinates. This is not the visible 334 * bounds you see on screen, but the actual behind-the-scenes window bounds, which is larger. 335 */ getDividerBounds()336 public Rect getDividerBounds() { 337 return new Rect(mDividerBounds); 338 } 339 340 /** 341 * Gets the bounds of divider window, in parent-based coordinates. This is not the visible 342 * bounds you see on screen, but the actual behind-the-scenes window bounds, which is larger. 343 */ getRefDividerBounds()344 public Rect getRefDividerBounds() { 345 final Rect outBounds = getDividerBounds(); 346 outBounds.offset(-mRootBounds.left, -mRootBounds.top); 347 return outBounds; 348 } 349 350 /** 351 * Gets the bounds of divider window, in screen-based coordinates. This is not the visible 352 * bounds you see on screen, but the actual behind-the-scenes window bounds, which is larger. 353 */ getDividerBounds(Rect rect)354 public void getDividerBounds(Rect rect) { 355 rect.set(mDividerBounds); 356 } 357 358 /** 359 * Gets the bounds of divider window, in parent-based coordinates. This is not the visible 360 * bounds you see on screen, but the actual behind-the-scenes window bounds, which is larger. 361 */ getRefDividerBounds(Rect rect)362 public void getRefDividerBounds(Rect rect) { 363 getDividerBounds(rect); 364 rect.offset(-mRootBounds.left, -mRootBounds.top); 365 } 366 367 /** Gets bounds size equal to root bounds but outside of screen, used for position side stage 368 * when split inactive to avoid flicker when next time active. */ getInvisibleBounds(Rect rect)369 public void getInvisibleBounds(Rect rect) { 370 rect.set(mInvisibleBounds); 371 } 372 373 /** Returns leash of the current divider bar. */ 374 @Nullable getDividerLeash()375 public SurfaceControl getDividerLeash() { 376 return mSplitWindowManager == null ? null : mSplitWindowManager.getSurfaceControl(); 377 } 378 getDividerPosition()379 int getDividerPosition() { 380 return mDividerPosition; 381 } 382 383 /** 384 * Finds the {@link SnapPosition} nearest to the current divider position. 385 */ calculateCurrentSnapPosition()386 public int calculateCurrentSnapPosition() { 387 return mDividerSnapAlgorithm.calculateNearestSnapPosition(mDividerPosition); 388 } 389 390 /** Updates the {@link SplitState} using the current divider position. */ updateStateWithCurrentPosition()391 public void updateStateWithCurrentPosition() { 392 mSplitState.set(calculateCurrentSnapPosition()); 393 } 394 395 /** 396 * Returns the divider position as a fraction from 0 to 1. 397 */ getDividerPositionAsFraction()398 public float getDividerPositionAsFraction() { 399 if (Flags.enableFlexibleTwoAppSplit()) { 400 return Math.min(1f, Math.max(0f, mIsLeftRightSplit 401 ? (getTopLeftBounds().right + getBottomRightBounds().left) / 2f 402 / getDisplayWidth() 403 : (getTopLeftBounds().bottom + getBottomRightBounds().top) / 2f 404 / getDisplayHeight())); 405 } else { 406 return Math.min(1f, Math.max(0f, mIsLeftRightSplit 407 ? (float) ((getTopLeftBounds().right + getBottomRightBounds().left) / 2f) 408 / getBottomRightBounds().right 409 : (float) ((getTopLeftBounds().bottom + getBottomRightBounds().top) / 2f) 410 / getBottomRightBounds().bottom)); 411 } 412 } 413 updateInvisibleRect()414 private void updateInvisibleRect() { 415 mInvisibleBounds.set(mRootBounds.left, mRootBounds.top, 416 mIsLeftRightSplit ? mRootBounds.right / 2 : mRootBounds.right, 417 mIsLeftRightSplit ? mRootBounds.bottom : mRootBounds.bottom / 2); 418 mInvisibleBounds.offset(mIsLeftRightSplit ? mRootBounds.right : 0, 419 mIsLeftRightSplit ? 0 : mRootBounds.bottom); 420 } 421 422 /** 423 * (Re)calculates and activates any needed touch zones, so the user can tap them and retrieve 424 * offscreen apps. 425 */ populateTouchZones()426 public void populateTouchZones() { 427 if (!Flags.enableFlexibleTwoAppSplit()) { 428 return; 429 } 430 431 if (!mOffscreenTouchZones.isEmpty()) { 432 removeTouchZones(); 433 } 434 435 int currentPosition = mSplitState.get(); 436 // TODO (b/349828130): Can delete this warning after brief soak time. 437 if (currentPosition != calculateCurrentSnapPosition()) { 438 Log.wtf(TAG, "SplitState is " + mSplitState.get() 439 + ", expected " + calculateCurrentSnapPosition()); 440 } 441 442 switch (currentPosition) { 443 case SNAP_TO_2_10_90: 444 case SNAP_TO_3_10_45_45: 445 mOffscreenTouchZones.add(new OffscreenTouchZone(true /* isTopLeft */, 446 () -> flingDividerToOtherSide(currentPosition))); 447 break; 448 case SNAP_TO_2_90_10: 449 case SNAP_TO_3_45_45_10: 450 mOffscreenTouchZones.add(new OffscreenTouchZone(false /* isTopLeft */, 451 () -> flingDividerToOtherSide(currentPosition))); 452 break; 453 } 454 455 mOffscreenTouchZones.forEach(mParentContainerCallbacks::inflateOnStageRoot); 456 } 457 458 /** Removes all touch zones. */ removeTouchZones()459 public void removeTouchZones() { 460 if (!Flags.enableFlexibleTwoAppSplit()) { 461 return; 462 } 463 464 // TODO (b/349828130): It would be good to reuse a Transaction from StageCoordinator's 465 // mTransactionPool here, but passing it through SplitLayout and specifically 466 // SplitLayout.release() is complicated because that function is purposely called with a 467 // null value sometimes. When that function is refactored, we should also pass the 468 // Transaction in here. 469 SurfaceControl.Transaction t = new SurfaceControl.Transaction(); 470 mOffscreenTouchZones.forEach(touchZone -> touchZone.release(t)); 471 t.apply(); 472 mOffscreenTouchZones.clear(); 473 } 474 475 /** Applies new configuration, returns {@code false} if there's no effect to the layout. */ updateConfiguration(Configuration configuration)476 public boolean updateConfiguration(Configuration configuration) { 477 // Update the split bounds when necessary. Besides root bounds changed, split bounds need to 478 // be updated when the rotation changed to cover the case that users rotated the screen 180 479 // degrees. 480 // Make sure to render the divider bar with proper resources that matching the screen 481 // orientation. 482 final int rotation = configuration.windowConfiguration.getRotation(); 483 final Rect rootBounds = configuration.windowConfiguration.getBounds(); 484 final int orientation = configuration.orientation; 485 final int density = configuration.densityDpi; 486 final int uiMode = configuration.uiMode; 487 final boolean wasLeftRightSplit = mIsLeftRightSplit; 488 489 if (mOrientation == orientation 490 && mRotation == rotation 491 && mDensity == density 492 && mUiMode == uiMode 493 && mRootBounds.equals(rootBounds)) { 494 return false; 495 } 496 497 mContext = mContext.createConfigurationContext(configuration); 498 mSplitWindowManager.setConfiguration(configuration); 499 mOrientation = orientation; 500 mTempRect.set(mRootBounds); 501 mRootBounds.set(rootBounds); 502 mRotation = rotation; 503 mDensity = density; 504 mUiMode = uiMode; 505 mIsLargeScreen = configuration.smallestScreenWidthDp >= 600; 506 mIsLeftRightSplit = SplitScreenUtils.isLeftRightSplit(mAllowLeftRightSplitInPortrait, 507 configuration); 508 updateLayouts(); 509 updateDividerConfig(mContext); 510 initDividerPosition(mTempRect, wasLeftRightSplit); 511 updateInvisibleRect(); 512 513 return true; 514 } 515 516 /** Rotate the layout to specific rotation and calculate new bounds. The stable insets value 517 * should be calculated by display layout. */ rotateTo(int newRotation)518 public void rotateTo(int newRotation) { 519 final int rotationDelta = (newRotation - mRotation + 4) % 4; 520 final boolean changeOrient = (rotationDelta % 2) != 0; 521 522 mRotation = newRotation; 523 Rect tmpRect = new Rect(mRootBounds); 524 if (changeOrient) { 525 tmpRect.set(mRootBounds.top, mRootBounds.left, mRootBounds.bottom, mRootBounds.right); 526 } 527 528 // We only need new bounds here, other configuration should be update later. 529 final boolean wasLeftRightSplit = SplitScreenUtils.isLeftRightSplit( 530 mAllowLeftRightSplitInPortrait, mIsLargeScreen, 531 mRootBounds.width() >= mRootBounds.height()); 532 mTempRect.set(mRootBounds); 533 mRootBounds.set(tmpRect); 534 mIsLeftRightSplit = SplitScreenUtils.isLeftRightSplit(mAllowLeftRightSplitInPortrait, 535 mIsLargeScreen, mRootBounds.width() >= mRootBounds.height()); 536 updateLayouts(); 537 initDividerPosition(mTempRect, wasLeftRightSplit); 538 } 539 540 /** 541 * Updates the divider position to the position in the current orientation and bounds using the 542 * snap fraction calculated based on the previous orientation and bounds. 543 */ initDividerPosition(Rect oldBounds, boolean wasLeftRightSplit)544 private void initDividerPosition(Rect oldBounds, boolean wasLeftRightSplit) { 545 final float snapRatio = (float) mDividerPosition 546 / (float) (wasLeftRightSplit ? oldBounds.width() : oldBounds.height()); 547 // Estimate position by previous ratio. 548 final float length = 549 (float) (mIsLeftRightSplit ? mRootBounds.width() : mRootBounds.height()); 550 final int estimatePosition = (int) (length * snapRatio); 551 // Init divider position by estimated position using current bounds snap algorithm. 552 mDividerPosition = mDividerSnapAlgorithm.calculateNonDismissingSnapTarget( 553 estimatePosition).position; 554 updateBounds(mDividerPosition); 555 } 556 updateBounds(int position)557 private void updateBounds(int position) { 558 updateBounds(position, getTopLeftBounds(), getBottomRightBounds(), mDividerBounds, 559 true /* setEffectBounds */); 560 } 561 562 /** 563 * Updates the bounds of the divider window and both split apps. 564 * @param position The left/top edge of the visual divider, where the edge of app A meets the 565 * divider. Not to be confused with the actual divider surface, which is larger 566 * and overlaps the apps a bit. 567 */ updateBounds(int position, Rect bounds1, Rect bounds2, Rect dividerBounds, boolean setEffectBounds)568 private void updateBounds(int position, Rect bounds1, Rect bounds2, Rect dividerBounds, 569 boolean setEffectBounds) { 570 dividerBounds.set(mRootBounds); 571 bounds1.set(mRootBounds); 572 bounds2.set(mRootBounds); 573 if (mIsLeftRightSplit) { 574 position += mRootBounds.left; 575 dividerBounds.left = position - mDividerInsets; 576 dividerBounds.right = dividerBounds.left + mDividerWindowWidth; 577 bounds1.right = position; 578 bounds2.left = bounds1.right + mDividerSize; 579 580 // For flexible split, expand app offscreen as well 581 if (mDividerSnapAlgorithm.areOffscreenRatiosSupported()) { 582 int distanceToCenter = position - mDividerSnapAlgorithm.getMiddleTarget().position; 583 if (position < mDividerSnapAlgorithm.getMiddleTarget().position) { 584 bounds1.left += distanceToCenter * 2; 585 } else { 586 bounds2.right += distanceToCenter * 2; 587 } 588 } 589 590 } else { 591 position += mRootBounds.top; 592 dividerBounds.top = position - mDividerInsets; 593 dividerBounds.bottom = dividerBounds.top + mDividerWindowWidth; 594 bounds1.bottom = position; 595 bounds2.top = bounds1.bottom + mDividerSize; 596 597 // For flexible split, expand app offscreen as well 598 if (mDividerSnapAlgorithm.areOffscreenRatiosSupported()) { 599 int distanceToCenter = position - mDividerSnapAlgorithm.getMiddleTarget().position; 600 if (position < mDividerSnapAlgorithm.getMiddleTarget().position) { 601 bounds1.top += distanceToCenter * 2; 602 } else { 603 bounds2.bottom += distanceToCenter * 2; 604 } 605 } 606 } 607 DockedDividerUtils.sanitizeStackBounds(bounds1, true /** topLeft */); 608 DockedDividerUtils.sanitizeStackBounds(bounds2, false /** topLeft */); 609 if (setEffectBounds) { 610 mSurfaceEffectPolicy.applyDividerPosition( 611 position, mIsLeftRightSplit, mDividerSnapAlgorithm); 612 } 613 } 614 615 /** Inflates {@link DividerView} on the root surface. */ init()616 public void init() { 617 if (mInitialized) return; 618 mInitialized = true; 619 mSplitWindowManager.init(this, mInsetsState, false /* isRestoring */); 620 populateTouchZones(); 621 mDisplayImeController.addPositionProcessor(mImePositionProcessor); 622 } 623 624 /** Releases the surface holding the current {@link DividerView}. */ release(SurfaceControl.Transaction t)625 public void release(SurfaceControl.Transaction t) { 626 if (!mInitialized) return; 627 mInitialized = false; 628 mSplitWindowManager.release(t); 629 removeTouchZones(); 630 mDisplayImeController.removePositionProcessor(mImePositionProcessor); 631 mImePositionProcessor.reset(); 632 if (mDividerFlingAnimator != null) { 633 mDividerFlingAnimator.cancel(); 634 } 635 resetDividerPosition(); 636 } 637 release()638 public void release() { 639 release(null /* t */); 640 } 641 642 /** Releases and re-inflates {@link DividerView} on the root surface. */ update(SurfaceControl.Transaction t, boolean resetImePosition)643 public void update(SurfaceControl.Transaction t, boolean resetImePosition) { 644 if (!mInitialized) { 645 init(); 646 return; 647 } 648 mSplitWindowManager.release(t); 649 if (resetImePosition) { 650 mImePositionProcessor.reset(); 651 } 652 mSplitWindowManager.init(this, mInsetsState, true /* isRestoring */); 653 populateTouchZones(); 654 // Update the surface positions again after recreating the divider in case nothing else 655 // triggers it 656 mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this); 657 } 658 659 @Override insetsChanged(InsetsState insetsState)660 public void insetsChanged(InsetsState insetsState) { 661 mInsetsState.set(insetsState); 662 663 if (!mInitialized) { 664 return; 665 } 666 if (mFreezeDividerWindow) { 667 // DO NOT change its layout before transition actually run because it might cause 668 // flicker. 669 return; 670 } 671 672 // Check to see if insets changed in such a way that the divider needs to be animated to 673 // a new position. (We only do this when switching to pinned taskbar mode and back). 674 Insets pinnedTaskbarInsets = calculatePinnedTaskbarInsets(insetsState); 675 if (!mPinnedTaskbarInsets.equals(pinnedTaskbarInsets)) { 676 mPinnedTaskbarInsets = pinnedTaskbarInsets; 677 // Refresh the DividerSnapAlgorithm. 678 updateLayouts(); 679 // If the divider is no longer placed on a snap point, animate it to the nearest one 680 DividerSnapAlgorithm.SnapTarget snapTarget = 681 findSnapTarget(mDividerPosition, 0, false /* hardDismiss */); 682 if (snapTarget.position != mDividerPosition) { 683 snapToTarget(mDividerPosition, snapTarget, 684 InsetsController.ANIMATION_DURATION_RESIZE, 685 InsetsController.RESIZE_INTERPOLATOR); 686 } 687 } 688 689 mSplitWindowManager.onInsetsChanged(insetsState); 690 } 691 692 /** 693 * Calculates the insets that might trigger a divider algorithm recalculation. 694 */ calculatePinnedTaskbarInsets(InsetsState insetsState)695 private Insets calculatePinnedTaskbarInsets(InsetsState insetsState) { 696 for (int i = insetsState.sourceSize() - 1; i >= 0; i--) { 697 final InsetsSource source = insetsState.sourceAt(i); 698 // If Taskbar is pinned... 699 if (source.getType() == WindowInsets.Type.navigationBars() 700 && source.hasFlags(InsetsSource.FLAG_INSETS_ROUNDED_CORNER)) { 701 // Return Insets representing the pinned taskbar state. 702 return source.calculateVisibleInsets(mRootBounds); 703 } 704 } 705 706 // Else, divider can calculate based on the full display. 707 return Insets.NONE; 708 } 709 710 @Override insetsControlChanged(InsetsState insetsState, InsetsSourceControl[] activeControls)711 public void insetsControlChanged(InsetsState insetsState, 712 InsetsSourceControl[] activeControls) { 713 if (!mInsetsState.equals(insetsState)) { 714 insetsChanged(insetsState); 715 } 716 } 717 setFreezeDividerWindow(boolean freezeDividerWindow)718 public void setFreezeDividerWindow(boolean freezeDividerWindow) { 719 mFreezeDividerWindow = freezeDividerWindow; 720 } 721 722 /** Update current layout as divider put on start or end position. */ setDividerAtBorder(boolean start)723 public void setDividerAtBorder(boolean start) { 724 final int pos = start ? mDividerSnapAlgorithm.getDismissStartTarget().position 725 : mDividerSnapAlgorithm.getDismissEndTarget().position; 726 setDividerPosition(pos, false /* applyLayoutChange */); 727 } 728 729 /** 730 * Updates bounds with the passing position. Usually used to update recording bounds while 731 * performing animation or dragging divider bar to resize the splits. 732 */ updateDividerBounds(int position, boolean shouldUseParallaxEffect)733 void updateDividerBounds(int position, boolean shouldUseParallaxEffect) { 734 updateBounds(position); 735 mSplitLayoutHandler.onLayoutSizeChanging(this, 736 mSurfaceEffectPolicy.mRetreatingSideParallax.x, 737 mSurfaceEffectPolicy.mRetreatingSideParallax.y, shouldUseParallaxEffect); 738 } 739 setDividerPosition(int position, boolean applyLayoutChange)740 void setDividerPosition(int position, boolean applyLayoutChange) { 741 mDividerPosition = position; 742 updateBounds(mDividerPosition); 743 if (applyLayoutChange) { 744 mSplitLayoutHandler.onLayoutSizeChanged(this); 745 } 746 } 747 748 /** 749 * Updates divider position and split bounds base on the ratio within root bounds. Falls back 750 * to middle position if the provided SnapTarget is not supported. 751 */ setDivideRatio(@ersistentSnapPosition int snapPosition)752 public void setDivideRatio(@PersistentSnapPosition int snapPosition) { 753 final SnapTarget snapTarget = mDividerSnapAlgorithm.findSnapTarget( 754 snapPosition); 755 756 setDividerPosition(snapTarget != null 757 ? snapTarget.position 758 : mDividerSnapAlgorithm.getMiddleTarget().position, 759 false /* applyLayoutChange */); 760 } 761 762 /** Resets divider position. */ resetDividerPosition()763 public void resetDividerPosition() { 764 mDividerPosition = mDividerSnapAlgorithm.getMiddleTarget().position; 765 updateBounds(mDividerPosition); 766 mWinToken1 = null; 767 mWinToken2 = null; 768 getTopLeftContentBounds().setEmpty(); 769 getBottomRightContentBounds().setEmpty(); 770 } 771 772 /** 773 * Set divider should interactive to user or not. 774 * 775 * @param interactive divider interactive. 776 * @param hideHandle divider handle hidden or not, only work when interactive is false. 777 * @param from caller from where. 778 */ setDividerInteractive(boolean interactive, boolean hideHandle, String from)779 public void setDividerInteractive(boolean interactive, boolean hideHandle, String from) { 780 mSplitWindowManager.setInteractive(interactive, hideHandle, from); 781 } 782 783 /** 784 * Sets new divider position and updates bounds correspondingly. Notifies listener if the new 785 * target indicates dismissing split. 786 */ snapToTarget(int currentPosition, SnapTarget snapTarget, int duration, Interpolator interpolator)787 public void snapToTarget(int currentPosition, SnapTarget snapTarget, int duration, 788 Interpolator interpolator) { 789 switch (snapTarget.snapPosition) { 790 case SNAP_TO_START_AND_DISMISS: 791 flingDividerPosition(currentPosition, snapTarget.position, duration, interpolator, 792 () -> mSplitLayoutHandler.onSnappedToDismiss(false /* bottomOrRight */, 793 EXIT_REASON_DRAG_DIVIDER)); 794 break; 795 case SNAP_TO_END_AND_DISMISS: 796 flingDividerPosition(currentPosition, snapTarget.position, duration, interpolator, 797 () -> mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */, 798 EXIT_REASON_DRAG_DIVIDER)); 799 break; 800 default: 801 flingDividerPosition(currentPosition, snapTarget.position, duration, interpolator, 802 () -> { 803 setDividerPosition(snapTarget.position, true /* applyLayoutChange */); 804 mSplitState.set(snapTarget.snapPosition); 805 }); 806 break; 807 } 808 } 809 810 /** 811 * Same as {@link #snapToTarget(int, SnapTarget, int, Interpolator)}, with default animation 812 * duration and interpolator. 813 */ snapToTarget(int currentPosition, SnapTarget snapTarget)814 public void snapToTarget(int currentPosition, SnapTarget snapTarget) { 815 snapToTarget(currentPosition, snapTarget, FLING_RESIZE_DURATION, 816 FAST_OUT_SLOW_IN); 817 } 818 onStartDragging()819 void onStartDragging() { 820 mInteractionJankMonitor.begin(getDividerLeash(), mContext, mHandler, 821 CUJ_SPLIT_SCREEN_RESIZE); 822 } 823 onDraggingCancelled()824 void onDraggingCancelled() { 825 mInteractionJankMonitor.cancel(CUJ_SPLIT_SCREEN_RESIZE); 826 } 827 onDoubleTappedDivider()828 void onDoubleTappedDivider() { 829 if (isCurrentlySwapping()) { 830 return; 831 } 832 833 mSplitLayoutHandler.onDoubleTappedDivider(); 834 } 835 836 /** 837 * Returns {@link SnapTarget} which matches passing position and velocity. 838 * If hardDismiss is set to {@code true}, it will be harder to reach dismiss target. 839 */ findSnapTarget(int position, float velocity, boolean hardDismiss)840 public SnapTarget findSnapTarget(int position, float velocity, 841 boolean hardDismiss) { 842 return mDividerSnapAlgorithm.calculateSnapTarget(position, velocity, hardDismiss); 843 } 844 845 /** 846 * (Re)calculates the split screen logic for this particular display/orientation. Refreshes the 847 * DividerSnapAlgorithm, which controls divider snap points, and populates a map in SplitState 848 * with bounds for all valid split layouts. 849 */ updateLayouts()850 private void updateLayouts() { 851 // Update SplitState map 852 853 if (Flags.enableFlexibleTwoAppSplit()) { 854 mSplitState.populateLayouts( 855 mRootBounds, mDividerSize, mIsLeftRightSplit, mPinnedTaskbarInsets.toRect()); 856 } 857 858 // Get new DividerSnapAlgorithm 859 860 final Rect insets = getDisplayStableInsets(mContext); 861 862 // Make split axis insets value same as the larger one to avoid bounds1 and bounds2 863 // have difference for avoiding size-compat mode when switching unresizable apps in 864 // landscape while they are letterboxed. 865 if (!mIsLeftRightSplit) { 866 final int largerInsets = Math.max(insets.top, insets.bottom); 867 insets.set(insets.left, largerInsets, insets.right, largerInsets); 868 } 869 870 mDividerSnapAlgorithm = new DividerSnapAlgorithm( 871 mContext.getResources(), 872 mRootBounds.width(), 873 mRootBounds.height(), 874 mDividerSize, 875 mIsLeftRightSplit, 876 insets, 877 mPinnedTaskbarInsets.toRect(), 878 mIsLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP /* dockSide */); 879 } 880 881 /** Fling divider from current position to end or start position then exit */ flingDividerToDismiss(boolean toEnd, int reason)882 public void flingDividerToDismiss(boolean toEnd, int reason) { 883 final int target = toEnd ? mDividerSnapAlgorithm.getDismissEndTarget().position 884 : mDividerSnapAlgorithm.getDismissStartTarget().position; 885 flingDividerPosition(getDividerPosition(), target, FLING_EXIT_DURATION, FAST_OUT_SLOW_IN, 886 () -> mSplitLayoutHandler.onSnappedToDismiss(toEnd, reason)); 887 } 888 889 /** Fling divider from current position to center position. */ flingDividerToCenter(@ullable Runnable finishCallback)890 public void flingDividerToCenter(@Nullable Runnable finishCallback) { 891 final SnapTarget target = mDividerSnapAlgorithm.getMiddleTarget(); 892 final int pos = target.position; 893 flingDividerPosition(getDividerPosition(), pos, FLING_ENTER_DURATION, FAST_OUT_SLOW_IN, 894 () -> { 895 setDividerPosition(pos, true /* applyLayoutChange */); 896 mSplitState.set(target.snapPosition); 897 if (finishCallback != null) { 898 finishCallback.run(); 899 } 900 }); 901 } 902 903 /** 904 * Moves the divider to the other side of the screen. Does nothing if the divider is in the 905 * center. 906 * TODO (b/349828130): Currently only supports the two-app case. For n-apps, 907 * DividerSnapAlgorithm will need to be refactored, and this function will change as well. 908 */ flingDividerToOtherSide(@ersistentSnapPosition int currentSnapPosition)909 public void flingDividerToOtherSide(@PersistentSnapPosition int currentSnapPosition) { 910 // If a fling animation is already running, just return. 911 if (mDividerFlingAnimator != null) return; 912 913 switch (currentSnapPosition) { 914 case SNAP_TO_2_10_90 -> 915 snapToTarget(mDividerPosition, mDividerSnapAlgorithm.getLastSplitTarget(), 916 FLING_OFFSCREEN_DURATION, EMPHASIZED); 917 case SNAP_TO_2_90_10 -> 918 snapToTarget(mDividerPosition, mDividerSnapAlgorithm.getFirstSplitTarget(), 919 FLING_OFFSCREEN_DURATION, EMPHASIZED); 920 } 921 } 922 923 @VisibleForTesting flingDividerPosition(int from, int to, int duration, Interpolator interpolator, @Nullable Runnable flingFinishedCallback)924 void flingDividerPosition(int from, int to, int duration, Interpolator interpolator, 925 @Nullable Runnable flingFinishedCallback) { 926 if (from == to) { 927 if (flingFinishedCallback != null) { 928 flingFinishedCallback.run(); 929 } 930 mInteractionJankMonitor.end( 931 CUJ_SPLIT_SCREEN_RESIZE); 932 return; 933 } 934 935 if (mDividerFlingAnimator != null) { 936 mDividerFlingAnimator.cancel(); 937 } 938 939 mDividerFlingAnimator = ValueAnimator 940 .ofInt(from, to) 941 .setDuration(duration); 942 mDividerFlingAnimator.setInterpolator(interpolator); 943 944 // If the divider is being physically controlled by the user, we use a cool parallax effect 945 // on the task windows. So if this "snap" animation is an extension of a user-controlled 946 // movement, we pass in true here to continue the parallax effect smoothly. 947 boolean isBeingMovedByUser = mSplitWindowManager.getDividerView() != null 948 && mSplitWindowManager.getDividerView().isMoving(); 949 950 mDividerFlingAnimator.addUpdateListener( 951 animation -> updateDividerBounds( 952 (int) animation.getAnimatedValue(), 953 isBeingMovedByUser /* shouldUseParallaxEffect */ 954 ) 955 ); 956 mDividerFlingAnimator.addListener(new AnimatorListenerAdapter() { 957 @Override 958 public void onAnimationEnd(Animator animation) { 959 if (flingFinishedCallback != null) { 960 flingFinishedCallback.run(); 961 } 962 mInteractionJankMonitor.end( 963 CUJ_SPLIT_SCREEN_RESIZE); 964 mDividerFlingAnimator = null; 965 } 966 967 @Override 968 public void onAnimationCancel(Animator animation) { 969 mDividerFlingAnimator = null; 970 } 971 }); 972 mDividerFlingAnimator.start(); 973 } 974 975 /** Switch both surface position with animation. */ playSwapAnimation(SurfaceControl.Transaction t, StageTaskListener topLeftStage, StageTaskListener bottomRightStage, Consumer<Rect> finishCallback)976 public void playSwapAnimation(SurfaceControl.Transaction t, StageTaskListener topLeftStage, 977 StageTaskListener bottomRightStage, Consumer<Rect> finishCallback) { 978 final Rect insets = getDisplayStableInsets(mContext); 979 // If we have insets in the direction of the swap, the animation won't look correct because 980 // window contents will shift and redraw again at the end. So we show a veil to hide that. 981 insets.set(mIsLeftRightSplit ? insets.left : 0, mIsLeftRightSplit ? 0 : insets.top, 982 mIsLeftRightSplit ? insets.right : 0, mIsLeftRightSplit ? 0 : insets.bottom); 983 final boolean shouldVeil = 984 insets.left != 0 || insets.top != 0 || insets.right != 0 || insets.bottom != 0; 985 986 // Find the "left/top"-most position of the app surface -- usually 0, but sometimes negative 987 // if the left/top app is offscreen. 988 int leftTop = 0; 989 if (Flags.enableFlexibleTwoAppSplit()) { 990 leftTop = mIsLeftRightSplit ? getTopLeftBounds().left : getTopLeftBounds().top; 991 } 992 993 final int dividerPos = mDividerSnapAlgorithm.calculateNonDismissingSnapTarget( 994 leftTop + (mIsLeftRightSplit 995 ? getBottomRightBounds().width() : getBottomRightBounds().height()) 996 ).position; 997 final Rect endBounds1 = new Rect(); 998 final Rect endBounds2 = new Rect(); 999 final Rect endDividerBounds = new Rect(); 1000 // Compute destination bounds. 1001 updateBounds(dividerPos, endBounds2, endBounds1, endDividerBounds, 1002 false /* setEffectBounds */); 1003 // Offset to real position under root container. 1004 endBounds1.offset(-mRootBounds.left, -mRootBounds.top); 1005 endBounds2.offset(-mRootBounds.left, -mRootBounds.top); 1006 endDividerBounds.offset(-mRootBounds.left, -mRootBounds.top); 1007 1008 ValueAnimator animator1 = moveSurface(t, topLeftStage, getTopLeftRefBounds(), endBounds1, 1009 -insets.left, -insets.top, true /* roundCorners */, true /* isGoingBehind */, 1010 shouldVeil); 1011 ValueAnimator animator2 = moveSurface(t, bottomRightStage, getBottomRightRefBounds(), 1012 endBounds2, insets.left, insets.top, true /* roundCorners */, 1013 false /* isGoingBehind */, shouldVeil); 1014 ValueAnimator animator3 = moveSurface(t, null /* stage */, getRefDividerBounds(), 1015 endDividerBounds, 0 /* offsetX */, 0 /* offsetY */, false /* roundCorners */, 1016 false /* isGoingBehind */, false /* addVeil */); 1017 1018 mSwapAnimator = new AnimatorSet(); 1019 mSwapAnimator.playTogether(animator1, animator2, animator3); 1020 mSwapAnimator.setDuration(SWAP_ANIMATION_TOTAL_DURATION); 1021 mSwapAnimator.addListener(new AnimatorListenerAdapter() { 1022 @Override 1023 public void onAnimationStart(Animator animation) { 1024 mInteractionJankMonitor.begin(getDividerLeash(), 1025 mContext, mHandler, CUJ_SPLIT_SCREEN_DOUBLE_TAP_DIVIDER); 1026 } 1027 1028 @Override 1029 public void onAnimationEnd(Animator animation) { 1030 mDividerPosition = dividerPos; 1031 updateBounds(mDividerPosition); 1032 finishCallback.accept(insets); 1033 mInteractionJankMonitor.end(CUJ_SPLIT_SCREEN_DOUBLE_TAP_DIVIDER); 1034 } 1035 1036 @Override 1037 public void onAnimationCancel(Animator animation) { 1038 mInteractionJankMonitor.cancel(CUJ_SPLIT_SCREEN_DOUBLE_TAP_DIVIDER); 1039 } 1040 }); 1041 mSwapAnimator.start(); 1042 } 1043 1044 /** Returns true if a swap animation is currently playing. */ isCurrentlySwapping()1045 public boolean isCurrentlySwapping() { 1046 return mSwapAnimator != null && mSwapAnimator.isRunning(); 1047 } 1048 1049 /** 1050 * Animates a task leash across the screen. Currently used only for the swap animation. 1051 * 1052 * @param stage The stage holding the task being animated. If null, it is the divider. 1053 * @param roundCorners Whether we should round the corners of the task while animating. 1054 * @param isGoingBehind Whether we should a shrink-and-grow effect to the task while it is 1055 * moving. (Simulates moving behind the divider.) 1056 */ moveSurface(SurfaceControl.Transaction t, StageTaskListener stage, Rect start, Rect end, float offsetX, float offsetY, boolean roundCorners, boolean isGoingBehind, boolean addVeil)1057 private ValueAnimator moveSurface(SurfaceControl.Transaction t, StageTaskListener stage, 1058 Rect start, Rect end, float offsetX, float offsetY, boolean roundCorners, 1059 boolean isGoingBehind, boolean addVeil) { 1060 final boolean isApp = stage != null; // check if this is an app or a divider 1061 final SurfaceControl leash = isApp ? stage.getRootLeash() : getDividerLeash(); 1062 final ActivityManager.RunningTaskInfo taskInfo = isApp ? stage.getRunningTaskInfo() : null; 1063 final SplitDecorManager decorManager = isApp ? stage.getDecorManager() : null; 1064 1065 Rect tempStart = new Rect(start); 1066 Rect tempEnd = new Rect(end); 1067 final float diffX = tempEnd.left - tempStart.left; 1068 final float diffY = tempEnd.top - tempStart.top; 1069 final float diffWidth = tempEnd.width() - tempStart.width(); 1070 final float diffHeight = tempEnd.height() - tempStart.height(); 1071 1072 // Get display measurements (for possible shrink animation). 1073 final RoundedCorner roundedCorner = mSplitWindowManager.getDividerView().getDisplay() 1074 .getRoundedCorner(0 /* position */); 1075 float cornerRadius = roundedCorner == null ? 0 : roundedCorner.getRadius(); 1076 float shrinkMarginPx = PipUtils.dpToPx( 1077 SWAP_ANIMATION_SHRINK_MARGIN_DP, mContext.getResources().getDisplayMetrics()); 1078 float shrinkAmountPx = shrinkMarginPx * 2; 1079 1080 // Timing calculations 1081 float shrinkPortion = SWAP_ANIMATION_SHRINK_DURATION / SWAP_ANIMATION_TOTAL_DURATION; 1082 float growPortion = 1 - shrinkPortion; 1083 1084 ValueAnimator animator = ValueAnimator.ofFloat(0, 1); 1085 // Set the base animation to proceed linearly. Each component of the animation (movement, 1086 // shrinking, growing) overrides it with a different interpolator later. 1087 animator.setInterpolator(LINEAR); 1088 animator.addUpdateListener(animation -> { 1089 if (leash == null) return; 1090 if (roundCorners) { 1091 // Add rounded corners to the task leash while it is animating. 1092 t.setCornerRadius(leash, cornerRadius); 1093 } 1094 1095 final float progress = (float) animation.getAnimatedValue(); 1096 final float moveProgress = EMPHASIZED.getInterpolation(progress); 1097 float instantaneousX = tempStart.left + moveProgress * diffX; 1098 float instantaneousY = tempStart.top + moveProgress * diffY; 1099 int width = (int) (tempStart.width() + moveProgress * diffWidth); 1100 int height = (int) (tempStart.height() + moveProgress * diffHeight); 1101 1102 if (isGoingBehind) { 1103 float shrinkDiffX; // the position adjustments needed for this frame 1104 float shrinkDiffY; 1105 float shrinkScaleX; // the scale adjustments needed for this frame 1106 float shrinkScaleY; 1107 1108 // Find the max amount we will be shrinking this leash, as a proportion (e.g. 0.1f). 1109 float maxShrinkX = shrinkAmountPx / height; 1110 float maxShrinkY = shrinkAmountPx / width; 1111 1112 // Find if we are in the shrinking part of the animation, or the growing part. 1113 boolean shrinking = progress <= shrinkPortion; 1114 1115 if (shrinking) { 1116 // Find how far into the shrink portion we are (e.g. 0.5f). 1117 float shrinkProgress = progress / shrinkPortion; 1118 // Find how much we should have progressed in shrinking the leash (e.g. 0.8f). 1119 float interpolatedShrinkProgress = 1120 SHRINK_INTERPOLATOR.getInterpolation(shrinkProgress); 1121 // Find how much width proportion we should be taking off (e.g. 0.1f) 1122 float widthProportionLost = maxShrinkX * interpolatedShrinkProgress; 1123 shrinkScaleX = 1 - widthProportionLost; 1124 // Find how much height proportion we should be taking off (e.g. 0.1f) 1125 float heightProportionLost = maxShrinkY * interpolatedShrinkProgress; 1126 shrinkScaleY = 1 - heightProportionLost; 1127 // Add a small amount to the leash's position to keep the task centered. 1128 shrinkDiffX = (width * widthProportionLost) / 2; 1129 shrinkDiffY = (height * heightProportionLost) / 2; 1130 } else { 1131 // Find how far into the grow portion we are (e.g. 0.5f). 1132 float growProgress = (progress - shrinkPortion) / growPortion; 1133 // Find how much we should have progressed in growing the leash (e.g. 0.8f). 1134 float interpolatedGrowProgress = 1135 GROW_INTERPOLATOR.getInterpolation(growProgress); 1136 // Find how much width proportion we should be taking off (e.g. 0.1f) 1137 float widthProportionLost = maxShrinkX * (1 - interpolatedGrowProgress); 1138 shrinkScaleX = 1 - widthProportionLost; 1139 // Find how much height proportion we should be taking off (e.g. 0.1f) 1140 float heightProportionLost = maxShrinkY * (1 - interpolatedGrowProgress); 1141 shrinkScaleY = 1 - heightProportionLost; 1142 // Add a small amount to the leash's position to keep the task centered. 1143 shrinkDiffX = (width * widthProportionLost) / 2; 1144 shrinkDiffY = (height * heightProportionLost) / 2; 1145 } 1146 1147 instantaneousX += shrinkDiffX; 1148 instantaneousY += shrinkDiffY; 1149 width *= shrinkScaleX; 1150 height *= shrinkScaleY; 1151 // Set scale on the leash's contents. 1152 t.setScale(leash, shrinkScaleX, shrinkScaleY); 1153 } 1154 1155 // Set layers 1156 if (taskInfo != null) { 1157 t.setLayer(leash, isGoingBehind 1158 ? ANIMATING_BACK_APP_LAYER 1159 : ANIMATING_FRONT_APP_LAYER); 1160 } else { 1161 t.setLayer(leash, ANIMATING_DIVIDER_LAYER); 1162 } 1163 1164 if (offsetX == 0 && offsetY == 0) { 1165 t.setPosition(leash, instantaneousX, instantaneousY); 1166 mTempRect.set((int) instantaneousX, (int) instantaneousY, 1167 (int) (instantaneousX + width), (int) (instantaneousY + height)); 1168 t.setWindowCrop(leash, width, height); 1169 if (addVeil) { 1170 decorManager.drawNextVeilFrameForSwapAnimation( 1171 taskInfo, mTempRect, t, isGoingBehind, leash, 0, 0); 1172 } 1173 } else { 1174 final int diffOffsetX = (int) (moveProgress * offsetX); 1175 final int diffOffsetY = (int) (moveProgress * offsetY); 1176 t.setPosition(leash, instantaneousX + diffOffsetX, instantaneousY + diffOffsetY); 1177 mTempRect.set(0, 0, width, height); 1178 mTempRect.offsetTo(-diffOffsetX, -diffOffsetY); 1179 t.setCrop(leash, mTempRect); 1180 if (addVeil) { 1181 decorManager.drawNextVeilFrameForSwapAnimation( 1182 taskInfo, mTempRect, t, isGoingBehind, leash, diffOffsetX, diffOffsetY); 1183 } 1184 } 1185 t.apply(); 1186 }); 1187 return animator; 1188 } 1189 getDisplayStableInsets(Context context)1190 private Rect getDisplayStableInsets(Context context) { 1191 final DisplayLayout displayLayout = 1192 mDisplayController.getDisplayLayout(context.getDisplayId()); 1193 return displayLayout != null 1194 ? displayLayout.stableInsets() 1195 : context.getSystemService(WindowManager.class) 1196 .getMaximumWindowMetrics() 1197 .getWindowInsets() 1198 .getInsetsIgnoringVisibility(WindowInsets.Type.systemBars() 1199 | WindowInsets.Type.displayCutout()) 1200 .toRect(); 1201 } 1202 1203 /** 1204 * @return {@code true} if we should create a left-right split, {@code false} if we should 1205 * create a top-bottom split. 1206 */ isLeftRightSplit()1207 public boolean isLeftRightSplit() { 1208 return mIsLeftRightSplit; 1209 } 1210 1211 /** Apply recorded surface layout to the {@link SurfaceControl.Transaction}. */ applySurfaceChanges(SurfaceControl.Transaction t, SurfaceControl leash1, SurfaceControl leash2, SurfaceControl dimLayer1, SurfaceControl dimLayer2, boolean applyResizingOffset)1212 public void applySurfaceChanges(SurfaceControl.Transaction t, SurfaceControl leash1, 1213 SurfaceControl leash2, SurfaceControl dimLayer1, SurfaceControl dimLayer2, 1214 boolean applyResizingOffset) { 1215 final SurfaceControl dividerLeash = getDividerLeash(); 1216 if (dividerLeash != null) { 1217 getRefDividerBounds(mTempRect); 1218 t.setPosition(dividerLeash, mTempRect.left, mTempRect.top); 1219 // Resets layer of divider bar to make sure it is always on top. 1220 t.setLayer(dividerLeash, RESTING_DIVIDER_LAYER); 1221 } 1222 if (dimLayer1 != null) { 1223 t.setLayer(dimLayer1, RESTING_DIM_LAYER); 1224 } 1225 if (dimLayer2 != null) { 1226 t.setLayer(dimLayer2, RESTING_DIM_LAYER); 1227 } 1228 copyTopLeftRefBounds(mTempRect); 1229 t.setPosition(leash1, mTempRect.left, mTempRect.top) 1230 .setWindowCrop(leash1, mTempRect.width(), mTempRect.height()); 1231 copyBottomRightRefBounds(mTempRect); 1232 t.setPosition(leash2, mTempRect.left, mTempRect.top) 1233 .setWindowCrop(leash2, mTempRect.width(), mTempRect.height()); 1234 1235 if (mImePositionProcessor.adjustSurfaceLayoutForIme( 1236 t, dividerLeash, leash1, leash2, dimLayer1, dimLayer2)) { 1237 return; 1238 } 1239 1240 mSurfaceEffectPolicy.adjustDimSurface(t, dimLayer1, dimLayer2); 1241 if (applyResizingOffset) { 1242 mSurfaceEffectPolicy.adjustRootSurface(t, leash1, leash2); 1243 } 1244 } 1245 1246 /** Apply recorded task layout to the {@link WindowContainerTransaction}. 1247 * 1248 * @return true if stage bounds actually update. 1249 */ applyTaskChanges(WindowContainerTransaction wct, ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2)1250 public boolean applyTaskChanges(WindowContainerTransaction wct, 1251 ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2) { 1252 boolean boundsChanged = false; 1253 if (!getTopLeftBounds().equals(getTopLeftContentBounds()) 1254 || !task1.token.equals(mWinToken1)) { 1255 setTaskBounds(wct, task1, getTopLeftBounds()); 1256 getTopLeftContentBounds().set(getTopLeftBounds()); 1257 mWinToken1 = task1.token; 1258 boundsChanged = true; 1259 } 1260 if (!getBottomRightBounds().equals(getBottomRightContentBounds()) 1261 || !task2.token.equals(mWinToken2)) { 1262 setTaskBounds(wct, task2, getBottomRightBounds()); 1263 getBottomRightContentBounds().set(getBottomRightBounds()); 1264 mWinToken2 = task2.token; 1265 boundsChanged = true; 1266 } 1267 return boundsChanged; 1268 } 1269 1270 /** Set bounds to the {@link WindowContainerTransaction} for single task. */ setTaskBounds(WindowContainerTransaction wct, ActivityManager.RunningTaskInfo task, Rect bounds)1271 public void setTaskBounds(WindowContainerTransaction wct, 1272 ActivityManager.RunningTaskInfo task, Rect bounds) { 1273 wct.setBounds(task.token, bounds); 1274 wct.setSmallestScreenWidthDp(task.token, getSmallestWidthDp(bounds)); 1275 } 1276 getSmallestWidthDp(Rect bounds)1277 private int getSmallestWidthDp(Rect bounds) { 1278 mTempRect.set(bounds); 1279 mTempRect.inset(getDisplayStableInsets(mContext)); 1280 final int minWidth = Math.min(mTempRect.width(), mTempRect.height()); 1281 final float density = mContext.getResources().getDisplayMetrics().density; 1282 return (int) (minWidth / density); 1283 } 1284 getDisplayWidth()1285 public int getDisplayWidth() { 1286 return mRootBounds.width(); 1287 } 1288 getDisplayHeight()1289 public int getDisplayHeight() { 1290 return mRootBounds.height(); 1291 } 1292 1293 /** 1294 * Shift configuration bounds to prevent client apps get configuration changed or relaunch. And 1295 * restore shifted configuration bounds if it's no longer shifted. 1296 */ applyLayoutOffsetTarget(WindowContainerTransaction wct, int offsetX, int offsetY, ActivityManager.RunningTaskInfo taskInfo1, ActivityManager.RunningTaskInfo taskInfo2)1297 public void applyLayoutOffsetTarget(WindowContainerTransaction wct, int offsetX, int offsetY, 1298 ActivityManager.RunningTaskInfo taskInfo1, ActivityManager.RunningTaskInfo taskInfo2) { 1299 if (offsetX == 0 && offsetY == 0) { 1300 wct.setBounds(taskInfo1.token, getTopLeftBounds()); 1301 wct.setScreenSizeDp(taskInfo1.token, 1302 SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED); 1303 1304 wct.setBounds(taskInfo2.token, getBottomRightBounds()); 1305 wct.setScreenSizeDp(taskInfo2.token, 1306 SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED); 1307 } else { 1308 copyTopLeftBounds(mTempRect); 1309 mTempRect.offset(offsetX, offsetY); 1310 wct.setBounds(taskInfo1.token, mTempRect); 1311 wct.setScreenSizeDp(taskInfo1.token, 1312 taskInfo1.configuration.screenWidthDp, 1313 taskInfo1.configuration.screenHeightDp); 1314 1315 copyBottomRightBounds(mTempRect); 1316 mTempRect.offset(offsetX, offsetY); 1317 wct.setBounds(taskInfo2.token, mTempRect); 1318 wct.setScreenSizeDp(taskInfo2.token, 1319 taskInfo2.configuration.screenWidthDp, 1320 taskInfo2.configuration.screenHeightDp); 1321 } 1322 } 1323 1324 /** Dumps the current split bounds recorded in this layout. */ dump(@onNull PrintWriter pw, String prefix)1325 public void dump(@NonNull PrintWriter pw, String prefix) { 1326 final String innerPrefix = prefix + "\t"; 1327 pw.println(prefix + TAG + ":"); 1328 pw.println(innerPrefix + "mAllowLeftRightSplitInPortrait=" + mAllowLeftRightSplitInPortrait); 1329 pw.println(innerPrefix + "mIsLeftRightSplit=" + mIsLeftRightSplit); 1330 pw.println(innerPrefix + "mFreezeDividerWindow=" + mFreezeDividerWindow); 1331 pw.println(innerPrefix + "mDimNonImeSide=" + mDimNonImeSide); 1332 pw.println(innerPrefix + "mDividerPosition=" + mDividerPosition); 1333 pw.println(innerPrefix + "bounds1=" + getTopLeftBounds().toShortString()); 1334 pw.println(innerPrefix + "dividerBounds=" + mDividerBounds.toShortString()); 1335 pw.println(innerPrefix + "bounds2=" + getBottomRightBounds().toShortString()); 1336 } 1337 1338 /** Handles layout change event. */ 1339 public interface SplitLayoutHandler { 1340 1341 /** Calls when dismissing split. */ onSnappedToDismiss(boolean snappedToEnd, int reason)1342 void onSnappedToDismiss(boolean snappedToEnd, int reason); 1343 1344 /** 1345 * Calls when resizing the split bounds. 1346 * 1347 * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl, 1348 * SurfaceControl, SurfaceControl, boolean) 1349 */ onLayoutSizeChanging(SplitLayout layout, int offsetX, int offsetY, boolean shouldUseParallaxEffect)1350 void onLayoutSizeChanging(SplitLayout layout, int offsetX, int offsetY, 1351 boolean shouldUseParallaxEffect); 1352 1353 /** 1354 * Calls when finish resizing the split bounds. 1355 * 1356 * @see #applyTaskChanges(WindowContainerTransaction, ActivityManager.RunningTaskInfo, 1357 * ActivityManager.RunningTaskInfo) 1358 * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl, 1359 * SurfaceControl, SurfaceControl, boolean) 1360 */ onLayoutSizeChanged(SplitLayout layout)1361 void onLayoutSizeChanged(SplitLayout layout); 1362 1363 /** 1364 * Calls when re-positioning the split bounds. Like moving split bounds while showing IME 1365 * panel. 1366 * 1367 * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl, 1368 * SurfaceControl, SurfaceControl, boolean) 1369 */ onLayoutPositionChanging(SplitLayout layout)1370 void onLayoutPositionChanging(SplitLayout layout); 1371 1372 /** 1373 * Notifies the target offset for shifting layout. So layout handler can shift configuration 1374 * bounds correspondingly to make sure client apps won't get configuration changed or 1375 * relaunched. If the layout is no longer shifted, layout handler should restore shifted 1376 * configuration bounds. 1377 * 1378 * @see #applyLayoutOffsetTarget(WindowContainerTransaction, int, int, 1379 * ActivityManager.RunningTaskInfo, ActivityManager.RunningTaskInfo) 1380 */ setLayoutOffsetTarget(int offsetX, int offsetY, SplitLayout layout)1381 void setLayoutOffsetTarget(int offsetX, int offsetY, SplitLayout layout); 1382 1383 /** Calls when user double tapped on the divider bar. */ onDoubleTappedDivider()1384 default void onDoubleTappedDivider() { 1385 } 1386 1387 /** 1388 * Sets the excludedInsetsTypes for the IME in the root WindowContainer. 1389 */ setExcludeImeInsets(boolean exclude)1390 void setExcludeImeInsets(boolean exclude); 1391 1392 /** Returns split position of the token. */ 1393 @SplitPosition getSplitItemPosition(WindowContainerToken token)1394 int getSplitItemPosition(WindowContainerToken token); 1395 } 1396 1397 /** Records IME top offset changes and updates SplitLayout correspondingly. */ 1398 private class ImePositionProcessor implements DisplayImeController.ImePositionProcessor { 1399 /** 1400 * Maximum size of an adjusted split bounds relative to original stack bounds. Used to 1401 * restrict IME adjustment so that a min portion of top split remains visible. 1402 */ 1403 private static final float ADJUSTED_SPLIT_FRACTION_MAX = 0.7f; 1404 private static final float ADJUSTED_NONFOCUS_DIM = 0.3f; 1405 1406 private final int mDisplayId; 1407 1408 private boolean mHasImeFocus; 1409 private boolean mImeShown; 1410 private int mYOffsetForIme; 1411 private float mDimValue1; 1412 private float mDimValue2; 1413 1414 private int mStartImeTop; 1415 private int mEndImeTop; 1416 1417 private int mTargetYOffset; 1418 private int mLastYOffset; 1419 private float mTargetDim1; 1420 private float mTargetDim2; 1421 private float mLastDim1; 1422 private float mLastDim2; 1423 ImePositionProcessor(int displayId)1424 private ImePositionProcessor(int displayId) { 1425 mDisplayId = displayId; 1426 } 1427 1428 @Override onImeRequested(int displayId, boolean isRequested)1429 public void onImeRequested(int displayId, boolean isRequested) { 1430 if (displayId != mDisplayId) return; 1431 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "IME was set to requested=%s", 1432 isRequested); 1433 mSplitLayoutHandler.setExcludeImeInsets(true); 1434 } 1435 1436 @Override onImeStartPositioning(int displayId, int hiddenTop, int shownTop, boolean showing, boolean isFloating, SurfaceControl.Transaction t)1437 public int onImeStartPositioning(int displayId, int hiddenTop, int shownTop, 1438 boolean showing, boolean isFloating, SurfaceControl.Transaction t) { 1439 if (displayId != mDisplayId || !mInitialized) { 1440 return 0; 1441 } 1442 1443 final int imeTargetPosition = getImeTargetPosition(); 1444 mHasImeFocus = imeTargetPosition != SPLIT_POSITION_UNDEFINED; 1445 if (!mHasImeFocus) { 1446 if (!android.view.inputmethod.Flags.refactorInsetsController() || showing) { 1447 return 0; 1448 } 1449 } 1450 1451 mStartImeTop = showing ? hiddenTop : shownTop; 1452 mEndImeTop = showing ? shownTop : hiddenTop; 1453 mImeShown = showing; 1454 1455 // Update target dim values 1456 mLastDim1 = mDimValue1; 1457 mTargetDim1 = imeTargetPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT && mImeShown 1458 && mDimNonImeSide ? ADJUSTED_NONFOCUS_DIM : 0.0f; 1459 mLastDim2 = mDimValue2; 1460 mTargetDim2 = imeTargetPosition == SPLIT_POSITION_TOP_OR_LEFT && mImeShown 1461 && mDimNonImeSide ? ADJUSTED_NONFOCUS_DIM : 0.0f; 1462 1463 // Calculate target bounds offset for IME 1464 mLastYOffset = mYOffsetForIme; 1465 final boolean needOffset = imeTargetPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT 1466 && !isFloating && !mIsLeftRightSplit && mImeShown; 1467 mTargetYOffset = needOffset ? getTargetYOffset() : 0; 1468 1469 if (mTargetYOffset != mLastYOffset) { 1470 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, 1471 "Split IME animation starting, fromY=%d toY=%d", 1472 mLastYOffset, mTargetYOffset); 1473 // Freeze the configuration size with offset to prevent app get a configuration 1474 // changed or relaunch. This is required to make sure client apps will calculate 1475 // insets properly after layout shifted. 1476 mSplitLayoutHandler.setLayoutOffsetTarget(0, mTargetYOffset, SplitLayout.this); 1477 } 1478 1479 // Make {@link DividerView} non-interactive while IME showing in split mode. Listen to 1480 // ImePositionProcessor#onImeVisibilityChanged directly in DividerView is not enough 1481 // because DividerView won't receive onImeVisibilityChanged callback after it being 1482 // re-inflated. 1483 setDividerInteractive(!mImeShown || !mHasImeFocus || isFloating, true, 1484 "onImeStartPositioning"); 1485 1486 if (android.view.inputmethod.Flags.refactorInsetsController()) { 1487 if (mImeShown) { 1488 mSplitLayoutHandler.setExcludeImeInsets(false); 1489 } 1490 } 1491 1492 return mTargetYOffset != mLastYOffset ? IME_ANIMATION_NO_ALPHA : 0; 1493 } 1494 1495 @Override onImePositionChanged(int displayId, int imeTop, SurfaceControl.Transaction t)1496 public void onImePositionChanged(int displayId, int imeTop, SurfaceControl.Transaction t) { 1497 if (displayId != mDisplayId || !mHasImeFocus) { 1498 if (!android.view.inputmethod.Flags.refactorInsetsController() || mImeShown) { 1499 return; 1500 } 1501 } 1502 onProgress(getProgress(imeTop)); 1503 mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this); 1504 } 1505 1506 @Override onImeEndPositioning(int displayId, boolean cancel, SurfaceControl.Transaction t)1507 public void onImeEndPositioning(int displayId, boolean cancel, 1508 SurfaceControl.Transaction t) { 1509 if (displayId != mDisplayId || cancel) return; 1510 if (!mHasImeFocus) { 1511 if (!android.view.inputmethod.Flags.refactorInsetsController() || mImeShown) { 1512 return; 1513 } 1514 } 1515 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, 1516 "Split IME animation ending, canceled=%b", cancel); 1517 onProgress(1.0f); 1518 mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this); 1519 if (android.view.inputmethod.Flags.refactorInsetsController()) { 1520 if (!mImeShown) { 1521 // The IME hide animation is started immediately and at that point, the IME 1522 // insets are not yet set to hidden. Therefore only resetting the 1523 // excludedTypes at the end of the animation. Note: InsetsPolicy will only 1524 // set the IME height to zero, when it is visible. When it becomes invisible, 1525 // we dispatch the insets (the height there is zero as well) 1526 mSplitLayoutHandler.setExcludeImeInsets(false); 1527 } 1528 } 1529 } 1530 1531 @Override onImeControlTargetChanged(int displayId, boolean controlling)1532 public void onImeControlTargetChanged(int displayId, boolean controlling) { 1533 if (displayId != mDisplayId) return; 1534 // Restore the split layout when wm-shell is not controlling IME insets anymore. 1535 if (!controlling && mImeShown) { 1536 reset(); 1537 setDividerInteractive(true, true, "onImeControlTargetChanged"); 1538 mSplitLayoutHandler.setLayoutOffsetTarget(0, 0, SplitLayout.this); 1539 mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this); 1540 } 1541 } 1542 1543 /** 1544 * When IME is triggered on the bottom app in split screen, we want to translate the bottom 1545 * app up by a certain amount so that it's not covered too much by the IME. But there's also 1546 * an upper limit to the amount we want to translate (since we still need some of the top 1547 * app to be visible too). So this function essentially says "try to translate the bottom 1548 * app up, but stop before you make the top app too small." 1549 */ getTargetYOffset()1550 private int getTargetYOffset() { 1551 // We want to translate up the bottom app by this amount. 1552 final int desiredOffset = Math.abs(mEndImeTop - mStartImeTop); 1553 1554 // But we also want to keep this much of the top app visible. 1555 final float amountOfTopAppToKeepVisible = 1556 getTopLeftBounds().height() * (1 - ADJUSTED_SPLIT_FRACTION_MAX); 1557 1558 // So the current onscreen size of the top app, minus the minimum size, is the max 1559 // translation we will allow. 1560 final float currentOnScreenSizeOfTopApp = getTopLeftBounds().bottom; 1561 final int maxOffset = 1562 (int) Math.max(currentOnScreenSizeOfTopApp - amountOfTopAppToKeepVisible, 0); 1563 1564 return -Math.min(desiredOffset, maxOffset); 1565 } 1566 1567 @SplitPosition getImeTargetPosition()1568 private int getImeTargetPosition() { 1569 final WindowContainerToken token = mTaskOrganizer.getImeTarget(mDisplayId); 1570 return mSplitLayoutHandler.getSplitItemPosition(token); 1571 } 1572 getProgress(int currImeTop)1573 private float getProgress(int currImeTop) { 1574 return ((float) currImeTop - mStartImeTop) / (mEndImeTop - mStartImeTop); 1575 } 1576 onProgress(float progress)1577 private void onProgress(float progress) { 1578 mDimValue1 = getProgressValue(mLastDim1, mTargetDim1, progress); 1579 mDimValue2 = getProgressValue(mLastDim2, mTargetDim2, progress); 1580 mYOffsetForIme = 1581 (int) getProgressValue((float) mLastYOffset, (float) mTargetYOffset, progress); 1582 } 1583 getProgressValue(float start, float end, float progress)1584 private float getProgressValue(float start, float end, float progress) { 1585 return start + (end - start) * progress; 1586 } 1587 reset()1588 void reset() { 1589 mHasImeFocus = false; 1590 mImeShown = false; 1591 mYOffsetForIme = mLastYOffset = mTargetYOffset = 0; 1592 mDimValue1 = mLastDim1 = mTargetDim1 = 0.0f; 1593 mDimValue2 = mLastDim2 = mTargetDim2 = 0.0f; 1594 } 1595 1596 /** 1597 * Adjusts surface layout while showing IME. 1598 * 1599 * @return {@code false} if there's no need to adjust, otherwise {@code true} 1600 */ adjustSurfaceLayoutForIme(SurfaceControl.Transaction t, SurfaceControl dividerLeash, SurfaceControl leash1, SurfaceControl leash2, SurfaceControl dimLayer1, SurfaceControl dimLayer2)1601 boolean adjustSurfaceLayoutForIme(SurfaceControl.Transaction t, 1602 SurfaceControl dividerLeash, SurfaceControl leash1, SurfaceControl leash2, 1603 SurfaceControl dimLayer1, SurfaceControl dimLayer2) { 1604 final boolean showDim = mDimValue1 > 0.001f || mDimValue2 > 0.001f; 1605 boolean adjusted = false; 1606 if (mYOffsetForIme != 0) { 1607 if (dividerLeash != null) { 1608 getRefDividerBounds(mTempRect); 1609 mTempRect.offset(0, mYOffsetForIme); 1610 t.setPosition(dividerLeash, mTempRect.left, mTempRect.top); 1611 } 1612 1613 copyTopLeftRefBounds(mTempRect); 1614 mTempRect.offset(0, mYOffsetForIme); 1615 t.setPosition(leash1, mTempRect.left, mTempRect.top); 1616 1617 copyBottomRightRefBounds(mTempRect); 1618 mTempRect.offset(0, mYOffsetForIme); 1619 t.setPosition(leash2, mTempRect.left, mTempRect.top); 1620 adjusted = true; 1621 } 1622 1623 if (showDim) { 1624 t.setAlpha(dimLayer1, mDimValue1).setVisibility(dimLayer1, mDimValue1 > 0.001f); 1625 t.setAlpha(dimLayer2, mDimValue2).setVisibility(dimLayer2, mDimValue2 > 0.001f); 1626 adjusted = true; 1627 } 1628 return adjusted; 1629 } 1630 } 1631 } 1632