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 import static android.view.WindowManagerPolicyConstants.SPLIT_DIVIDER_LAYER; 27 28 import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_END; 29 import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_START; 30 import static com.android.wm.shell.animation.Interpolators.DIM_INTERPOLATOR; 31 import static com.android.wm.shell.animation.Interpolators.SLOWDOWN_INTERPOLATOR; 32 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; 33 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; 34 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; 35 36 import android.animation.Animator; 37 import android.animation.AnimatorListenerAdapter; 38 import android.animation.ValueAnimator; 39 import android.annotation.NonNull; 40 import android.app.ActivityManager; 41 import android.content.Context; 42 import android.content.res.Configuration; 43 import android.content.res.Resources; 44 import android.graphics.Point; 45 import android.graphics.Rect; 46 import android.view.Display; 47 import android.view.InsetsSourceControl; 48 import android.view.InsetsState; 49 import android.view.RoundedCorner; 50 import android.view.SurfaceControl; 51 import android.view.WindowInsets; 52 import android.view.WindowManager; 53 import android.window.WindowContainerToken; 54 import android.window.WindowContainerTransaction; 55 56 import androidx.annotation.Nullable; 57 58 import com.android.internal.annotations.VisibleForTesting; 59 import com.android.internal.policy.DividerSnapAlgorithm; 60 import com.android.internal.policy.DockedDividerUtils; 61 import com.android.wm.shell.R; 62 import com.android.wm.shell.ShellTaskOrganizer; 63 import com.android.wm.shell.animation.Interpolators; 64 import com.android.wm.shell.common.DisplayImeController; 65 import com.android.wm.shell.common.DisplayInsetsController; 66 import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; 67 68 import java.io.PrintWriter; 69 70 /** 71 * Records and handles layout of splits. Helps to calculate proper bounds when configuration or 72 * divide position changes. 73 */ 74 public final class SplitLayout implements DisplayInsetsController.OnInsetsChangedListener { 75 76 private final int mDividerWindowWidth; 77 private final int mDividerInsets; 78 private final int mDividerSize; 79 80 private final Rect mTempRect = new Rect(); 81 private final Rect mRootBounds = new Rect(); 82 private final Rect mDividerBounds = new Rect(); 83 private final Rect mBounds1 = new Rect(); 84 private final Rect mBounds2 = new Rect(); 85 private final Rect mWinBounds1 = new Rect(); 86 private final Rect mWinBounds2 = new Rect(); 87 private final SplitLayoutHandler mSplitLayoutHandler; 88 private final SplitWindowManager mSplitWindowManager; 89 private final DisplayImeController mDisplayImeController; 90 private final ImePositionProcessor mImePositionProcessor; 91 private final DismissingEffectPolicy mDismissingEffectPolicy; 92 private final ShellTaskOrganizer mTaskOrganizer; 93 private final InsetsState mInsetsState = new InsetsState(); 94 95 private Context mContext; 96 private DividerSnapAlgorithm mDividerSnapAlgorithm; 97 private WindowContainerToken mWinToken1; 98 private WindowContainerToken mWinToken2; 99 private int mDividePosition; 100 private boolean mInitialized = false; 101 private int mOrientation; 102 private int mRotation; 103 SplitLayout(String windowName, Context context, Configuration configuration, SplitLayoutHandler splitLayoutHandler, SplitWindowManager.ParentContainerCallbacks parentContainerCallbacks, DisplayImeController displayImeController, ShellTaskOrganizer taskOrganizer, boolean applyDismissingParallax)104 public SplitLayout(String windowName, Context context, Configuration configuration, 105 SplitLayoutHandler splitLayoutHandler, 106 SplitWindowManager.ParentContainerCallbacks parentContainerCallbacks, 107 DisplayImeController displayImeController, ShellTaskOrganizer taskOrganizer, 108 boolean applyDismissingParallax) { 109 mContext = context.createConfigurationContext(configuration); 110 mOrientation = configuration.orientation; 111 mRotation = configuration.windowConfiguration.getRotation(); 112 mSplitLayoutHandler = splitLayoutHandler; 113 mDisplayImeController = displayImeController; 114 mSplitWindowManager = new SplitWindowManager(windowName, mContext, configuration, 115 parentContainerCallbacks); 116 mTaskOrganizer = taskOrganizer; 117 mImePositionProcessor = new ImePositionProcessor(mContext.getDisplayId()); 118 mDismissingEffectPolicy = new DismissingEffectPolicy(applyDismissingParallax); 119 120 final Resources resources = context.getResources(); 121 mDividerSize = resources.getDimensionPixelSize(R.dimen.split_divider_bar_width); 122 mDividerInsets = getDividerInsets(resources, context.getDisplay()); 123 mDividerWindowWidth = mDividerSize + 2 * mDividerInsets; 124 125 mRootBounds.set(configuration.windowConfiguration.getBounds()); 126 mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds); 127 resetDividerPosition(); 128 } 129 getDividerInsets(Resources resources, Display display)130 private int getDividerInsets(Resources resources, Display display) { 131 final int dividerInset = resources.getDimensionPixelSize( 132 com.android.internal.R.dimen.docked_stack_divider_insets); 133 134 int radius = 0; 135 RoundedCorner corner = display.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT); 136 radius = corner != null ? Math.max(radius, corner.getRadius()) : radius; 137 corner = display.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT); 138 radius = corner != null ? Math.max(radius, corner.getRadius()) : radius; 139 corner = display.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT); 140 radius = corner != null ? Math.max(radius, corner.getRadius()) : radius; 141 corner = display.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT); 142 radius = corner != null ? Math.max(radius, corner.getRadius()) : radius; 143 144 return Math.max(dividerInset, radius); 145 } 146 147 /** Gets bounds of the primary split. */ getBounds1()148 public Rect getBounds1() { 149 return new Rect(mBounds1); 150 } 151 152 /** Gets bounds of the secondary split. */ getBounds2()153 public Rect getBounds2() { 154 return new Rect(mBounds2); 155 } 156 157 /** Gets bounds of divider window. */ getDividerBounds()158 public Rect getDividerBounds() { 159 return new Rect(mDividerBounds); 160 } 161 162 /** Returns leash of the current divider bar. */ 163 @Nullable getDividerLeash()164 public SurfaceControl getDividerLeash() { 165 return mSplitWindowManager == null ? null : mSplitWindowManager.getSurfaceControl(); 166 } 167 getDividePosition()168 int getDividePosition() { 169 return mDividePosition; 170 } 171 172 /** 173 * Returns the divider position as a fraction from 0 to 1. 174 */ getDividerPositionAsFraction()175 public float getDividerPositionAsFraction() { 176 return Math.min(1f, Math.max(0f, isLandscape() 177 ? (float) ((mBounds1.right + mBounds2.left) / 2f) / mBounds2.right 178 : (float) ((mBounds1.bottom + mBounds2.top) / 2f) / mBounds2.bottom)); 179 } 180 181 /** Applies new configuration, returns {@code false} if there's no effect to the layout. */ updateConfiguration(Configuration configuration)182 public boolean updateConfiguration(Configuration configuration) { 183 boolean affectsLayout = false; 184 185 // Update the split bounds when necessary. Besides root bounds changed, split bounds need to 186 // be updated when the rotation changed to cover the case that users rotated the screen 180 187 // degrees. 188 // Make sure to render the divider bar with proper resources that matching the screen 189 // orientation. 190 final int rotation = configuration.windowConfiguration.getRotation(); 191 final Rect rootBounds = configuration.windowConfiguration.getBounds(); 192 final int orientation = configuration.orientation; 193 194 if (mOrientation == orientation 195 && rotation == mRotation 196 && mRootBounds.equals(rootBounds)) { 197 return false; 198 } 199 200 mContext = mContext.createConfigurationContext(configuration); 201 mSplitWindowManager.setConfiguration(configuration); 202 mOrientation = orientation; 203 mTempRect.set(mRootBounds); 204 mRootBounds.set(rootBounds); 205 mRotation = rotation; 206 mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds); 207 initDividerPosition(mTempRect); 208 209 if (mInitialized) { 210 release(); 211 init(); 212 } 213 214 return true; 215 } 216 initDividerPosition(Rect oldBounds)217 private void initDividerPosition(Rect oldBounds) { 218 final float snapRatio = (float) mDividePosition 219 / (float) (isLandscape(oldBounds) ? oldBounds.width() : oldBounds.height()); 220 // Estimate position by previous ratio. 221 final float length = 222 (float) (isLandscape() ? mRootBounds.width() : mRootBounds.height()); 223 final int estimatePosition = (int) (length * snapRatio); 224 // Init divider position by estimated position using current bounds snap algorithm. 225 mDividePosition = mDividerSnapAlgorithm.calculateNonDismissingSnapTarget( 226 estimatePosition).position; 227 updateBounds(mDividePosition); 228 } 229 230 /** Updates recording bounds of divider window and both of the splits. */ updateBounds(int position)231 private void updateBounds(int position) { 232 mDividerBounds.set(mRootBounds); 233 mBounds1.set(mRootBounds); 234 mBounds2.set(mRootBounds); 235 final boolean isLandscape = isLandscape(mRootBounds); 236 if (isLandscape) { 237 position += mRootBounds.left; 238 mDividerBounds.left = position - mDividerInsets; 239 mDividerBounds.right = mDividerBounds.left + mDividerWindowWidth; 240 mBounds1.right = position; 241 mBounds2.left = mBounds1.right + mDividerSize; 242 } else { 243 position += mRootBounds.top; 244 mDividerBounds.top = position - mDividerInsets; 245 mDividerBounds.bottom = mDividerBounds.top + mDividerWindowWidth; 246 mBounds1.bottom = position; 247 mBounds2.top = mBounds1.bottom + mDividerSize; 248 } 249 DockedDividerUtils.sanitizeStackBounds(mBounds1, true /** topLeft */); 250 DockedDividerUtils.sanitizeStackBounds(mBounds2, false /** topLeft */); 251 mDismissingEffectPolicy.applyDividerPosition(position, isLandscape); 252 } 253 254 /** Inflates {@link DividerView} on the root surface. */ init()255 public void init() { 256 if (mInitialized) return; 257 mInitialized = true; 258 mSplitWindowManager.init(this, mInsetsState); 259 mDisplayImeController.addPositionProcessor(mImePositionProcessor); 260 } 261 262 /** Releases the surface holding the current {@link DividerView}. */ release()263 public void release() { 264 if (!mInitialized) return; 265 mInitialized = false; 266 mSplitWindowManager.release(); 267 mDisplayImeController.removePositionProcessor(mImePositionProcessor); 268 mImePositionProcessor.reset(); 269 } 270 271 @Override insetsChanged(InsetsState insetsState)272 public void insetsChanged(InsetsState insetsState) { 273 mInsetsState.set(insetsState); 274 if (!mInitialized) { 275 return; 276 } 277 mSplitWindowManager.onInsetsChanged(insetsState); 278 } 279 280 @Override insetsControlChanged(InsetsState insetsState, InsetsSourceControl[] activeControls)281 public void insetsControlChanged(InsetsState insetsState, 282 InsetsSourceControl[] activeControls) { 283 if (!mInsetsState.equals(insetsState)) { 284 insetsChanged(insetsState); 285 } 286 } 287 288 /** 289 * Updates bounds with the passing position. Usually used to update recording bounds while 290 * performing animation or dragging divider bar to resize the splits. 291 */ updateDivideBounds(int position)292 void updateDivideBounds(int position) { 293 updateBounds(position); 294 mSplitLayoutHandler.onLayoutSizeChanging(this); 295 } 296 setDividePosition(int position)297 void setDividePosition(int position) { 298 mDividePosition = position; 299 updateBounds(mDividePosition); 300 mSplitLayoutHandler.onLayoutSizeChanged(this); 301 } 302 303 /** Sets divide position base on the ratio within root bounds. */ setDivideRatio(float ratio)304 public void setDivideRatio(float ratio) { 305 final int position = isLandscape() 306 ? mRootBounds.left + (int) (mRootBounds.width() * ratio) 307 : mRootBounds.top + (int) (mRootBounds.height() * ratio); 308 DividerSnapAlgorithm.SnapTarget snapTarget = 309 mDividerSnapAlgorithm.calculateNonDismissingSnapTarget(position); 310 setDividePosition(snapTarget.position); 311 } 312 313 /** Resets divider position. */ resetDividerPosition()314 public void resetDividerPosition() { 315 mDividePosition = mDividerSnapAlgorithm.getMiddleTarget().position; 316 updateBounds(mDividePosition); 317 mWinToken1 = null; 318 mWinToken2 = null; 319 mWinBounds1.setEmpty(); 320 mWinBounds2.setEmpty(); 321 } 322 323 /** 324 * Sets new divide position and updates bounds correspondingly. Notifies listener if the new 325 * target indicates dismissing split. 326 */ snapToTarget(int currentPosition, DividerSnapAlgorithm.SnapTarget snapTarget)327 public void snapToTarget(int currentPosition, DividerSnapAlgorithm.SnapTarget snapTarget) { 328 switch (snapTarget.flag) { 329 case FLAG_DISMISS_START: 330 flingDividePosition(currentPosition, snapTarget.position, 331 () -> mSplitLayoutHandler.onSnappedToDismiss(false /* bottomOrRight */)); 332 break; 333 case FLAG_DISMISS_END: 334 flingDividePosition(currentPosition, snapTarget.position, 335 () -> mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */)); 336 break; 337 default: 338 flingDividePosition(currentPosition, snapTarget.position, null); 339 break; 340 } 341 } 342 onDoubleTappedDivider()343 void onDoubleTappedDivider() { 344 mSplitLayoutHandler.onDoubleTappedDivider(); 345 } 346 347 /** 348 * Returns {@link DividerSnapAlgorithm.SnapTarget} which matches passing position and velocity. 349 * If hardDismiss is set to {@code true}, it will be harder to reach dismiss target. 350 */ findSnapTarget(int position, float velocity, boolean hardDismiss)351 public DividerSnapAlgorithm.SnapTarget findSnapTarget(int position, float velocity, 352 boolean hardDismiss) { 353 return mDividerSnapAlgorithm.calculateSnapTarget(position, velocity, hardDismiss); 354 } 355 getSnapAlgorithm(Context context, Rect rootBounds)356 private DividerSnapAlgorithm getSnapAlgorithm(Context context, Rect rootBounds) { 357 final boolean isLandscape = isLandscape(rootBounds); 358 return new DividerSnapAlgorithm( 359 context.getResources(), 360 rootBounds.width(), 361 rootBounds.height(), 362 mDividerSize, 363 !isLandscape, 364 getDisplayInsets(context), 365 isLandscape ? DOCKED_LEFT : DOCKED_TOP /* dockSide */); 366 } 367 368 @VisibleForTesting flingDividePosition(int from, int to, @Nullable Runnable flingFinishedCallback)369 void flingDividePosition(int from, int to, @Nullable Runnable flingFinishedCallback) { 370 if (from == to) { 371 // No animation run, still callback to stop resizing. 372 mSplitLayoutHandler.onLayoutSizeChanged(this); 373 return; 374 } 375 ValueAnimator animator = ValueAnimator 376 .ofInt(from, to) 377 .setDuration(250); 378 animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 379 animator.addUpdateListener( 380 animation -> updateDivideBounds((int) animation.getAnimatedValue())); 381 animator.addListener(new AnimatorListenerAdapter() { 382 @Override 383 public void onAnimationEnd(Animator animation) { 384 setDividePosition(to); 385 if (flingFinishedCallback != null) { 386 flingFinishedCallback.run(); 387 } 388 } 389 390 @Override 391 public void onAnimationCancel(Animator animation) { 392 setDividePosition(to); 393 } 394 }); 395 animator.start(); 396 } 397 getDisplayInsets(Context context)398 private static Rect getDisplayInsets(Context context) { 399 return context.getSystemService(WindowManager.class) 400 .getMaximumWindowMetrics() 401 .getWindowInsets() 402 .getInsets(WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout()) 403 .toRect(); 404 } 405 isLandscape(Rect bounds)406 private static boolean isLandscape(Rect bounds) { 407 return bounds.width() > bounds.height(); 408 } 409 410 /** Reverse the split position. */ 411 @SplitPosition reversePosition(@plitPosition int position)412 public static int reversePosition(@SplitPosition int position) { 413 switch (position) { 414 case SPLIT_POSITION_TOP_OR_LEFT: 415 return SPLIT_POSITION_BOTTOM_OR_RIGHT; 416 case SPLIT_POSITION_BOTTOM_OR_RIGHT: 417 return SPLIT_POSITION_TOP_OR_LEFT; 418 default: 419 return SPLIT_POSITION_UNDEFINED; 420 } 421 } 422 423 /** 424 * Return if this layout is landscape. 425 */ isLandscape()426 public boolean isLandscape() { 427 return isLandscape(mRootBounds); 428 } 429 430 /** Apply recorded surface layout to the {@link SurfaceControl.Transaction}. */ applySurfaceChanges(SurfaceControl.Transaction t, SurfaceControl leash1, SurfaceControl leash2, SurfaceControl dimLayer1, SurfaceControl dimLayer2)431 public void applySurfaceChanges(SurfaceControl.Transaction t, SurfaceControl leash1, 432 SurfaceControl leash2, SurfaceControl dimLayer1, SurfaceControl dimLayer2) { 433 final SurfaceControl dividerLeash = getDividerLeash(); 434 if (dividerLeash != null) { 435 t.setPosition(dividerLeash, mDividerBounds.left, mDividerBounds.top); 436 // Resets layer of divider bar to make sure it is always on top. 437 t.setLayer(dividerLeash, SPLIT_DIVIDER_LAYER); 438 } 439 t.setPosition(leash1, mBounds1.left, mBounds1.top) 440 .setWindowCrop(leash1, mBounds1.width(), mBounds1.height()); 441 t.setPosition(leash2, mBounds2.left, mBounds2.top) 442 .setWindowCrop(leash2, mBounds2.width(), mBounds2.height()); 443 444 if (mImePositionProcessor.adjustSurfaceLayoutForIme( 445 t, dividerLeash, leash1, leash2, dimLayer1, dimLayer2)) { 446 return; 447 } 448 449 mDismissingEffectPolicy.adjustDismissingSurface(t, leash1, leash2, dimLayer1, dimLayer2); 450 } 451 452 /** Apply recorded task layout to the {@link WindowContainerTransaction}. */ applyTaskChanges(WindowContainerTransaction wct, ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2)453 public void applyTaskChanges(WindowContainerTransaction wct, 454 ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2) { 455 if (mImePositionProcessor.applyTaskLayoutForIme(wct, task1.token, task2.token)) { 456 return; 457 } 458 459 if (!mBounds1.equals(mWinBounds1) || !task1.token.equals(mWinToken1)) { 460 wct.setBounds(task1.token, mBounds1); 461 mWinBounds1.set(mBounds1); 462 mWinToken1 = task1.token; 463 } 464 if (!mBounds2.equals(mWinBounds2) || !task2.token.equals(mWinToken2)) { 465 wct.setBounds(task2.token, mBounds2); 466 mWinBounds2.set(mBounds2); 467 mWinToken2 = task2.token; 468 } 469 } 470 471 /** 472 * Shift configuration bounds to prevent client apps get configuration changed or relaunch. And 473 * restore shifted configuration bounds if it's no longer shifted. 474 */ applyLayoutOffsetTarget(WindowContainerTransaction wct, int offsetX, int offsetY, ActivityManager.RunningTaskInfo taskInfo1, ActivityManager.RunningTaskInfo taskInfo2)475 public void applyLayoutOffsetTarget(WindowContainerTransaction wct, int offsetX, int offsetY, 476 ActivityManager.RunningTaskInfo taskInfo1, ActivityManager.RunningTaskInfo taskInfo2) { 477 if (offsetX == 0 && offsetY == 0) { 478 wct.setBounds(taskInfo1.token, mBounds1); 479 wct.setAppBounds(taskInfo1.token, null); 480 wct.setScreenSizeDp(taskInfo1.token, 481 SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED); 482 483 wct.setBounds(taskInfo2.token, mBounds2); 484 wct.setAppBounds(taskInfo2.token, null); 485 wct.setScreenSizeDp(taskInfo2.token, 486 SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED); 487 } else { 488 mTempRect.set(taskInfo1.configuration.windowConfiguration.getBounds()); 489 mTempRect.offset(offsetX, offsetY); 490 wct.setBounds(taskInfo1.token, mTempRect); 491 mTempRect.set(taskInfo1.configuration.windowConfiguration.getAppBounds()); 492 mTempRect.offset(offsetX, offsetY); 493 wct.setAppBounds(taskInfo1.token, mTempRect); 494 wct.setScreenSizeDp(taskInfo1.token, 495 taskInfo1.configuration.screenWidthDp, 496 taskInfo1.configuration.screenHeightDp); 497 498 mTempRect.set(taskInfo2.configuration.windowConfiguration.getBounds()); 499 mTempRect.offset(offsetX, offsetY); 500 wct.setBounds(taskInfo2.token, mTempRect); 501 mTempRect.set(taskInfo2.configuration.windowConfiguration.getAppBounds()); 502 mTempRect.offset(offsetX, offsetY); 503 wct.setAppBounds(taskInfo2.token, mTempRect); 504 wct.setScreenSizeDp(taskInfo2.token, 505 taskInfo2.configuration.screenWidthDp, 506 taskInfo2.configuration.screenHeightDp); 507 } 508 } 509 510 /** Dumps the current split bounds recorded in this layout. */ dump(@onNull PrintWriter pw, String prefix)511 public void dump(@NonNull PrintWriter pw, String prefix) { 512 pw.println(prefix + "bounds1=" + mBounds1.toShortString()); 513 pw.println(prefix + "dividerBounds=" + mDividerBounds.toShortString()); 514 pw.println(prefix + "bounds2=" + mBounds2.toShortString()); 515 } 516 517 /** Handles layout change event. */ 518 public interface SplitLayoutHandler { 519 520 /** Calls when dismissing split. */ onSnappedToDismiss(boolean snappedToEnd)521 void onSnappedToDismiss(boolean snappedToEnd); 522 523 /** 524 * Calls when resizing the split bounds. 525 * 526 * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl, 527 * SurfaceControl, SurfaceControl) 528 */ onLayoutSizeChanging(SplitLayout layout)529 void onLayoutSizeChanging(SplitLayout layout); 530 531 /** 532 * Calls when finish resizing the split bounds. 533 * 534 * @see #applyTaskChanges(WindowContainerTransaction, ActivityManager.RunningTaskInfo, 535 * ActivityManager.RunningTaskInfo) 536 * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl, 537 * SurfaceControl, SurfaceControl) 538 */ onLayoutSizeChanged(SplitLayout layout)539 void onLayoutSizeChanged(SplitLayout layout); 540 541 /** 542 * Calls when re-positioning the split bounds. Like moving split bounds while showing IME 543 * panel. 544 * 545 * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl, 546 * SurfaceControl, SurfaceControl) 547 */ onLayoutPositionChanging(SplitLayout layout)548 void onLayoutPositionChanging(SplitLayout layout); 549 550 /** 551 * Notifies the target offset for shifting layout. So layout handler can shift configuration 552 * bounds correspondingly to make sure client apps won't get configuration changed or 553 * relaunched. If the layout is no longer shifted, layout handler should restore shifted 554 * configuration bounds. 555 * 556 * @see #applyLayoutOffsetTarget(WindowContainerTransaction, int, int, 557 * ActivityManager.RunningTaskInfo, ActivityManager.RunningTaskInfo) 558 */ setLayoutOffsetTarget(int offsetX, int offsetY, SplitLayout layout)559 void setLayoutOffsetTarget(int offsetX, int offsetY, SplitLayout layout); 560 561 /** Calls when user double tapped on the divider bar. */ onDoubleTappedDivider()562 default void onDoubleTappedDivider() { 563 } 564 565 /** Returns split position of the token. */ 566 @SplitPosition getSplitItemPosition(WindowContainerToken token)567 int getSplitItemPosition(WindowContainerToken token); 568 } 569 570 /** 571 * Calculates and applies proper dismissing parallax offset and dimming value to hint users 572 * dismissing gesture. 573 */ 574 private class DismissingEffectPolicy { 575 /** Indicates whether to offset splitting bounds to hint dismissing progress or not. */ 576 private final boolean mApplyParallax; 577 578 // The current dismissing side. 579 int mDismissingSide = DOCKED_INVALID; 580 581 // The parallax offset to hint the dismissing side and progress. 582 final Point mDismissingParallaxOffset = new Point(); 583 584 // The dimming value to hint the dismissing side and progress. 585 float mDismissingDimValue = 0.0f; 586 DismissingEffectPolicy(boolean applyDismissingParallax)587 DismissingEffectPolicy(boolean applyDismissingParallax) { 588 mApplyParallax = applyDismissingParallax; 589 } 590 591 /** 592 * Applies a parallax to the task to hint dismissing progress. 593 * 594 * @param position the split position to apply dismissing parallax effect 595 * @param isLandscape indicates whether it's splitting horizontally or vertically 596 */ applyDividerPosition(int position, boolean isLandscape)597 void applyDividerPosition(int position, boolean isLandscape) { 598 mDismissingSide = DOCKED_INVALID; 599 mDismissingParallaxOffset.set(0, 0); 600 mDismissingDimValue = 0; 601 602 int totalDismissingDistance = 0; 603 if (position < mDividerSnapAlgorithm.getFirstSplitTarget().position) { 604 mDismissingSide = isLandscape ? DOCKED_LEFT : DOCKED_TOP; 605 totalDismissingDistance = mDividerSnapAlgorithm.getDismissStartTarget().position 606 - mDividerSnapAlgorithm.getFirstSplitTarget().position; 607 } else if (position > mDividerSnapAlgorithm.getLastSplitTarget().position) { 608 mDismissingSide = isLandscape ? DOCKED_RIGHT : DOCKED_BOTTOM; 609 totalDismissingDistance = mDividerSnapAlgorithm.getLastSplitTarget().position 610 - mDividerSnapAlgorithm.getDismissEndTarget().position; 611 } 612 613 if (mDismissingSide != DOCKED_INVALID) { 614 float fraction = Math.max(0, 615 Math.min(mDividerSnapAlgorithm.calculateDismissingFraction(position), 1f)); 616 mDismissingDimValue = DIM_INTERPOLATOR.getInterpolation(fraction); 617 fraction = calculateParallaxDismissingFraction(fraction, mDismissingSide); 618 if (isLandscape) { 619 mDismissingParallaxOffset.x = (int) (fraction * totalDismissingDistance); 620 } else { 621 mDismissingParallaxOffset.y = (int) (fraction * totalDismissingDistance); 622 } 623 } 624 } 625 626 /** 627 * @return for a specified {@code fraction}, this returns an adjusted value that simulates a 628 * slowing down parallax effect 629 */ calculateParallaxDismissingFraction(float fraction, int dockSide)630 private float calculateParallaxDismissingFraction(float fraction, int dockSide) { 631 float result = SLOWDOWN_INTERPOLATOR.getInterpolation(fraction) / 3.5f; 632 633 // Less parallax at the top, just because. 634 if (dockSide == WindowManager.DOCKED_TOP) { 635 result /= 2f; 636 } 637 return result; 638 } 639 640 /** Applies parallax offset and dimming value to the root surface at the dismissing side. */ adjustDismissingSurface(SurfaceControl.Transaction t, SurfaceControl leash1, SurfaceControl leash2, SurfaceControl dimLayer1, SurfaceControl dimLayer2)641 boolean adjustDismissingSurface(SurfaceControl.Transaction t, 642 SurfaceControl leash1, SurfaceControl leash2, 643 SurfaceControl dimLayer1, SurfaceControl dimLayer2) { 644 SurfaceControl targetLeash, targetDimLayer; 645 switch (mDismissingSide) { 646 case DOCKED_TOP: 647 case DOCKED_LEFT: 648 targetLeash = leash1; 649 targetDimLayer = dimLayer1; 650 mTempRect.set(mBounds1); 651 break; 652 case DOCKED_BOTTOM: 653 case DOCKED_RIGHT: 654 targetLeash = leash2; 655 targetDimLayer = dimLayer2; 656 mTempRect.set(mBounds2); 657 break; 658 case DOCKED_INVALID: 659 default: 660 t.setAlpha(dimLayer1, 0).hide(dimLayer1); 661 t.setAlpha(dimLayer2, 0).hide(dimLayer2); 662 return false; 663 } 664 665 if (mApplyParallax) { 666 t.setPosition(targetLeash, 667 mTempRect.left + mDismissingParallaxOffset.x, 668 mTempRect.top + mDismissingParallaxOffset.y); 669 // Transform the screen-based split bounds to surface-based crop bounds. 670 mTempRect.offsetTo(-mDismissingParallaxOffset.x, -mDismissingParallaxOffset.y); 671 t.setWindowCrop(targetLeash, mTempRect); 672 } 673 t.setAlpha(targetDimLayer, mDismissingDimValue) 674 .setVisibility(targetDimLayer, mDismissingDimValue > 0.001f); 675 return true; 676 } 677 } 678 679 /** Records IME top offset changes and updates SplitLayout correspondingly. */ 680 private class ImePositionProcessor implements DisplayImeController.ImePositionProcessor { 681 /** 682 * Maximum size of an adjusted split bounds relative to original stack bounds. Used to 683 * restrict IME adjustment so that a min portion of top split remains visible. 684 */ 685 private static final float ADJUSTED_SPLIT_FRACTION_MAX = 0.7f; 686 private static final float ADJUSTED_NONFOCUS_DIM = 0.3f; 687 688 private final int mDisplayId; 689 690 private boolean mImeShown; 691 private int mYOffsetForIme; 692 private float mDimValue1; 693 private float mDimValue2; 694 695 private int mStartImeTop; 696 private int mEndImeTop; 697 698 private int mTargetYOffset; 699 private int mLastYOffset; 700 private float mTargetDim1; 701 private float mTargetDim2; 702 private float mLastDim1; 703 private float mLastDim2; 704 ImePositionProcessor(int displayId)705 private ImePositionProcessor(int displayId) { 706 mDisplayId = displayId; 707 } 708 709 @Override onImeStartPositioning(int displayId, int hiddenTop, int shownTop, boolean showing, boolean isFloating, SurfaceControl.Transaction t)710 public int onImeStartPositioning(int displayId, int hiddenTop, int shownTop, 711 boolean showing, boolean isFloating, SurfaceControl.Transaction t) { 712 if (displayId != mDisplayId) return 0; 713 final int imeTargetPosition = getImeTargetPosition(); 714 if (!mInitialized || imeTargetPosition == SPLIT_POSITION_UNDEFINED) return 0; 715 mStartImeTop = showing ? hiddenTop : shownTop; 716 mEndImeTop = showing ? shownTop : hiddenTop; 717 mImeShown = showing; 718 719 // Update target dim values 720 mLastDim1 = mDimValue1; 721 mTargetDim1 = imeTargetPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT && showing 722 ? ADJUSTED_NONFOCUS_DIM : 0.0f; 723 mLastDim2 = mDimValue2; 724 mTargetDim2 = imeTargetPosition == SPLIT_POSITION_TOP_OR_LEFT && showing 725 ? ADJUSTED_NONFOCUS_DIM : 0.0f; 726 727 // Calculate target bounds offset for IME 728 mLastYOffset = mYOffsetForIme; 729 final boolean needOffset = imeTargetPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT 730 && !isFloating && !isLandscape(mRootBounds) && showing; 731 mTargetYOffset = needOffset ? getTargetYOffset() : 0; 732 733 if (mTargetYOffset != mLastYOffset) { 734 // Freeze the configuration size with offset to prevent app get a configuration 735 // changed or relaunch. This is required to make sure client apps will calculate 736 // insets properly after layout shifted. 737 if (mTargetYOffset == 0) { 738 mSplitLayoutHandler.setLayoutOffsetTarget(0, 0, SplitLayout.this); 739 } else { 740 mSplitLayoutHandler.setLayoutOffsetTarget(0, mTargetYOffset - mLastYOffset, 741 SplitLayout.this); 742 } 743 } 744 745 // Make {@link DividerView} non-interactive while IME showing in split mode. Listen to 746 // ImePositionProcessor#onImeVisibilityChanged directly in DividerView is not enough 747 // because DividerView won't receive onImeVisibilityChanged callback after it being 748 // re-inflated. 749 mSplitWindowManager.setInteractive( 750 !showing || imeTargetPosition == SPLIT_POSITION_UNDEFINED); 751 752 return needOffset ? IME_ANIMATION_NO_ALPHA : 0; 753 } 754 755 @Override onImePositionChanged(int displayId, int imeTop, SurfaceControl.Transaction t)756 public void onImePositionChanged(int displayId, int imeTop, SurfaceControl.Transaction t) { 757 if (displayId != mDisplayId) return; 758 onProgress(getProgress(imeTop)); 759 mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this); 760 } 761 762 @Override onImeEndPositioning(int displayId, boolean cancel, SurfaceControl.Transaction t)763 public void onImeEndPositioning(int displayId, boolean cancel, 764 SurfaceControl.Transaction t) { 765 if (displayId != mDisplayId || cancel) return; 766 onProgress(1.0f); 767 mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this); 768 } 769 770 @Override onImeControlTargetChanged(int displayId, boolean controlling)771 public void onImeControlTargetChanged(int displayId, boolean controlling) { 772 if (displayId != mDisplayId) return; 773 // Restore the split layout when wm-shell is not controlling IME insets anymore. 774 if (!controlling && mImeShown) { 775 reset(); 776 mSplitWindowManager.setInteractive(true); 777 mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this); 778 } 779 } 780 getTargetYOffset()781 private int getTargetYOffset() { 782 final int desireOffset = Math.abs(mEndImeTop - mStartImeTop); 783 // Make sure to keep at least 30% visible for the top split. 784 final int maxOffset = (int) (mBounds1.height() * ADJUSTED_SPLIT_FRACTION_MAX); 785 return -Math.min(desireOffset, maxOffset); 786 } 787 788 @SplitPosition getImeTargetPosition()789 private int getImeTargetPosition() { 790 final WindowContainerToken token = mTaskOrganizer.getImeTarget(mDisplayId); 791 return mSplitLayoutHandler.getSplitItemPosition(token); 792 } 793 getProgress(int currImeTop)794 private float getProgress(int currImeTop) { 795 return ((float) currImeTop - mStartImeTop) / (mEndImeTop - mStartImeTop); 796 } 797 onProgress(float progress)798 private void onProgress(float progress) { 799 mDimValue1 = getProgressValue(mLastDim1, mTargetDim1, progress); 800 mDimValue2 = getProgressValue(mLastDim2, mTargetDim2, progress); 801 mYOffsetForIme = 802 (int) getProgressValue((float) mLastYOffset, (float) mTargetYOffset, progress); 803 } 804 getProgressValue(float start, float end, float progress)805 private float getProgressValue(float start, float end, float progress) { 806 return start + (end - start) * progress; 807 } 808 reset()809 void reset() { 810 mImeShown = false; 811 mYOffsetForIme = mLastYOffset = mTargetYOffset = 0; 812 mDimValue1 = mLastDim1 = mTargetDim1 = 0.0f; 813 mDimValue2 = mLastDim2 = mTargetDim2 = 0.0f; 814 } 815 816 /** 817 * Applies adjusted task layout for showing IME. 818 * 819 * @return {@code false} if there's no need to adjust, otherwise {@code true} 820 */ applyTaskLayoutForIme(WindowContainerTransaction wct, WindowContainerToken token1, WindowContainerToken token2)821 boolean applyTaskLayoutForIme(WindowContainerTransaction wct, 822 WindowContainerToken token1, WindowContainerToken token2) { 823 if (mYOffsetForIme == 0) return false; 824 825 mTempRect.set(mBounds1); 826 mTempRect.offset(0, mYOffsetForIme); 827 wct.setBounds(token1, mTempRect); 828 829 mTempRect.set(mBounds2); 830 mTempRect.offset(0, mYOffsetForIme); 831 wct.setBounds(token2, mTempRect); 832 833 return true; 834 } 835 836 /** 837 * Adjusts surface layout while showing IME. 838 * 839 * @return {@code false} if there's no need to adjust, otherwise {@code true} 840 */ adjustSurfaceLayoutForIme(SurfaceControl.Transaction t, SurfaceControl dividerLeash, SurfaceControl leash1, SurfaceControl leash2, SurfaceControl dimLayer1, SurfaceControl dimLayer2)841 boolean adjustSurfaceLayoutForIme(SurfaceControl.Transaction t, 842 SurfaceControl dividerLeash, SurfaceControl leash1, SurfaceControl leash2, 843 SurfaceControl dimLayer1, SurfaceControl dimLayer2) { 844 final boolean showDim = mDimValue1 > 0.001f || mDimValue2 > 0.001f; 845 boolean adjusted = false; 846 if (mYOffsetForIme != 0) { 847 if (dividerLeash != null) { 848 mTempRect.set(mDividerBounds); 849 mTempRect.offset(0, mYOffsetForIme); 850 t.setPosition(dividerLeash, mTempRect.left, mTempRect.top); 851 } 852 853 mTempRect.set(mBounds1); 854 mTempRect.offset(0, mYOffsetForIme); 855 t.setPosition(leash1, mTempRect.left, mTempRect.top); 856 857 mTempRect.set(mBounds2); 858 mTempRect.offset(0, mYOffsetForIme); 859 t.setPosition(leash2, mTempRect.left, mTempRect.top); 860 adjusted = true; 861 } 862 863 if (showDim) { 864 t.setAlpha(dimLayer1, mDimValue1).setVisibility(dimLayer1, mDimValue1 > 0.001f); 865 t.setAlpha(dimLayer2, mDimValue2).setVisibility(dimLayer2, mDimValue2 > 0.001f); 866 adjusted = true; 867 } 868 return adjusted; 869 } 870 } 871 } 872