1 /* 2 * Copyright (C) 2024 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 androidx.window.extensions.embedding; 18 19 import static android.content.pm.ActivityInfo.CONFIG_DENSITY; 20 import static android.content.pm.ActivityInfo.CONFIG_LAYOUT_DIRECTION; 21 import static android.content.pm.ActivityInfo.CONFIG_WINDOW_CONFIGURATION; 22 import static android.util.TypedValue.COMPLEX_UNIT_DIP; 23 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 24 import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; 25 import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; 26 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; 27 import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE; 28 import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE; 29 import static android.window.TaskFragmentOperation.OP_TYPE_SET_DECOR_SURFACE_BOOSTED; 30 31 import static androidx.window.extensions.embedding.DividerAttributes.RATIO_SYSTEM_DEFAULT; 32 import static androidx.window.extensions.embedding.DividerAttributes.WIDTH_SYSTEM_DEFAULT; 33 import static androidx.window.extensions.embedding.SplitAttributesHelper.isReversedLayout; 34 import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_BOTTOM; 35 import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_LEFT; 36 import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_RIGHT; 37 import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_TOP; 38 39 import android.animation.Animator; 40 import android.animation.AnimatorListenerAdapter; 41 import android.animation.ValueAnimator; 42 import android.annotation.Nullable; 43 import android.app.Activity; 44 import android.app.ActivityThread; 45 import android.content.Context; 46 import android.content.res.Configuration; 47 import android.graphics.Color; 48 import android.graphics.PixelFormat; 49 import android.graphics.Rect; 50 import android.graphics.drawable.ColorDrawable; 51 import android.graphics.drawable.Drawable; 52 import android.graphics.drawable.RotateDrawable; 53 import android.hardware.display.DisplayManager; 54 import android.os.IBinder; 55 import android.util.TypedValue; 56 import android.view.Gravity; 57 import android.view.MotionEvent; 58 import android.view.SurfaceControl; 59 import android.view.SurfaceControlViewHost; 60 import android.view.VelocityTracker; 61 import android.view.View; 62 import android.view.WindowManager; 63 import android.view.WindowlessWindowManager; 64 import android.view.animation.PathInterpolator; 65 import android.widget.FrameLayout; 66 import android.widget.ImageButton; 67 import android.window.InputTransferToken; 68 import android.window.TaskFragmentOperation; 69 import android.window.TaskFragmentParentInfo; 70 import android.window.WindowContainerTransaction; 71 72 import androidx.annotation.GuardedBy; 73 import androidx.annotation.NonNull; 74 import androidx.window.extensions.core.util.function.Consumer; 75 import androidx.window.extensions.embedding.SplitAttributes.SplitType; 76 import androidx.window.extensions.embedding.SplitAttributes.SplitType.ExpandContainersSplitType; 77 import androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType; 78 79 import com.android.internal.R; 80 import com.android.internal.annotations.VisibleForTesting; 81 import com.android.window.flags.Flags; 82 83 import java.util.Objects; 84 import java.util.concurrent.Executor; 85 86 /** 87 * Manages the rendering and interaction of the divider. 88 */ 89 class DividerPresenter implements View.OnTouchListener { 90 static final float RATIO_EXPANDED_PRIMARY = 1.0f; 91 static final float RATIO_EXPANDED_SECONDARY = 0.0f; 92 private static final String WINDOW_NAME = "AE Divider"; 93 private static final int VEIL_LAYER = 0; 94 private static final int DIVIDER_LAYER = 1; 95 96 // TODO(b/327067596) Update based on UX guidance. 97 private static final Color DEFAULT_PRIMARY_VEIL_COLOR = Color.valueOf(Color.BLACK); 98 private static final Color DEFAULT_SECONDARY_VEIL_COLOR = Color.valueOf(Color.GRAY); 99 @VisibleForTesting 100 static final float DEFAULT_MIN_RATIO = 0.35f; 101 @VisibleForTesting 102 static final float DEFAULT_MAX_RATIO = 0.65f; 103 @VisibleForTesting 104 static final int DEFAULT_DIVIDER_WIDTH_DP = 24; 105 106 @VisibleForTesting 107 static final PathInterpolator FLING_ANIMATION_INTERPOLATOR = 108 new PathInterpolator(0.4f, 0f, 0.2f, 1f); 109 @VisibleForTesting 110 static final int FLING_ANIMATION_DURATION = 250; 111 @VisibleForTesting 112 static final int MIN_DISMISS_VELOCITY_DP_PER_SECOND = 600; 113 @VisibleForTesting 114 static final int MIN_FLING_VELOCITY_DP_PER_SECOND = 400; 115 116 private final int mTaskId; 117 118 @NonNull 119 private final Object mLock = new Object(); 120 121 @NonNull 122 private final DragEventCallback mDragEventCallback; 123 124 @NonNull 125 private final Executor mCallbackExecutor; 126 127 /** 128 * The VelocityTracker of the divider, used to track the dragging velocity. This field is 129 * {@code null} until dragging starts. 130 */ 131 @GuardedBy("mLock") 132 @Nullable 133 VelocityTracker mVelocityTracker; 134 135 /** 136 * The {@link Properties} of the divider. This field is {@code null} when no divider should be 137 * drawn, e.g. when the split doesn't have {@link DividerAttributes} or when the decor surface 138 * is not available. 139 */ 140 @GuardedBy("mLock") 141 @Nullable 142 @VisibleForTesting 143 Properties mProperties; 144 145 /** 146 * The {@link Renderer} of the divider. This field is {@code null} when no divider should be 147 * drawn, i.e. when {@link #mProperties} is {@code null}. The {@link Renderer} is recreated or 148 * updated when {@link #mProperties} is changed. 149 */ 150 @GuardedBy("mLock") 151 @Nullable 152 @VisibleForTesting 153 Renderer mRenderer; 154 155 /** 156 * The owner TaskFragment token of the decor surface. The decor surface is placed right above 157 * the owner TaskFragment surface and is removed if the owner TaskFragment is destroyed. 158 */ 159 @GuardedBy("mLock") 160 @Nullable 161 @VisibleForTesting 162 IBinder mDecorSurfaceOwner; 163 164 /** 165 * The current divider position relative to the Task bounds. For vertical split (left-to-right 166 * or right-to-left), it is the x coordinate in the task window, and for horizontal split 167 * (top-to-bottom or bottom-to-top), it is the y coordinate in the task window. 168 */ 169 @GuardedBy("mLock") 170 private int mDividerPosition; 171 DividerPresenter(int taskId, @NonNull DragEventCallback dragEventCallback, @NonNull Executor callbackExecutor)172 DividerPresenter(int taskId, @NonNull DragEventCallback dragEventCallback, 173 @NonNull Executor callbackExecutor) { 174 mTaskId = taskId; 175 mDragEventCallback = dragEventCallback; 176 mCallbackExecutor = callbackExecutor; 177 } 178 179 /** Updates the divider when external conditions are changed. */ updateDivider( @onNull WindowContainerTransaction wct, @NonNull TaskFragmentParentInfo parentInfo, @Nullable SplitContainer topSplitContainer)180 void updateDivider( 181 @NonNull WindowContainerTransaction wct, 182 @NonNull TaskFragmentParentInfo parentInfo, 183 @Nullable SplitContainer topSplitContainer) { 184 if (!Flags.activityEmbeddingInteractiveDividerFlag()) { 185 return; 186 } 187 188 synchronized (mLock) { 189 // Clean up the decor surface if top SplitContainer is null. 190 if (topSplitContainer == null) { 191 removeDecorSurfaceAndDivider(wct); 192 return; 193 } 194 195 final SplitAttributes splitAttributes = topSplitContainer.getCurrentSplitAttributes(); 196 final DividerAttributes dividerAttributes = splitAttributes.getDividerAttributes(); 197 198 // Clean up the decor surface if DividerAttributes is null. 199 if (dividerAttributes == null) { 200 removeDecorSurfaceAndDivider(wct); 201 return; 202 } 203 204 // At this point, a divider is required. 205 final TaskFragmentContainer primaryContainer = 206 topSplitContainer.getPrimaryContainer(); 207 final TaskFragmentContainer secondaryContainer = 208 topSplitContainer.getSecondaryContainer(); 209 210 // Create the decor surface if one is not available yet. 211 final SurfaceControl decorSurface = parentInfo.getDecorSurface(); 212 if (decorSurface == null) { 213 // Clean up when the decor surface is currently unavailable. 214 removeDivider(); 215 // Request to create the decor surface 216 createOrMoveDecorSurfaceLocked(wct, primaryContainer); 217 return; 218 } 219 220 // Update the decor surface owner if needed. 221 boolean isDraggableExpandType = 222 SplitAttributesHelper.isDraggableExpandType(splitAttributes); 223 final TaskFragmentContainer decorSurfaceOwnerContainer = 224 isDraggableExpandType ? secondaryContainer : primaryContainer; 225 226 if (!Objects.equals( 227 mDecorSurfaceOwner, decorSurfaceOwnerContainer.getTaskFragmentToken())) { 228 createOrMoveDecorSurfaceLocked(wct, decorSurfaceOwnerContainer); 229 } 230 231 final Configuration parentConfiguration = parentInfo.getConfiguration(); 232 final Rect taskBounds = parentConfiguration.windowConfiguration.getBounds(); 233 final boolean isVerticalSplit = isVerticalSplit(splitAttributes); 234 final boolean isReversedLayout = isReversedLayout(splitAttributes, parentConfiguration); 235 final int dividerWidthPx = getDividerWidthPx(dividerAttributes); 236 237 updateProperties( 238 new Properties( 239 parentConfiguration, 240 dividerAttributes, 241 decorSurface, 242 getInitialDividerPosition( 243 primaryContainer, secondaryContainer, taskBounds, 244 dividerWidthPx, isDraggableExpandType, isVerticalSplit, 245 isReversedLayout), 246 isVerticalSplit, 247 isReversedLayout, 248 parentInfo.getDisplayId(), 249 isDraggableExpandType, 250 primaryContainer, 251 secondaryContainer) 252 ); 253 } 254 } 255 256 @GuardedBy("mLock") updateProperties(@onNull Properties properties)257 private void updateProperties(@NonNull Properties properties) { 258 if (Properties.equalsForDivider(mProperties, properties)) { 259 return; 260 } 261 final Properties previousProperties = mProperties; 262 mProperties = properties; 263 264 if (mRenderer == null) { 265 // Create a new renderer when a renderer doesn't exist yet. 266 mRenderer = new Renderer(mProperties, this); 267 } else if (!Properties.areSameSurfaces( 268 previousProperties.mDecorSurface, mProperties.mDecorSurface) 269 || previousProperties.mDisplayId != mProperties.mDisplayId) { 270 // Release and recreate the renderer if the decor surface or the display has changed. 271 mRenderer.release(); 272 mRenderer = new Renderer(mProperties, this); 273 } else { 274 // Otherwise, update the renderer for the new properties. 275 mRenderer.update(mProperties); 276 } 277 } 278 279 /** 280 * Returns the window background color of the top activity in the container if set, or the 281 * default color if the background color of the top activity is unavailable. 282 */ 283 @VisibleForTesting 284 @NonNull getContainerBackgroundColor( @onNull TaskFragmentContainer container, @NonNull Color defaultColor)285 static Color getContainerBackgroundColor( 286 @NonNull TaskFragmentContainer container, @NonNull Color defaultColor) { 287 final Activity activity = container.getTopNonFinishingActivity(); 288 if (activity == null) { 289 // This can happen when the activities in the container are from a different process. 290 // TODO(b/340984203) Report whether the top activity is in the same process. Use default 291 // color if not. 292 return defaultColor; 293 } 294 295 final Drawable drawable = activity.getWindow().getDecorView().getBackground(); 296 if (drawable instanceof ColorDrawable colorDrawable) { 297 return Color.valueOf(colorDrawable.getColor()); 298 } 299 return defaultColor; 300 } 301 302 /** 303 * Creates a decor surface for the TaskFragment if no decor surface exists, or changes the owner 304 * of the existing decor surface to be the specified TaskFragment. 305 * 306 * See {@link TaskFragmentOperation#OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE}. 307 */ createOrMoveDecorSurface( @onNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container)308 void createOrMoveDecorSurface( 309 @NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container) { 310 synchronized (mLock) { 311 createOrMoveDecorSurfaceLocked(wct, container); 312 } 313 } 314 315 @GuardedBy("mLock") createOrMoveDecorSurfaceLocked( @onNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container)316 private void createOrMoveDecorSurfaceLocked( 317 @NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container) { 318 mDecorSurfaceOwner = container.getTaskFragmentToken(); 319 final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( 320 OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE) 321 .build(); 322 wct.addTaskFragmentOperation(mDecorSurfaceOwner, operation); 323 } 324 325 @GuardedBy("mLock") removeDecorSurfaceAndDivider(@onNull WindowContainerTransaction wct)326 private void removeDecorSurfaceAndDivider(@NonNull WindowContainerTransaction wct) { 327 if (mDecorSurfaceOwner != null) { 328 final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( 329 OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE) 330 .build(); 331 wct.addTaskFragmentOperation(mDecorSurfaceOwner, operation); 332 mDecorSurfaceOwner = null; 333 } 334 removeDivider(); 335 } 336 337 @GuardedBy("mLock") removeDivider()338 private void removeDivider() { 339 if (mRenderer != null) { 340 mRenderer.release(); 341 } 342 mProperties = null; 343 mRenderer = null; 344 } 345 346 @VisibleForTesting getInitialDividerPosition( @onNull TaskFragmentContainer primaryContainer, @NonNull TaskFragmentContainer secondaryContainer, @NonNull Rect taskBounds, int dividerWidthPx, boolean isDraggableExpandType, boolean isVerticalSplit, boolean isReversedLayout)347 static int getInitialDividerPosition( 348 @NonNull TaskFragmentContainer primaryContainer, 349 @NonNull TaskFragmentContainer secondaryContainer, 350 @NonNull Rect taskBounds, 351 int dividerWidthPx, 352 boolean isDraggableExpandType, 353 boolean isVerticalSplit, 354 boolean isReversedLayout) { 355 if (isDraggableExpandType) { 356 // If the secondary container is fully expanded by dragging the divider, we display the 357 // divider on the edge. 358 final int fullyExpandedPosition = isVerticalSplit 359 ? taskBounds.width() - dividerWidthPx 360 : taskBounds.height() - dividerWidthPx; 361 return isReversedLayout ? fullyExpandedPosition : 0; 362 } else { 363 final Rect primaryBounds = primaryContainer.getLastRequestedBounds(); 364 final Rect secondaryBounds = secondaryContainer.getLastRequestedBounds(); 365 return isVerticalSplit 366 ? Math.min(primaryBounds.right, secondaryBounds.right) 367 : Math.min(primaryBounds.bottom, secondaryBounds.bottom); 368 } 369 } 370 isVerticalSplit(@onNull SplitAttributes splitAttributes)371 private static boolean isVerticalSplit(@NonNull SplitAttributes splitAttributes) { 372 final int layoutDirection = splitAttributes.getLayoutDirection(); 373 switch (layoutDirection) { 374 case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: 375 case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: 376 case SplitAttributes.LayoutDirection.LOCALE: 377 return true; 378 case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: 379 case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: 380 return false; 381 default: 382 throw new IllegalArgumentException("Invalid layout direction:" + layoutDirection); 383 } 384 } 385 getDividerWidthPx(@onNull DividerAttributes dividerAttributes)386 private static int getDividerWidthPx(@NonNull DividerAttributes dividerAttributes) { 387 int dividerWidthDp = dividerAttributes.getWidthDp(); 388 return convertDpToPixel(dividerWidthDp); 389 } 390 convertDpToPixel(int dp)391 private static int convertDpToPixel(int dp) { 392 // TODO(b/329193115) support divider on secondary display 393 final Context applicationContext = ActivityThread.currentActivityThread().getApplication(); 394 395 return (int) TypedValue.applyDimension( 396 COMPLEX_UNIT_DIP, 397 dp, 398 applicationContext.getResources().getDisplayMetrics()); 399 } 400 getDisplayDensity()401 private static float getDisplayDensity() { 402 // TODO(b/329193115) support divider on secondary display 403 final Context applicationContext = 404 ActivityThread.currentActivityThread().getApplication(); 405 return applicationContext.getResources().getDisplayMetrics().density; 406 } 407 408 /** 409 * Returns the container bound offset that is a result of the presence of a divider. 410 * 411 * The offset is the relative position change for the container edge that is next to the divider 412 * due to the presence of the divider. The value could be negative or positive depending on the 413 * container position. Positive values indicate that the edge is shifting towards the right 414 * (or bottom) and negative values indicate that the edge is shifting towards the left (or top). 415 * 416 * @param splitAttributes the {@link SplitAttributes} of the split container that we want to 417 * compute bounds offset. 418 * @param position the position of the container in the split that we want to compute 419 * bounds offset for. 420 * @return the bounds offset in pixels. 421 */ getBoundsOffsetForDivider( @onNull SplitAttributes splitAttributes, @SplitPresenter.ContainerPosition int position)422 static int getBoundsOffsetForDivider( 423 @NonNull SplitAttributes splitAttributes, 424 @SplitPresenter.ContainerPosition int position) { 425 if (!Flags.activityEmbeddingInteractiveDividerFlag()) { 426 return 0; 427 } 428 final DividerAttributes dividerAttributes = splitAttributes.getDividerAttributes(); 429 if (dividerAttributes == null) { 430 return 0; 431 } 432 final int dividerWidthPx = getDividerWidthPx(dividerAttributes); 433 return getBoundsOffsetForDivider( 434 dividerWidthPx, 435 splitAttributes.getSplitType(), 436 position); 437 } 438 439 @VisibleForTesting getBoundsOffsetForDivider( int dividerWidthPx, @NonNull SplitType splitType, @SplitPresenter.ContainerPosition int position)440 static int getBoundsOffsetForDivider( 441 int dividerWidthPx, 442 @NonNull SplitType splitType, 443 @SplitPresenter.ContainerPosition int position) { 444 if (splitType instanceof ExpandContainersSplitType) { 445 // No divider offset is needed for the ExpandContainersSplitType. 446 return 0; 447 } 448 int primaryOffset; 449 if (splitType instanceof final RatioSplitType splitRatio) { 450 // When a divider is present, both containers shrink by an amount proportional to their 451 // split ratio and sum to the width of the divider, so that the ending sizing of the 452 // containers still maintain the same ratio. 453 primaryOffset = (int) (dividerWidthPx * splitRatio.getRatio()); 454 } else { 455 // Hinge split type (and other future split types) will have the divider width equally 456 // distributed to both containers. 457 primaryOffset = dividerWidthPx / 2; 458 } 459 final int secondaryOffset = dividerWidthPx - primaryOffset; 460 switch (position) { 461 case CONTAINER_POSITION_LEFT: 462 case CONTAINER_POSITION_TOP: 463 return -primaryOffset; 464 case CONTAINER_POSITION_RIGHT: 465 case CONTAINER_POSITION_BOTTOM: 466 return secondaryOffset; 467 default: 468 throw new IllegalArgumentException("Unknown position:" + position); 469 } 470 } 471 472 /** 473 * Sanitizes and sets default values in the {@link DividerAttributes}. 474 * 475 * Unset values will be set with system default values. See 476 * {@link DividerAttributes#WIDTH_SYSTEM_DEFAULT} and 477 * {@link DividerAttributes#RATIO_SYSTEM_DEFAULT}. 478 * 479 * @param dividerAttributes input {@link DividerAttributes} 480 * @return a {@link DividerAttributes} that has all values properly set. 481 */ 482 @Nullable sanitizeDividerAttributes( @ullable DividerAttributes dividerAttributes)483 static DividerAttributes sanitizeDividerAttributes( 484 @Nullable DividerAttributes dividerAttributes) { 485 if (dividerAttributes == null) { 486 return null; 487 } 488 int widthDp = dividerAttributes.getWidthDp(); 489 float minRatio = dividerAttributes.getPrimaryMinRatio(); 490 float maxRatio = dividerAttributes.getPrimaryMaxRatio(); 491 492 if (widthDp == WIDTH_SYSTEM_DEFAULT) { 493 widthDp = DEFAULT_DIVIDER_WIDTH_DP; 494 } 495 496 if (dividerAttributes.getDividerType() == DividerAttributes.DIVIDER_TYPE_DRAGGABLE) { 497 // Update minRatio and maxRatio only when it is a draggable divider. 498 if (minRatio == RATIO_SYSTEM_DEFAULT) { 499 minRatio = DEFAULT_MIN_RATIO; 500 } 501 if (maxRatio == RATIO_SYSTEM_DEFAULT) { 502 maxRatio = DEFAULT_MAX_RATIO; 503 } 504 } 505 506 return new DividerAttributes.Builder(dividerAttributes) 507 .setWidthDp(widthDp) 508 .setPrimaryMinRatio(minRatio) 509 .setPrimaryMaxRatio(maxRatio) 510 .build(); 511 } 512 513 @Override onTouch(@onNull View view, @NonNull MotionEvent event)514 public boolean onTouch(@NonNull View view, @NonNull MotionEvent event) { 515 synchronized (mLock) { 516 if (mProperties != null && mRenderer != null) { 517 final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); 518 mDividerPosition = calculateDividerPosition( 519 event, taskBounds, mProperties.mDividerWidthPx, 520 mProperties.mDividerAttributes, mProperties.mIsVerticalSplit, 521 calculateMinPosition(), calculateMaxPosition()); 522 mRenderer.setDividerPosition(mDividerPosition); 523 524 // Convert to use screen-based coordinates to prevent lost track of motion events 525 // while moving divider bar and calculating dragging velocity. 526 event.setLocation(event.getRawX(), event.getRawY()); 527 final int action = event.getAction() & MotionEvent.ACTION_MASK; 528 switch (action) { 529 case MotionEvent.ACTION_DOWN: 530 onStartDragging(event); 531 break; 532 case MotionEvent.ACTION_UP: 533 case MotionEvent.ACTION_CANCEL: 534 onFinishDragging(event); 535 break; 536 case MotionEvent.ACTION_MOVE: 537 onDrag(event); 538 break; 539 default: 540 break; 541 } 542 } 543 } 544 545 // Returns true to prevent the default button click callback. The button pressed state is 546 // set/unset when starting/finishing dragging. 547 return true; 548 } 549 550 @GuardedBy("mLock") onStartDragging(@onNull MotionEvent event)551 private void onStartDragging(@NonNull MotionEvent event) { 552 mVelocityTracker = VelocityTracker.obtain(); 553 mVelocityTracker.addMovement(event); 554 555 mRenderer.mIsDragging = true; 556 mRenderer.mDragHandle.setPressed(mRenderer.mIsDragging); 557 mRenderer.updateSurface(); 558 559 // Veil visibility change should be applied together with the surface boost transaction in 560 // the wct. 561 final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); 562 mRenderer.showVeils(t); 563 564 // Callbacks must be executed on the executor to release mLock and prevent deadlocks. 565 mCallbackExecutor.execute(() -> { 566 mDragEventCallback.onStartDragging( 567 wct -> { 568 synchronized (mLock) { 569 setDecorSurfaceBoosted(wct, mDecorSurfaceOwner, true /* boosted */, t); 570 } 571 }); 572 }); 573 } 574 575 @GuardedBy("mLock") onDrag(@onNull MotionEvent event)576 private void onDrag(@NonNull MotionEvent event) { 577 if (mVelocityTracker != null) { 578 mVelocityTracker.addMovement(event); 579 } 580 mRenderer.updateSurface(); 581 } 582 583 @GuardedBy("mLock") onFinishDragging(@onNull MotionEvent event)584 private void onFinishDragging(@NonNull MotionEvent event) { 585 float velocity = 0.0f; 586 if (mVelocityTracker != null) { 587 mVelocityTracker.addMovement(event); 588 mVelocityTracker.computeCurrentVelocity(1000 /* units */); 589 velocity = mProperties.mIsVerticalSplit 590 ? mVelocityTracker.getXVelocity() 591 : mVelocityTracker.getYVelocity(); 592 mVelocityTracker.recycle(); 593 } 594 595 final int prevDividerPosition = mDividerPosition; 596 mDividerPosition = dividerPositionForSnapPoints(mDividerPosition, velocity); 597 if (mDividerPosition != prevDividerPosition) { 598 ValueAnimator animator = getFlingAnimator(prevDividerPosition, mDividerPosition); 599 animator.start(); 600 } else { 601 onDraggingEnd(); 602 } 603 } 604 605 @GuardedBy("mLock") 606 @NonNull 607 @VisibleForTesting getFlingAnimator(int prevDividerPosition, int snappedDividerPosition)608 ValueAnimator getFlingAnimator(int prevDividerPosition, int snappedDividerPosition) { 609 final ValueAnimator animator = 610 getValueAnimator(prevDividerPosition, snappedDividerPosition); 611 animator.addUpdateListener(animation -> { 612 synchronized (mLock) { 613 updateDividerPosition((int) animation.getAnimatedValue()); 614 } 615 }); 616 animator.addListener(new AnimatorListenerAdapter() { 617 @Override 618 public void onAnimationEnd(Animator animation) { 619 synchronized (mLock) { 620 onDraggingEnd(); 621 } 622 } 623 624 @Override 625 public void onAnimationCancel(Animator animation) { 626 synchronized (mLock) { 627 onDraggingEnd(); 628 } 629 } 630 }); 631 return animator; 632 } 633 634 @VisibleForTesting getValueAnimator(int prevDividerPosition, int snappedDividerPosition)635 static ValueAnimator getValueAnimator(int prevDividerPosition, int snappedDividerPosition) { 636 ValueAnimator animator = ValueAnimator 637 .ofInt(prevDividerPosition, snappedDividerPosition) 638 .setDuration(FLING_ANIMATION_DURATION); 639 animator.setInterpolator(FLING_ANIMATION_INTERPOLATOR); 640 return animator; 641 } 642 643 @GuardedBy("mLock") updateDividerPosition(int position)644 private void updateDividerPosition(int position) { 645 mRenderer.setDividerPosition(position); 646 mRenderer.updateSurface(); 647 } 648 649 @GuardedBy("mLock") onDraggingEnd()650 private void onDraggingEnd() { 651 // Veil visibility change should be applied together with the surface boost transaction in 652 // the wct. 653 final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); 654 mRenderer.hideVeils(t); 655 656 // Callbacks must be executed on the executor to release mLock and prevent deadlocks. 657 // mDecorSurfaceOwner may change between here and when the callback is executed, 658 // e.g. when the decor surface owner becomes the secondary container when it is expanded to 659 // fullscreen. 660 mCallbackExecutor.execute(() -> { 661 mDragEventCallback.onFinishDragging( 662 mTaskId, 663 wct -> { 664 synchronized (mLock) { 665 setDecorSurfaceBoosted(wct, mDecorSurfaceOwner, false /* boosted */, t); 666 } 667 }); 668 }); 669 mRenderer.mIsDragging = false; 670 mRenderer.mDragHandle.setPressed(mRenderer.mIsDragging); 671 } 672 673 /** 674 * Returns the divider position adjusted for the min max ratio and fullscreen expansion. 675 * The adjusted divider position is in the range of [minPosition, maxPosition] for a split, 0 676 * for expanded right (bottom) container, or task width (height) minus the divider width for 677 * expanded left (top) container. 678 */ 679 @GuardedBy("mLock") dividerPositionForSnapPoints(int dividerPosition, float velocity)680 private int dividerPositionForSnapPoints(int dividerPosition, float velocity) { 681 final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); 682 final int minPosition = calculateMinPosition(); 683 final int maxPosition = calculateMaxPosition(); 684 final int fullyExpandedPosition = mProperties.mIsVerticalSplit 685 ? taskBounds.width() - mProperties.mDividerWidthPx 686 : taskBounds.height() - mProperties.mDividerWidthPx; 687 688 final float displayDensity = getDisplayDensity(); 689 final boolean isDraggingToFullscreenAllowed = 690 isDraggingToFullscreenAllowed(mProperties.mDividerAttributes); 691 return dividerPositionWithPositionOptions( 692 dividerPosition, 693 minPosition, 694 maxPosition, 695 fullyExpandedPosition, 696 velocity, 697 displayDensity, 698 isDraggingToFullscreenAllowed); 699 } 700 701 /** 702 * Returns the divider position given a set of position options. A snap algorithm can adjust 703 * the ending position to either fully expand one container or move the divider back to 704 * the specified min/max ratio depending on the dragging velocity and if dragging to fullscreen 705 * is allowed. 706 */ 707 @VisibleForTesting dividerPositionWithPositionOptions(int dividerPosition, int minPosition, int maxPosition, int fullyExpandedPosition, float velocity, float displayDensity, boolean isDraggingToFullscreenAllowed)708 static int dividerPositionWithPositionOptions(int dividerPosition, int minPosition, 709 int maxPosition, int fullyExpandedPosition, float velocity, float displayDensity, 710 boolean isDraggingToFullscreenAllowed) { 711 if (isDraggingToFullscreenAllowed) { 712 final float minDismissVelocityPxPerSecond = 713 MIN_DISMISS_VELOCITY_DP_PER_SECOND * displayDensity; 714 if (dividerPosition < minPosition && velocity < -minDismissVelocityPxPerSecond) { 715 return 0; 716 } 717 if (dividerPosition > maxPosition && velocity > minDismissVelocityPxPerSecond) { 718 return fullyExpandedPosition; 719 } 720 } 721 final float minFlingVelocityPxPerSecond = 722 MIN_FLING_VELOCITY_DP_PER_SECOND * displayDensity; 723 if (Math.abs(velocity) >= minFlingVelocityPxPerSecond) { 724 return dividerPositionForFling( 725 dividerPosition, minPosition, maxPosition, velocity); 726 } 727 if (dividerPosition >= minPosition && dividerPosition <= maxPosition) { 728 return dividerPosition; 729 } 730 return snap( 731 dividerPosition, 732 isDraggingToFullscreenAllowed 733 ? new int[] {0, minPosition, maxPosition, fullyExpandedPosition} 734 : new int[] {minPosition, maxPosition}); 735 } 736 737 /** 738 * Returns the closest position that is in the fling direction. 739 */ dividerPositionForFling(int dividerPosition, int minPosition, int maxPosition, float velocity)740 private static int dividerPositionForFling(int dividerPosition, int minPosition, 741 int maxPosition, float velocity) { 742 final boolean isBackwardDirection = velocity < 0; 743 if (isBackwardDirection) { 744 return dividerPosition < maxPosition ? minPosition : maxPosition; 745 } else { 746 return dividerPosition > minPosition ? maxPosition : minPosition; 747 } 748 } 749 750 /** 751 * Returns the snapped position from a list of possible positions. Currently, this method 752 * snaps to the closest position by distance from the divider position. 753 */ 754 private static int snap(int dividerPosition, int[] possiblePositions) { 755 int snappedPosition = dividerPosition; 756 float minDistance = Float.MAX_VALUE; 757 for (int position : possiblePositions) { 758 float distance = Math.abs(dividerPosition - position); 759 if (distance < minDistance) { 760 snappedPosition = position; 761 minDistance = distance; 762 } 763 } 764 return snappedPosition; 765 } 766 767 private static void setDecorSurfaceBoosted( 768 @NonNull WindowContainerTransaction wct, 769 @Nullable IBinder decorSurfaceOwner, 770 boolean boosted, 771 @NonNull SurfaceControl.Transaction clientTransaction) { 772 if (decorSurfaceOwner == null) { 773 return; 774 } 775 wct.addTaskFragmentOperation( 776 decorSurfaceOwner, 777 new TaskFragmentOperation.Builder(OP_TYPE_SET_DECOR_SURFACE_BOOSTED) 778 .setBooleanValue(boosted) 779 .setSurfaceTransaction(clientTransaction) 780 .build() 781 ); 782 } 783 784 /** Calculates the new divider position based on the touch event and divider attributes. */ 785 @VisibleForTesting 786 static int calculateDividerPosition(@NonNull MotionEvent event, @NonNull Rect taskBounds, 787 int dividerWidthPx, @NonNull DividerAttributes dividerAttributes, 788 boolean isVerticalSplit, int minPosition, int maxPosition) { 789 // The touch event is in display space. Converting it into the task window space. 790 final int touchPositionInTaskSpace = isVerticalSplit 791 ? (int) (event.getRawX()) - taskBounds.left 792 : (int) (event.getRawY()) - taskBounds.top; 793 794 // Assuming that the touch position is at the center of the divider bar, so the divider 795 // position is offset by half of the divider width. 796 int dividerPosition = touchPositionInTaskSpace - dividerWidthPx / 2; 797 798 // If dragging to fullscreen is not allowed, limit the divider position to the min and max 799 // ratios set in DividerAttributes. Otherwise, dragging beyond the min and max ratios is 800 // temporarily allowed and the final ratio will be adjusted in onFinishDragging. 801 if (!isDraggingToFullscreenAllowed(dividerAttributes)) { 802 dividerPosition = Math.clamp(dividerPosition, minPosition, maxPosition); 803 } 804 return dividerPosition; 805 } 806 807 @GuardedBy("mLock") 808 private int calculateMinPosition() { 809 return calculateMinPosition( 810 mProperties.mConfiguration.windowConfiguration.getBounds(), 811 mProperties.mDividerWidthPx, mProperties.mDividerAttributes, 812 mProperties.mIsVerticalSplit, mProperties.mIsReversedLayout); 813 } 814 815 @GuardedBy("mLock") 816 private int calculateMaxPosition() { 817 return calculateMaxPosition( 818 mProperties.mConfiguration.windowConfiguration.getBounds(), 819 mProperties.mDividerWidthPx, mProperties.mDividerAttributes, 820 mProperties.mIsVerticalSplit, mProperties.mIsReversedLayout); 821 } 822 823 /** Calculates the min position of the divider that the user is allowed to drag to. */ 824 @VisibleForTesting 825 static int calculateMinPosition(@NonNull Rect taskBounds, int dividerWidthPx, 826 @NonNull DividerAttributes dividerAttributes, boolean isVerticalSplit, 827 boolean isReversedLayout) { 828 // The usable size is the task window size minus the divider bar width. This is shared 829 // between the primary and secondary containers based on the split ratio. 830 final int usableSize = isVerticalSplit 831 ? taskBounds.width() - dividerWidthPx 832 : taskBounds.height() - dividerWidthPx; 833 return (int) (isReversedLayout 834 ? usableSize - usableSize * dividerAttributes.getPrimaryMaxRatio() 835 : usableSize * dividerAttributes.getPrimaryMinRatio()); 836 } 837 838 /** Calculates the max position of the divider that the user is allowed to drag to. */ 839 @VisibleForTesting 840 static int calculateMaxPosition(@NonNull Rect taskBounds, int dividerWidthPx, 841 @NonNull DividerAttributes dividerAttributes, boolean isVerticalSplit, 842 boolean isReversedLayout) { 843 // The usable size is the task window size minus the divider bar width. This is shared 844 // between the primary and secondary containers based on the split ratio. 845 final int usableSize = isVerticalSplit 846 ? taskBounds.width() - dividerWidthPx 847 : taskBounds.height() - dividerWidthPx; 848 return (int) (isReversedLayout 849 ? usableSize - usableSize * dividerAttributes.getPrimaryMinRatio() 850 : usableSize * dividerAttributes.getPrimaryMaxRatio()); 851 } 852 853 /** 854 * Returns the new split ratio of the {@link SplitContainer} based on the current divider 855 * position. 856 */ 857 float calculateNewSplitRatio() { 858 synchronized (mLock) { 859 return calculateNewSplitRatio( 860 mDividerPosition, 861 mProperties.mConfiguration.windowConfiguration.getBounds(), 862 mProperties.mDividerWidthPx, 863 mProperties.mIsVerticalSplit, 864 mProperties.mIsReversedLayout, 865 calculateMinPosition(), 866 calculateMaxPosition(), 867 isDraggingToFullscreenAllowed(mProperties.mDividerAttributes)); 868 } 869 } 870 871 private static boolean isDraggingToFullscreenAllowed( 872 @NonNull DividerAttributes dividerAttributes) { 873 // TODO(b/293654166) Use DividerAttributes.isDraggingToFullscreenAllowed when extension is 874 // updated to v7. 875 return false; 876 } 877 878 /** 879 * Returns the new split ratio of the {@link SplitContainer} based on the current divider 880 * position. 881 * 882 * @param dividerPosition the divider position. See {@link #mDividerPosition}. 883 * @param taskBounds the task bounds 884 * @param dividerWidthPx the width of the divider in pixels. 885 * @param isVerticalSplit if {@code true}, the split is a vertical split. If {@code false}, the 886 * split is a horizontal split. See 887 * {@link #isVerticalSplit(SplitAttributes)}. 888 * @param isReversedLayout if {@code true}, the split layout is reversed, i.e. right-to-left or 889 * bottom-to-top. If {@code false}, the split is not reversed, i.e. 890 * left-to-right or top-to-bottom. See 891 * {@link SplitAttributesHelper#isReversedLayout} 892 * @return the computed split ratio of the primary container. If the primary container is fully 893 * expanded, {@link #RATIO_EXPANDED_PRIMARY} is returned. If the secondary container is fully 894 * expanded, {@link #RATIO_EXPANDED_SECONDARY} is returned. 895 */ 896 @VisibleForTesting 897 static float calculateNewSplitRatio( 898 int dividerPosition, 899 @NonNull Rect taskBounds, 900 int dividerWidthPx, 901 boolean isVerticalSplit, 902 boolean isReversedLayout, 903 int minPosition, 904 int maxPosition, 905 boolean isDraggingToFullscreenAllowed) { 906 907 // Handle the fully expanded cases. 908 if (isDraggingToFullscreenAllowed) { 909 // The divider position is already adjusted by the snap algorithm in onFinishDragging. 910 // If the divider position is not in the range [minPosition, maxPosition], then one of 911 // the containers is fully expanded. 912 if (dividerPosition < minPosition) { 913 return isReversedLayout ? RATIO_EXPANDED_PRIMARY : RATIO_EXPANDED_SECONDARY; 914 } 915 if (dividerPosition > maxPosition) { 916 return isReversedLayout ? RATIO_EXPANDED_SECONDARY : RATIO_EXPANDED_PRIMARY; 917 } 918 } else { 919 dividerPosition = Math.clamp(dividerPosition, minPosition, maxPosition); 920 } 921 922 final int usableSize = isVerticalSplit 923 ? taskBounds.width() - dividerWidthPx 924 : taskBounds.height() - dividerWidthPx; 925 926 final float newRatio; 927 if (isVerticalSplit) { 928 final int newPrimaryWidth = isReversedLayout 929 ? taskBounds.width() - (dividerPosition + dividerWidthPx) 930 : dividerPosition; 931 newRatio = 1.0f * newPrimaryWidth / usableSize; 932 } else { 933 final int newPrimaryHeight = isReversedLayout 934 ? taskBounds.height() - (dividerPosition + dividerWidthPx) 935 : dividerPosition; 936 newRatio = 1.0f * newPrimaryHeight / usableSize; 937 } 938 return newRatio; 939 } 940 941 /** Callbacks for drag events */ 942 interface DragEventCallback { 943 /** 944 * Called when the user starts dragging the divider. Callbacks are executed on 945 * {@link #mCallbackExecutor}. 946 * 947 * @param action additional action that should be applied to the 948 * {@link WindowContainerTransaction} 949 */ 950 void onStartDragging(@NonNull Consumer<WindowContainerTransaction> action); 951 952 /** 953 * Called when the user finishes dragging the divider. Callbacks are executed on 954 * {@link #mCallbackExecutor}. 955 * 956 * @param taskId the Task id of the {@link TaskContainer} that this divider belongs to. 957 * @param action additional action that should be applied to the 958 * {@link WindowContainerTransaction} 959 */ 960 void onFinishDragging(int taskId, @NonNull Consumer<WindowContainerTransaction> action); 961 } 962 963 /** 964 * Properties for the {@link DividerPresenter}. The rendering of the divider solely depends on 965 * these properties. When any value is updated, the divider is re-rendered. The Properties 966 * instance is created only when all the pre-conditions of drawing a divider are met. 967 */ 968 @VisibleForTesting 969 static class Properties { 970 private static final int CONFIGURATION_MASK_FOR_DIVIDER = 971 CONFIG_DENSITY | CONFIG_WINDOW_CONFIGURATION | CONFIG_LAYOUT_DIRECTION; 972 @NonNull 973 private final Configuration mConfiguration; 974 @NonNull 975 private final DividerAttributes mDividerAttributes; 976 @NonNull 977 private final SurfaceControl mDecorSurface; 978 979 /** The initial position of the divider calculated based on container bounds. */ 980 private final int mInitialDividerPosition; 981 982 /** Whether the split is vertical, such as left-to-right or right-to-left split. */ 983 private final boolean mIsVerticalSplit; 984 985 private final int mDisplayId; 986 private final boolean mIsReversedLayout; 987 private final boolean mIsDraggableExpandType; 988 @NonNull 989 private final TaskFragmentContainer mPrimaryContainer; 990 @NonNull 991 private final TaskFragmentContainer mSecondaryContainer; 992 private final int mDividerWidthPx; 993 994 @VisibleForTesting 995 Properties( 996 @NonNull Configuration configuration, 997 @NonNull DividerAttributes dividerAttributes, 998 @NonNull SurfaceControl decorSurface, 999 int initialDividerPosition, 1000 boolean isVerticalSplit, 1001 boolean isReversedLayout, 1002 int displayId, 1003 boolean isDraggableExpandType, 1004 @NonNull TaskFragmentContainer primaryContainer, 1005 @NonNull TaskFragmentContainer secondaryContainer) { 1006 mConfiguration = configuration; 1007 mDividerAttributes = dividerAttributes; 1008 mDecorSurface = decorSurface; 1009 mInitialDividerPosition = initialDividerPosition; 1010 mIsVerticalSplit = isVerticalSplit; 1011 mIsReversedLayout = isReversedLayout; 1012 mDisplayId = displayId; 1013 mIsDraggableExpandType = isDraggableExpandType; 1014 mPrimaryContainer = primaryContainer; 1015 mSecondaryContainer = secondaryContainer; 1016 mDividerWidthPx = getDividerWidthPx(dividerAttributes); 1017 } 1018 1019 /** 1020 * Compares whether two Properties objects are equal for rendering the divider. The 1021 * Configuration is checked for rendering related fields, and other fields are checked for 1022 * regular equality. 1023 */ 1024 private static boolean equalsForDivider(@Nullable Properties a, @Nullable Properties b) { 1025 if (a == b) { 1026 return true; 1027 } 1028 if (a == null || b == null) { 1029 return false; 1030 } 1031 return areSameSurfaces(a.mDecorSurface, b.mDecorSurface) 1032 && Objects.equals(a.mDividerAttributes, b.mDividerAttributes) 1033 && areConfigurationsEqualForDivider(a.mConfiguration, b.mConfiguration) 1034 && a.mInitialDividerPosition == b.mInitialDividerPosition 1035 && a.mIsVerticalSplit == b.mIsVerticalSplit 1036 && a.mDisplayId == b.mDisplayId 1037 && a.mIsReversedLayout == b.mIsReversedLayout 1038 && a.mIsDraggableExpandType == b.mIsDraggableExpandType 1039 && a.mPrimaryContainer == b.mPrimaryContainer 1040 && a.mSecondaryContainer == b.mSecondaryContainer; 1041 } 1042 1043 private static boolean areSameSurfaces( 1044 @Nullable SurfaceControl sc1, @Nullable SurfaceControl sc2) { 1045 if (sc1 == sc2) { 1046 // If both are null or both refer to the same object. 1047 return true; 1048 } 1049 if (sc1 == null || sc2 == null) { 1050 return false; 1051 } 1052 return sc1.isSameSurface(sc2); 1053 } 1054 1055 private static boolean areConfigurationsEqualForDivider( 1056 @NonNull Configuration a, @NonNull Configuration b) { 1057 final int diff = a.diff(b); 1058 return (diff & CONFIGURATION_MASK_FOR_DIVIDER) == 0; 1059 } 1060 } 1061 1062 /** 1063 * Handles the rendering of the divider. When the decor surface is updated, the renderer is 1064 * recreated. When other fields in the Properties are changed, the renderer is updated. 1065 */ 1066 @VisibleForTesting 1067 static class Renderer { 1068 @NonNull 1069 private final SurfaceControl mDividerSurface; 1070 @NonNull 1071 private final WindowlessWindowManager mWindowlessWindowManager; 1072 @NonNull 1073 private final SurfaceControlViewHost mViewHost; 1074 @NonNull 1075 private final FrameLayout mDividerLayout; 1076 @NonNull 1077 private final View mDividerLine; 1078 private View mDragHandle; 1079 @NonNull 1080 private final View.OnTouchListener mListener; 1081 @NonNull 1082 private Properties mProperties; 1083 private int mHandleWidthPx; 1084 @Nullable 1085 private SurfaceControl mPrimaryVeil; 1086 @Nullable 1087 private SurfaceControl mSecondaryVeil; 1088 private boolean mIsDragging; 1089 private int mDividerPosition; 1090 private int mDividerSurfaceWidthPx; 1091 1092 private Renderer(@NonNull Properties properties, @NonNull View.OnTouchListener listener) { 1093 mProperties = properties; 1094 mListener = listener; 1095 1096 mDividerSurface = createChildSurface("DividerSurface", true /* visible */); 1097 mWindowlessWindowManager = new WindowlessWindowManager( 1098 mProperties.mConfiguration, 1099 mDividerSurface, 1100 new InputTransferToken()); 1101 1102 final Context context = ActivityThread.currentActivityThread().getApplication(); 1103 final DisplayManager displayManager = context.getSystemService(DisplayManager.class); 1104 mViewHost = new SurfaceControlViewHost( 1105 context, displayManager.getDisplay(mProperties.mDisplayId), 1106 mWindowlessWindowManager, "DividerContainer"); 1107 mDividerLayout = new FrameLayout(context); 1108 mDividerLine = new View(context); 1109 1110 update(); 1111 } 1112 1113 /** Updates the divider when properties are changed */ 1114 private void update(@NonNull Properties newProperties) { 1115 mProperties = newProperties; 1116 update(); 1117 } 1118 1119 /** Updates the divider when initializing or when properties are changed */ 1120 @VisibleForTesting 1121 void update() { 1122 mDividerPosition = mProperties.mInitialDividerPosition; 1123 mWindowlessWindowManager.setConfiguration(mProperties.mConfiguration); 1124 1125 if (mProperties.mDividerAttributes.getDividerType() 1126 == DividerAttributes.DIVIDER_TYPE_DRAGGABLE) { 1127 // TODO(b/329193115) support divider on secondary display 1128 final Context context = ActivityThread.currentActivityThread().getApplication(); 1129 mHandleWidthPx = context.getResources().getDimensionPixelSize( 1130 R.dimen.activity_embedding_divider_touch_target_width); 1131 } else { 1132 mHandleWidthPx = 0; 1133 } 1134 1135 // TODO handle synchronization between surface transactions and WCT. 1136 final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); 1137 updateSurface(t); 1138 updateLayout(); 1139 updateDivider(t); 1140 t.apply(); 1141 } 1142 1143 @VisibleForTesting 1144 void release() { 1145 mViewHost.release(); 1146 // TODO handle synchronization between surface transactions and WCT. 1147 final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); 1148 t.remove(mDividerSurface); 1149 removeVeils(t); 1150 t.apply(); 1151 } 1152 1153 private void setDividerPosition(int dividerPosition) { 1154 mDividerPosition = dividerPosition; 1155 } 1156 1157 /** 1158 * Updates the positions and crops of the divider surface and veil surfaces. This method 1159 * should be called when {@link #mProperties} is changed or while dragging to update the 1160 * position of the divider surface and the veil surfaces. 1161 * 1162 * This method applies the changes in a stand-alone surface transaction immediately. 1163 */ 1164 private void updateSurface() { 1165 final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); 1166 updateSurface(t); 1167 t.apply(); 1168 } 1169 1170 /** 1171 * Updates the positions and crops of the divider surface and veil surfaces. This method 1172 * should be called when {@link #mProperties} is changed or while dragging to update the 1173 * position of the divider surface and the veil surfaces. 1174 * 1175 * This method applies the changes in the provided surface transaction and can be synced 1176 * with other changes. 1177 */ 1178 private void updateSurface(@NonNull SurfaceControl.Transaction t) { 1179 final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); 1180 1181 int dividerSurfacePosition; 1182 if (mProperties.mDividerAttributes.getDividerType() 1183 == DividerAttributes.DIVIDER_TYPE_DRAGGABLE) { 1184 // When the divider drag handle width is larger than the divider width, the position 1185 // of the divider surface is adjusted so that it is large enough to host both the 1186 // divider line and the divider drag handle. 1187 mDividerSurfaceWidthPx = Math.max(mProperties.mDividerWidthPx, mHandleWidthPx); 1188 dividerSurfacePosition = mProperties.mIsReversedLayout 1189 ? mDividerPosition 1190 : mDividerPosition + mProperties.mDividerWidthPx - mDividerSurfaceWidthPx; 1191 dividerSurfacePosition = 1192 Math.clamp(dividerSurfacePosition, 0, 1193 mProperties.mIsVerticalSplit 1194 ? taskBounds.width() - mDividerSurfaceWidthPx 1195 : taskBounds.height() - mDividerSurfaceWidthPx); 1196 } else { 1197 mDividerSurfaceWidthPx = mProperties.mDividerWidthPx; 1198 dividerSurfacePosition = mDividerPosition; 1199 } 1200 1201 if (mProperties.mIsVerticalSplit) { 1202 t.setPosition(mDividerSurface, dividerSurfacePosition, 0.0f); 1203 t.setWindowCrop(mDividerSurface, mDividerSurfaceWidthPx, taskBounds.height()); 1204 } else { 1205 t.setPosition(mDividerSurface, 0.0f, dividerSurfacePosition); 1206 t.setWindowCrop(mDividerSurface, taskBounds.width(), mDividerSurfaceWidthPx); 1207 } 1208 1209 // Update divider line position in the surface 1210 final int offset = mDividerPosition - dividerSurfacePosition; 1211 mDividerLine.setX(mProperties.mIsVerticalSplit ? offset : 0); 1212 mDividerLine.setY(mProperties.mIsVerticalSplit ? 0 : offset); 1213 1214 if (mIsDragging) { 1215 updateVeils(t); 1216 } 1217 } 1218 1219 /** 1220 * Updates the layout parameters of the layout used to host the divider. This method should 1221 * be called only when {@link #mProperties} is changed. This should not be called while 1222 * dragging, because the layout parameters are not changed during dragging. 1223 */ 1224 private void updateLayout() { 1225 final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); 1226 final WindowManager.LayoutParams lp = mProperties.mIsVerticalSplit 1227 ? new WindowManager.LayoutParams( 1228 mDividerSurfaceWidthPx, 1229 taskBounds.height(), 1230 TYPE_APPLICATION_PANEL, 1231 FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL | FLAG_SLIPPERY, 1232 PixelFormat.TRANSLUCENT) 1233 : new WindowManager.LayoutParams( 1234 taskBounds.width(), 1235 mDividerSurfaceWidthPx, 1236 TYPE_APPLICATION_PANEL, 1237 FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL | FLAG_SLIPPERY, 1238 PixelFormat.TRANSLUCENT); 1239 lp.setTitle(WINDOW_NAME); 1240 1241 // Ensure that the divider layout is always LTR regardless of the locale, because we 1242 // already considered the locale when determining the split layout direction and the 1243 // computed divider line position always starts from the left. This only affects the 1244 // horizontal layout and does not have any effect on the top-to-bottom layout. 1245 mDividerLayout.setLayoutDirection(View.LAYOUT_DIRECTION_LTR); 1246 mViewHost.setView(mDividerLayout, lp); 1247 mViewHost.relayout(lp); 1248 } 1249 1250 /** 1251 * Updates the UI component of the divider, including the drag handle and the veils. This 1252 * method should be called only when {@link #mProperties} is changed. This should not be 1253 * called while dragging, because the UI components are not changed during dragging and 1254 * only their surface positions are changed. 1255 */ 1256 private void updateDivider(@NonNull SurfaceControl.Transaction t) { 1257 mDividerLayout.removeAllViews(); 1258 mDividerLayout.addView(mDividerLine); 1259 if (mProperties.mIsDraggableExpandType && !mIsDragging) { 1260 // If a container is fully expanded, the divider overlays on the expanded container. 1261 mDividerLine.setBackgroundColor(Color.TRANSPARENT); 1262 } else { 1263 mDividerLine.setBackgroundColor(mProperties.mDividerAttributes.getDividerColor()); 1264 } 1265 final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); 1266 mDividerLine.setLayoutParams( 1267 mProperties.mIsVerticalSplit 1268 ? new FrameLayout.LayoutParams( 1269 mProperties.mDividerWidthPx, taskBounds.height()) 1270 : new FrameLayout.LayoutParams( 1271 taskBounds.width(), mProperties.mDividerWidthPx) 1272 ); 1273 if (mProperties.mDividerAttributes.getDividerType() 1274 == DividerAttributes.DIVIDER_TYPE_DRAGGABLE) { 1275 createVeils(); 1276 drawDragHandle(); 1277 } else { 1278 removeVeils(t); 1279 } 1280 mViewHost.getView().invalidate(); 1281 } 1282 1283 private void drawDragHandle() { 1284 final Context context = mDividerLayout.getContext(); 1285 final ImageButton button = new ImageButton(context); 1286 final FrameLayout.LayoutParams params = mProperties.mIsVerticalSplit 1287 ? new FrameLayout.LayoutParams( 1288 context.getResources().getDimensionPixelSize( 1289 R.dimen.activity_embedding_divider_touch_target_width), 1290 context.getResources().getDimensionPixelSize( 1291 R.dimen.activity_embedding_divider_touch_target_height)) 1292 : new FrameLayout.LayoutParams( 1293 context.getResources().getDimensionPixelSize( 1294 R.dimen.activity_embedding_divider_touch_target_height), 1295 context.getResources().getDimensionPixelSize( 1296 R.dimen.activity_embedding_divider_touch_target_width)); 1297 params.gravity = Gravity.CENTER; 1298 button.setLayoutParams(params); 1299 button.setBackgroundColor(Color.TRANSPARENT); 1300 1301 final Drawable handle = context.getResources().getDrawable( 1302 R.drawable.activity_embedding_divider_handle, context.getTheme()); 1303 if (mProperties.mIsVerticalSplit) { 1304 button.setImageDrawable(handle); 1305 } else { 1306 // Rotate the handle drawable 1307 RotateDrawable rotatedHandle = new RotateDrawable(); 1308 rotatedHandle.setFromDegrees(90f); 1309 rotatedHandle.setToDegrees(90f); 1310 rotatedHandle.setPivotXRelative(true); 1311 rotatedHandle.setPivotYRelative(true); 1312 rotatedHandle.setPivotX(0.5f); 1313 rotatedHandle.setPivotY(0.5f); 1314 rotatedHandle.setLevel(1); 1315 rotatedHandle.setDrawable(handle); 1316 1317 button.setImageDrawable(rotatedHandle); 1318 } 1319 1320 button.setOnTouchListener(mListener); 1321 mDragHandle = button; 1322 mDividerLayout.addView(button); 1323 } 1324 1325 @NonNull 1326 private SurfaceControl createChildSurface(@NonNull String name, boolean visible) { 1327 final Rect bounds = mProperties.mConfiguration.windowConfiguration.getBounds(); 1328 return new SurfaceControl.Builder() 1329 .setParent(mProperties.mDecorSurface) 1330 .setName(name) 1331 .setHidden(!visible) 1332 .setCallsite("DividerManager.createChildSurface") 1333 .setBufferSize(bounds.width(), bounds.height()) 1334 .setEffectLayer() 1335 .build(); 1336 } 1337 1338 private void createVeils() { 1339 if (mPrimaryVeil == null) { 1340 mPrimaryVeil = createChildSurface("DividerPrimaryVeil", false /* visible */); 1341 } 1342 if (mSecondaryVeil == null) { 1343 mSecondaryVeil = createChildSurface("DividerSecondaryVeil", false /* visible */); 1344 } 1345 } 1346 1347 private void removeVeils(@NonNull SurfaceControl.Transaction t) { 1348 if (mPrimaryVeil != null) { 1349 t.remove(mPrimaryVeil); 1350 } 1351 if (mSecondaryVeil != null) { 1352 t.remove(mSecondaryVeil); 1353 } 1354 mPrimaryVeil = null; 1355 mSecondaryVeil = null; 1356 } 1357 1358 private void showVeils(@NonNull SurfaceControl.Transaction t) { 1359 final Color primaryVeilColor = getContainerBackgroundColor( 1360 mProperties.mPrimaryContainer, DEFAULT_PRIMARY_VEIL_COLOR); 1361 final Color secondaryVeilColor = getContainerBackgroundColor( 1362 mProperties.mSecondaryContainer, DEFAULT_SECONDARY_VEIL_COLOR); 1363 t.setColor(mPrimaryVeil, colorToFloatArray(primaryVeilColor)) 1364 .setColor(mSecondaryVeil, colorToFloatArray(secondaryVeilColor)) 1365 .setLayer(mDividerSurface, DIVIDER_LAYER) 1366 .setLayer(mPrimaryVeil, VEIL_LAYER) 1367 .setLayer(mSecondaryVeil, VEIL_LAYER) 1368 .setVisibility(mPrimaryVeil, true) 1369 .setVisibility(mSecondaryVeil, true); 1370 updateVeils(t); 1371 } 1372 1373 private void hideVeils(@NonNull SurfaceControl.Transaction t) { 1374 t.setVisibility(mPrimaryVeil, false).setVisibility(mSecondaryVeil, false); 1375 } 1376 1377 private void updateVeils(@NonNull SurfaceControl.Transaction t) { 1378 final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); 1379 1380 // Relative bounds of the primary and secondary containers in the Task. 1381 Rect primaryBounds; 1382 Rect secondaryBounds; 1383 if (mProperties.mIsVerticalSplit) { 1384 final Rect boundsLeft = new Rect(0, 0, mDividerPosition, taskBounds.height()); 1385 final Rect boundsRight = new Rect(mDividerPosition + mProperties.mDividerWidthPx, 0, 1386 taskBounds.width(), taskBounds.height()); 1387 primaryBounds = mProperties.mIsReversedLayout ? boundsRight : boundsLeft; 1388 secondaryBounds = mProperties.mIsReversedLayout ? boundsLeft : boundsRight; 1389 } else { 1390 final Rect boundsTop = new Rect(0, 0, taskBounds.width(), mDividerPosition); 1391 final Rect boundsBottom = new Rect( 1392 0, mDividerPosition + mProperties.mDividerWidthPx, 1393 taskBounds.width(), taskBounds.height()); 1394 primaryBounds = mProperties.mIsReversedLayout ? boundsBottom : boundsTop; 1395 secondaryBounds = mProperties.mIsReversedLayout ? boundsTop : boundsBottom; 1396 } 1397 if (mPrimaryVeil != null) { 1398 t.setWindowCrop(mPrimaryVeil, primaryBounds.width(), primaryBounds.height()); 1399 t.setPosition(mPrimaryVeil, primaryBounds.left, primaryBounds.top); 1400 t.setVisibility(mPrimaryVeil, !primaryBounds.isEmpty()); 1401 } 1402 if (mSecondaryVeil != null) { 1403 t.setWindowCrop(mSecondaryVeil, secondaryBounds.width(), secondaryBounds.height()); 1404 t.setPosition(mSecondaryVeil, secondaryBounds.left, secondaryBounds.top); 1405 t.setVisibility(mSecondaryVeil, !secondaryBounds.isEmpty()); 1406 } 1407 } 1408 1409 private static float[] colorToFloatArray(@NonNull Color color) { 1410 return new float[]{color.red(), color.green(), color.blue()}; 1411 } 1412 } 1413 } 1414