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 com.android.quickstep.orientation; 18 19 import static android.view.Gravity.BOTTOM; 20 import static android.view.Gravity.CENTER_HORIZONTAL; 21 import static android.view.Gravity.END; 22 import static android.view.Gravity.START; 23 import static android.view.Gravity.TOP; 24 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 25 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; 26 27 import static com.android.launcher3.Flags.enableOverviewIconMenu; 28 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X; 29 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y; 30 import static com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL; 31 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT; 32 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT; 33 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN; 34 35 import android.graphics.Matrix; 36 import android.graphics.Point; 37 import android.graphics.PointF; 38 import android.graphics.Rect; 39 import android.graphics.RectF; 40 import android.graphics.drawable.ShapeDrawable; 41 import android.util.FloatProperty; 42 import android.util.Pair; 43 import android.view.Gravity; 44 import android.view.Surface; 45 import android.view.View; 46 import android.widget.FrameLayout; 47 import android.widget.LinearLayout; 48 49 import androidx.annotation.NonNull; 50 51 import com.android.launcher3.DeviceProfile; 52 import com.android.launcher3.R; 53 import com.android.launcher3.Utilities; 54 import com.android.launcher3.logger.LauncherAtom; 55 import com.android.launcher3.touch.DefaultPagedViewHandler; 56 import com.android.launcher3.touch.SingleAxisSwipeDetector; 57 import com.android.launcher3.util.SplitConfigurationOptions; 58 import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds; 59 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption; 60 import com.android.launcher3.util.SplitConfigurationOptions.StagePosition; 61 import com.android.quickstep.views.IconAppChipView; 62 63 import java.util.ArrayList; 64 import java.util.List; 65 66 public class PortraitPagedViewHandler extends DefaultPagedViewHandler implements 67 RecentsPagedOrientationHandler { 68 69 private final Matrix mTmpMatrix = new Matrix(); 70 private final RectF mTmpRectF = new RectF(); 71 72 @Override getPrimaryValue(T x, T y)73 public <T> T getPrimaryValue(T x, T y) { 74 return x; 75 } 76 77 @Override getSecondaryValue(T x, T y)78 public <T> T getSecondaryValue(T x, T y) { 79 return y; 80 } 81 82 @Override isLayoutNaturalToLauncher()83 public boolean isLayoutNaturalToLauncher() { 84 return true; 85 } 86 87 @Override adjustFloatingIconStartVelocity(PointF velocity)88 public void adjustFloatingIconStartVelocity(PointF velocity) { 89 //no-op 90 } 91 92 @Override fixBoundsForHomeAnimStartRect(RectF outStartRect, DeviceProfile deviceProfile)93 public void fixBoundsForHomeAnimStartRect(RectF outStartRect, DeviceProfile deviceProfile) { 94 if (outStartRect.left > deviceProfile.widthPx) { 95 outStartRect.offsetTo(0, outStartRect.top); 96 } else if (outStartRect.left < -deviceProfile.widthPx) { 97 outStartRect.offsetTo(0, outStartRect.top); 98 } 99 } 100 101 @Override setSecondary(T target, Float2DAction<T> action, float param)102 public <T> void setSecondary(T target, Float2DAction<T> action, float param) { 103 action.call(target, 0, param); 104 } 105 106 @Override set(T target, Int2DAction<T> action, int primaryParam, int secondaryParam)107 public <T> void set(T target, Int2DAction<T> action, int primaryParam, 108 int secondaryParam) { 109 action.call(target, primaryParam, secondaryParam); 110 } 111 112 @Override getPrimarySize(View view)113 public int getPrimarySize(View view) { 114 return view.getWidth(); 115 } 116 117 @Override getPrimarySize(RectF rect)118 public float getPrimarySize(RectF rect) { 119 return rect.width(); 120 } 121 122 @Override getStart(RectF rect)123 public float getStart(RectF rect) { 124 return rect.left; 125 } 126 127 @Override getEnd(RectF rect)128 public float getEnd(RectF rect) { 129 return rect.right; 130 } 131 132 @Override rotateInsets(@onNull Rect insets, @NonNull Rect outInsets)133 public void rotateInsets(@NonNull Rect insets, @NonNull Rect outInsets) { 134 outInsets.set(insets); 135 } 136 137 @Override getClearAllSidePadding(View view, boolean isRtl)138 public int getClearAllSidePadding(View view, boolean isRtl) { 139 return (isRtl ? view.getPaddingRight() : - view.getPaddingLeft()) / 2; 140 } 141 142 @Override getSecondaryDimension(View view)143 public int getSecondaryDimension(View view) { 144 return view.getHeight(); 145 } 146 147 @Override getPrimaryViewTranslate()148 public FloatProperty<View> getPrimaryViewTranslate() { 149 return VIEW_TRANSLATE_X; 150 } 151 152 @Override getSecondaryViewTranslate()153 public FloatProperty<View> getSecondaryViewTranslate() { 154 return VIEW_TRANSLATE_Y; 155 } 156 157 @Override getDegreesRotated()158 public float getDegreesRotated() { 159 return 0; 160 } 161 162 @Override getRotation()163 public int getRotation() { 164 return Surface.ROTATION_0; 165 } 166 167 @Override setPrimaryScale(View view, float scale)168 public void setPrimaryScale(View view, float scale) { 169 view.setScaleX(scale); 170 } 171 172 @Override setSecondaryScale(View view, float scale)173 public void setSecondaryScale(View view, float scale) { 174 view.setScaleY(scale); 175 } 176 getSecondaryTranslationDirectionFactor()177 public int getSecondaryTranslationDirectionFactor() { 178 return -1; 179 } 180 181 @Override getSplitTranslationDirectionFactor(int stagePosition, DeviceProfile deviceProfile)182 public int getSplitTranslationDirectionFactor(int stagePosition, DeviceProfile deviceProfile) { 183 if (deviceProfile.isLeftRightSplit && stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) { 184 return -1; 185 } else { 186 return 1; 187 } 188 } 189 190 @Override getTaskMenuX(float x, View thumbnailView, DeviceProfile deviceProfile, float taskInsetMargin, View taskViewIcon)191 public float getTaskMenuX(float x, View thumbnailView, 192 DeviceProfile deviceProfile, float taskInsetMargin, View taskViewIcon) { 193 if (deviceProfile.isLandscape) { 194 return x + taskInsetMargin 195 + (thumbnailView.getMeasuredWidth() - thumbnailView.getMeasuredHeight()) / 2f; 196 } else { 197 return x + taskInsetMargin; 198 } 199 } 200 201 @Override getTaskMenuY(float y, View thumbnailView, int stagePosition, View taskMenuView, float taskInsetMargin, View taskViewIcon)202 public float getTaskMenuY(float y, View thumbnailView, int stagePosition, 203 View taskMenuView, float taskInsetMargin, View taskViewIcon) { 204 return y + taskInsetMargin; 205 } 206 207 @Override getTaskMenuWidth(View thumbnailView, DeviceProfile deviceProfile, @StagePosition int stagePosition)208 public int getTaskMenuWidth(View thumbnailView, DeviceProfile deviceProfile, 209 @StagePosition int stagePosition) { 210 if (enableOverviewIconMenu()) { 211 return thumbnailView.getResources().getDimensionPixelSize( 212 R.dimen.task_thumbnail_icon_menu_expanded_width); 213 } 214 int padding = thumbnailView.getResources() 215 .getDimensionPixelSize(R.dimen.task_menu_edge_padding); 216 return (deviceProfile.isLandscape && !deviceProfile.isTablet 217 ? thumbnailView.getMeasuredHeight() 218 : thumbnailView.getMeasuredWidth()) - (2 * padding); 219 } 220 221 @Override getTaskMenuHeight(float taskInsetMargin, DeviceProfile deviceProfile, float taskMenuX, float taskMenuY)222 public int getTaskMenuHeight(float taskInsetMargin, DeviceProfile deviceProfile, 223 float taskMenuX, float taskMenuY) { 224 return (int) (deviceProfile.heightPx - deviceProfile.getInsets().top - taskMenuY 225 - deviceProfile.getOverviewActionsClaimedSpaceBelow()); 226 } 227 228 @Override setTaskOptionsMenuLayoutOrientation(DeviceProfile deviceProfile, LinearLayout taskMenuLayout, int dividerSpacing, ShapeDrawable dividerDrawable)229 public void setTaskOptionsMenuLayoutOrientation(DeviceProfile deviceProfile, 230 LinearLayout taskMenuLayout, int dividerSpacing, 231 ShapeDrawable dividerDrawable) { 232 taskMenuLayout.setOrientation(LinearLayout.VERTICAL); 233 dividerDrawable.setIntrinsicHeight(dividerSpacing); 234 taskMenuLayout.setDividerDrawable(dividerDrawable); 235 } 236 237 @Override setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp, LinearLayout viewGroup, DeviceProfile deviceProfile)238 public void setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp, 239 LinearLayout viewGroup, DeviceProfile deviceProfile) { 240 viewGroup.setOrientation(LinearLayout.HORIZONTAL); 241 lp.width = LinearLayout.LayoutParams.MATCH_PARENT; 242 lp.height = WRAP_CONTENT; 243 } 244 245 @Override getDwbLayoutTranslations(int taskViewWidth, int taskViewHeight, SplitBounds splitBounds, DeviceProfile deviceProfile, View[] thumbnailViews, int desiredTaskId, View banner)246 public Pair<Float, Float> getDwbLayoutTranslations(int taskViewWidth, 247 int taskViewHeight, SplitBounds splitBounds, DeviceProfile deviceProfile, 248 View[] thumbnailViews, int desiredTaskId, View banner) { 249 float translationX = 0; 250 float translationY = 0; 251 FrameLayout.LayoutParams bannerParams = (FrameLayout.LayoutParams) banner.getLayoutParams(); 252 banner.setPivotX(0); 253 banner.setPivotY(0); 254 banner.setRotation(getDegreesRotated()); 255 if (splitBounds == null) { 256 // Single, fullscreen case 257 bannerParams.width = MATCH_PARENT; 258 bannerParams.gravity = BOTTOM | CENTER_HORIZONTAL; 259 return new Pair<>(translationX, translationY); 260 } 261 262 bannerParams.gravity = 263 BOTTOM | (deviceProfile.isLeftRightSplit ? START : CENTER_HORIZONTAL); 264 265 // Set correct width 266 if (desiredTaskId == splitBounds.leftTopTaskId) { 267 bannerParams.width = thumbnailViews[0].getMeasuredWidth(); 268 } else { 269 bannerParams.width = thumbnailViews[1].getMeasuredWidth(); 270 } 271 272 // Set translations 273 if (deviceProfile.isLeftRightSplit) { 274 if (desiredTaskId == splitBounds.rightBottomTaskId) { 275 float leftTopTaskPercent = splitBounds.appsStackedVertically 276 ? splitBounds.topTaskPercent 277 : splitBounds.leftTaskPercent; 278 float dividerThicknessPercent = splitBounds.appsStackedVertically 279 ? splitBounds.dividerHeightPercent 280 : splitBounds.dividerWidthPercent; 281 translationX = ((taskViewWidth * leftTopTaskPercent) 282 + (taskViewWidth * dividerThicknessPercent)); 283 } 284 } else { 285 if (desiredTaskId == splitBounds.leftTopTaskId) { 286 FrameLayout.LayoutParams snapshotParams = 287 (FrameLayout.LayoutParams) thumbnailViews[0] 288 .getLayoutParams(); 289 float bottomRightTaskPlusDividerPercent = splitBounds.appsStackedVertically 290 ? (1f - splitBounds.topTaskPercent) 291 : (1f - splitBounds.leftTaskPercent); 292 translationY = -((taskViewHeight - snapshotParams.topMargin) 293 * bottomRightTaskPlusDividerPercent); 294 } 295 } 296 return new Pair<>(translationX, translationY); 297 } 298 299 /* ---------- The following are only used by TaskViewTouchHandler. ---------- */ 300 301 @Override getUpDownSwipeDirection()302 public SingleAxisSwipeDetector.Direction getUpDownSwipeDirection() { 303 return VERTICAL; 304 } 305 306 @Override getUpDirection(boolean isRtl)307 public int getUpDirection(boolean isRtl) { 308 // Ignore rtl since it only affects X value displacement, Y displacement doesn't change 309 return SingleAxisSwipeDetector.DIRECTION_POSITIVE; 310 } 311 312 @Override isGoingUp(float displacement, boolean isRtl)313 public boolean isGoingUp(float displacement, boolean isRtl) { 314 // Ignore rtl since it only affects X value displacement, Y displacement doesn't change 315 return displacement < 0; 316 } 317 318 @Override getTaskDragDisplacementFactor(boolean isRtl)319 public int getTaskDragDisplacementFactor(boolean isRtl) { 320 // Ignore rtl since it only affects X value displacement, Y displacement doesn't change 321 return 1; 322 } 323 324 /* -------------------- */ 325 @Override getDistanceToBottomOfRect(DeviceProfile dp, Rect rect)326 public int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect) { 327 return dp.heightPx - rect.bottom; 328 } 329 330 @Override getSplitPositionOptions(DeviceProfile dp)331 public List<SplitPositionOption> getSplitPositionOptions(DeviceProfile dp) { 332 if (dp.isTablet) { 333 return Utilities.getSplitPositionOptions(dp); 334 } 335 336 List<SplitPositionOption> options = new ArrayList<>(); 337 if (dp.isSeascape()) { 338 options.add(new SplitPositionOption( 339 R.drawable.ic_split_horizontal, R.string.recent_task_option_split_screen, 340 STAGE_POSITION_BOTTOM_OR_RIGHT, STAGE_TYPE_MAIN)); 341 } else if (dp.isLeftRightSplit) { 342 options.add(new SplitPositionOption( 343 R.drawable.ic_split_horizontal, R.string.recent_task_option_split_screen, 344 STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN)); 345 } else { 346 // Only add top option 347 options.add(new SplitPositionOption( 348 R.drawable.ic_split_vertical, R.string.recent_task_option_split_screen, 349 STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN)); 350 } 351 return options; 352 } 353 354 @Override getInitialSplitPlaceholderBounds(int placeholderHeight, int placeholderInset, DeviceProfile dp, @StagePosition int stagePosition, Rect out)355 public void getInitialSplitPlaceholderBounds(int placeholderHeight, int placeholderInset, 356 DeviceProfile dp, @StagePosition int stagePosition, Rect out) { 357 int screenWidth = dp.widthPx; 358 int screenHeight = dp.heightPx; 359 boolean pinToRight = stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT; 360 int insetSizeAdjustment = getPlaceholderSizeAdjustment(dp, pinToRight); 361 362 out.set(0, 0, screenWidth, placeholderHeight + insetSizeAdjustment); 363 if (!dp.isLeftRightSplit) { 364 // portrait, phone or tablet - spans width of screen, nothing else to do 365 out.inset(placeholderInset, 0); 366 367 // Adjust the top to account for content off screen. This will help to animate the view 368 // in with rounded corners. 369 int totalHeight = (int) (1.0f * screenHeight / 2 * (screenWidth - 2 * placeholderInset) 370 / screenWidth); 371 out.top -= (totalHeight - placeholderHeight); 372 return; 373 } 374 375 // Now we rotate the portrait rect depending on what side we want pinned 376 377 float postRotateScale = (float) screenHeight / screenWidth; 378 mTmpMatrix.reset(); 379 mTmpMatrix.postRotate(pinToRight ? 90 : 270); 380 mTmpMatrix.postTranslate(pinToRight ? screenWidth : 0, pinToRight ? 0 : screenWidth); 381 // The placeholder height stays constant after rotation, so we don't change width scale 382 mTmpMatrix.postScale(1, postRotateScale); 383 384 mTmpRectF.set(out); 385 mTmpMatrix.mapRect(mTmpRectF); 386 mTmpRectF.inset(0, placeholderInset); 387 mTmpRectF.roundOut(out); 388 389 // Adjust the top to account for content off screen. This will help to animate the view in 390 // with rounded corners. 391 int totalWidth = (int) (1.0f * screenWidth / 2 * (screenHeight - 2 * placeholderInset) 392 / screenHeight); 393 int width = out.width(); 394 if (pinToRight) { 395 out.right += totalWidth - width; 396 } else { 397 out.left -= totalWidth - width; 398 } 399 } 400 401 @Override updateSplitIconParams(View out, float onScreenRectCenterX, float onScreenRectCenterY, float fullscreenScaleX, float fullscreenScaleY, int drawableWidth, int drawableHeight, DeviceProfile dp, @StagePosition int stagePosition)402 public void updateSplitIconParams(View out, float onScreenRectCenterX, 403 float onScreenRectCenterY, float fullscreenScaleX, float fullscreenScaleY, 404 int drawableWidth, int drawableHeight, DeviceProfile dp, 405 @StagePosition int stagePosition) { 406 boolean pinToRight = stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT; 407 float insetAdjustment = getPlaceholderSizeAdjustment(dp, pinToRight) / 2f; 408 if (!dp.isLeftRightSplit) { 409 out.setX(onScreenRectCenterX / fullscreenScaleX 410 - 1.0f * drawableWidth / 2); 411 out.setY((onScreenRectCenterY + insetAdjustment) / fullscreenScaleY 412 - 1.0f * drawableHeight / 2); 413 } else { 414 if (pinToRight) { 415 out.setX((onScreenRectCenterX - insetAdjustment) / fullscreenScaleX 416 - 1.0f * drawableWidth / 2); 417 } else { 418 out.setX((onScreenRectCenterX + insetAdjustment) / fullscreenScaleX 419 - 1.0f * drawableWidth / 2); 420 } 421 out.setY(onScreenRectCenterY / fullscreenScaleY 422 - 1.0f * drawableHeight / 2); 423 } 424 } 425 426 /** 427 * The split placeholder comes with a default inset to buffer the icon from the top of the 428 * screen. But if the device already has a large inset (from cutouts etc), use that instead. 429 */ getPlaceholderSizeAdjustment(DeviceProfile dp, boolean pinToRight)430 private int getPlaceholderSizeAdjustment(DeviceProfile dp, boolean pinToRight) { 431 int insetThickness; 432 if (!dp.isLandscape) { 433 insetThickness = dp.getInsets().top; 434 } else { 435 insetThickness = pinToRight ? dp.getInsets().right : dp.getInsets().left; 436 } 437 return Math.max(insetThickness - dp.splitPlaceholderInset, 0); 438 } 439 440 @Override setSplitInstructionsParams(View out, DeviceProfile dp, int splitInstructionsHeight, int splitInstructionsWidth)441 public void setSplitInstructionsParams(View out, DeviceProfile dp, int splitInstructionsHeight, 442 int splitInstructionsWidth) { 443 out.setPivotX(0); 444 out.setPivotY(splitInstructionsHeight); 445 out.setRotation(getDegreesRotated()); 446 int distanceToEdge; 447 if (dp.isPhone) { 448 if (dp.isLandscape) { 449 distanceToEdge = out.getResources().getDimensionPixelSize( 450 R.dimen.split_instructions_bottom_margin_phone_landscape); 451 } else { 452 distanceToEdge = out.getResources().getDimensionPixelSize( 453 R.dimen.split_instructions_bottom_margin_phone_portrait); 454 } 455 } else { 456 distanceToEdge = dp.getOverviewActionsClaimedSpaceBelow(); 457 } 458 459 // Center the view in case of unbalanced insets on left or right of screen 460 int insetCorrectionX = (dp.getInsets().right - dp.getInsets().left) / 2; 461 // Adjust for any insets on the bottom edge 462 int insetCorrectionY = dp.getInsets().bottom; 463 out.setTranslationX(insetCorrectionX); 464 out.setTranslationY(-distanceToEdge + insetCorrectionY); 465 FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) out.getLayoutParams(); 466 lp.gravity = CENTER_HORIZONTAL | BOTTOM; 467 out.setLayoutParams(lp); 468 } 469 470 @Override getFinalSplitPlaceholderBounds(int splitDividerSize, DeviceProfile dp, @StagePosition int stagePosition, Rect out1, Rect out2)471 public void getFinalSplitPlaceholderBounds(int splitDividerSize, DeviceProfile dp, 472 @StagePosition int stagePosition, Rect out1, Rect out2) { 473 int screenHeight = dp.heightPx; 474 int screenWidth = dp.widthPx; 475 out1.set(0, 0, screenWidth, screenHeight / 2 - splitDividerSize); 476 out2.set(0, screenHeight / 2 + splitDividerSize, screenWidth, screenHeight); 477 if (!dp.isLeftRightSplit) { 478 // Portrait - the window bounds are always top and bottom half 479 return; 480 } 481 482 // Now we rotate the portrait rect depending on what side we want pinned 483 boolean pinToRight = stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT; 484 float postRotateScale = (float) screenHeight / screenWidth; 485 486 mTmpMatrix.reset(); 487 mTmpMatrix.postRotate(pinToRight ? 90 : 270); 488 mTmpMatrix.postTranslate(pinToRight ? screenHeight : 0, pinToRight ? 0 : screenWidth); 489 mTmpMatrix.postScale(1 / postRotateScale, postRotateScale); 490 491 mTmpRectF.set(out1); 492 mTmpMatrix.mapRect(mTmpRectF); 493 mTmpRectF.roundOut(out1); 494 495 mTmpRectF.set(out2); 496 mTmpMatrix.mapRect(mTmpRectF); 497 mTmpRectF.roundOut(out2); 498 } 499 500 @Override setSplitTaskSwipeRect(DeviceProfile dp, Rect outRect, SplitBounds splitInfo, int desiredStagePosition)501 public void setSplitTaskSwipeRect(DeviceProfile dp, Rect outRect, 502 SplitBounds splitInfo, int desiredStagePosition) { 503 float topLeftTaskPercent = splitInfo.appsStackedVertically 504 ? splitInfo.topTaskPercent 505 : splitInfo.leftTaskPercent; 506 float dividerBarPercent = splitInfo.appsStackedVertically 507 ? splitInfo.dividerHeightPercent 508 : splitInfo.dividerWidthPercent; 509 510 int taskbarHeight = dp.isTransientTaskbar ? 0 : dp.taskbarHeight; 511 float scale = (float) outRect.height() / (dp.availableHeightPx - taskbarHeight); 512 float topTaskHeight = dp.availableHeightPx * topLeftTaskPercent; 513 float scaledTopTaskHeight = topTaskHeight * scale; 514 float dividerHeight = dp.availableHeightPx * dividerBarPercent; 515 float scaledDividerHeight = dividerHeight * scale; 516 517 if (desiredStagePosition == SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT) { 518 if (dp.isLeftRightSplit) { 519 outRect.right = outRect.left + Math.round(outRect.width() * topLeftTaskPercent); 520 } else { 521 outRect.bottom = Math.round(outRect.top + scaledTopTaskHeight); 522 } 523 } else { 524 if (dp.isLeftRightSplit) { 525 outRect.left += Math.round(outRect.width() 526 * (topLeftTaskPercent + dividerBarPercent)); 527 } else { 528 outRect.top += Math.round(scaledTopTaskHeight + scaledDividerHeight); 529 } 530 } 531 } 532 533 @Override measureGroupedTaskViewThumbnailBounds(View primarySnapshot, View secondarySnapshot, int parentWidth, int parentHeight, SplitBounds splitBoundsConfig, DeviceProfile dp, boolean isRtl)534 public void measureGroupedTaskViewThumbnailBounds(View primarySnapshot, View secondarySnapshot, 535 int parentWidth, int parentHeight, SplitBounds splitBoundsConfig, 536 DeviceProfile dp, boolean isRtl) { 537 int spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx; 538 int totalThumbnailHeight = parentHeight - spaceAboveSnapshot; 539 float dividerScale = splitBoundsConfig.appsStackedVertically 540 ? splitBoundsConfig.dividerHeightPercent 541 : splitBoundsConfig.dividerWidthPercent; 542 Pair<Point, Point> taskViewSizes = 543 getGroupedTaskViewSizes(dp, splitBoundsConfig, parentWidth, parentHeight); 544 if (dp.isLeftRightSplit) { 545 int scaledDividerBar = Math.round(parentWidth * dividerScale); 546 if (isRtl) { 547 int translationX = taskViewSizes.second.x + scaledDividerBar; 548 primarySnapshot.setTranslationX(-translationX); 549 secondarySnapshot.setTranslationX(0); 550 } else { 551 int translationX = taskViewSizes.first.x + scaledDividerBar; 552 secondarySnapshot.setTranslationX(translationX); 553 primarySnapshot.setTranslationX(0); 554 } 555 secondarySnapshot.setTranslationY(spaceAboveSnapshot); 556 557 // Reset unused translations 558 primarySnapshot.setTranslationY(0); 559 } else { 560 float finalDividerHeight = Math.round(totalThumbnailHeight * dividerScale); 561 float translationY = taskViewSizes.first.y + spaceAboveSnapshot + finalDividerHeight; 562 secondarySnapshot.setTranslationY(translationY); 563 564 FrameLayout.LayoutParams primaryParams = 565 (FrameLayout.LayoutParams) primarySnapshot.getLayoutParams(); 566 FrameLayout.LayoutParams secondaryParams = 567 (FrameLayout.LayoutParams) secondarySnapshot.getLayoutParams(); 568 secondaryParams.topMargin = 0; 569 primaryParams.topMargin = spaceAboveSnapshot; 570 571 // Reset unused translations 572 primarySnapshot.setTranslationY(0); 573 secondarySnapshot.setTranslationX(0); 574 primarySnapshot.setTranslationX(0); 575 } 576 primarySnapshot.measure( 577 View.MeasureSpec.makeMeasureSpec(taskViewSizes.first.x, View.MeasureSpec.EXACTLY), 578 View.MeasureSpec.makeMeasureSpec(taskViewSizes.first.y, View.MeasureSpec.EXACTLY)); 579 secondarySnapshot.measure( 580 View.MeasureSpec.makeMeasureSpec(taskViewSizes.second.x, View.MeasureSpec.EXACTLY), 581 View.MeasureSpec.makeMeasureSpec(taskViewSizes.second.y, 582 View.MeasureSpec.EXACTLY)); 583 primarySnapshot.setScaleX(1); 584 secondarySnapshot.setScaleX(1); 585 primarySnapshot.setScaleY(1); 586 secondarySnapshot.setScaleY(1); 587 } 588 589 @Override getGroupedTaskViewSizes( DeviceProfile dp, SplitBounds splitBoundsConfig, int parentWidth, int parentHeight)590 public Pair<Point, Point> getGroupedTaskViewSizes( 591 DeviceProfile dp, 592 SplitBounds splitBoundsConfig, 593 int parentWidth, 594 int parentHeight) { 595 int spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx; 596 int totalThumbnailHeight = parentHeight - spaceAboveSnapshot; 597 float dividerScale = splitBoundsConfig.appsStackedVertically 598 ? splitBoundsConfig.dividerHeightPercent 599 : splitBoundsConfig.dividerWidthPercent; 600 float taskPercent = splitBoundsConfig.appsStackedVertically 601 ? splitBoundsConfig.topTaskPercent 602 : splitBoundsConfig.leftTaskPercent; 603 604 Point firstTaskViewSize = new Point(); 605 Point secondTaskViewSize = new Point(); 606 607 if (dp.isLeftRightSplit) { 608 int scaledDividerBar = Math.round(parentWidth * dividerScale); 609 firstTaskViewSize.x = Math.round(parentWidth * taskPercent); 610 firstTaskViewSize.y = totalThumbnailHeight; 611 612 secondTaskViewSize.x = parentWidth - firstTaskViewSize.x - scaledDividerBar; 613 secondTaskViewSize.y = totalThumbnailHeight; 614 } else { 615 int taskbarHeight = dp.isTransientTaskbar ? 0 : dp.taskbarHeight; 616 float scale = (float) totalThumbnailHeight / (dp.availableHeightPx - taskbarHeight); 617 float topTaskHeight = dp.availableHeightPx * taskPercent; 618 float finalDividerHeight = Math.round(totalThumbnailHeight * dividerScale); 619 float scaledTopTaskHeight = topTaskHeight * scale; 620 firstTaskViewSize.x = parentWidth; 621 firstTaskViewSize.y = Math.round(scaledTopTaskHeight); 622 623 secondTaskViewSize.x = parentWidth; 624 secondTaskViewSize.y = Math.round(totalThumbnailHeight - firstTaskViewSize.y 625 - finalDividerHeight); 626 } 627 628 return new Pair<>(firstTaskViewSize, secondTaskViewSize); 629 } 630 631 @Override setTaskIconParams(FrameLayout.LayoutParams iconParams, int taskIconMargin, int taskIconHeight, int thumbnailTopMargin, boolean isRtl)632 public void setTaskIconParams(FrameLayout.LayoutParams iconParams, int taskIconMargin, 633 int taskIconHeight, int thumbnailTopMargin, boolean isRtl) { 634 iconParams.gravity = TOP | CENTER_HORIZONTAL; 635 // Reset margins, since they may have been set on rotation 636 iconParams.leftMargin = iconParams.rightMargin = 0; 637 iconParams.topMargin = iconParams.bottomMargin = 0; 638 } 639 640 @Override setIconAppChipChildrenParams(FrameLayout.LayoutParams iconParams, int chipChildMarginStart)641 public void setIconAppChipChildrenParams(FrameLayout.LayoutParams iconParams, 642 int chipChildMarginStart) { 643 iconParams.setMarginStart(chipChildMarginStart); 644 iconParams.gravity = Gravity.START | Gravity.CENTER_VERTICAL; 645 iconParams.topMargin = 0; 646 } 647 648 @Override setIconAppChipMenuParams(IconAppChipView iconAppChipView, FrameLayout.LayoutParams iconMenuParams, int iconMenuMargin, int thumbnailTopMargin)649 public void setIconAppChipMenuParams(IconAppChipView iconAppChipView, 650 FrameLayout.LayoutParams iconMenuParams, int iconMenuMargin, int thumbnailTopMargin) { 651 iconMenuParams.gravity = TOP | START; 652 iconMenuParams.setMarginStart(iconMenuMargin); 653 iconMenuParams.topMargin = thumbnailTopMargin; 654 iconMenuParams.bottomMargin = 0; 655 iconMenuParams.setMarginEnd(0); 656 657 iconAppChipView.setPivotX(0); 658 iconAppChipView.setPivotY(0); 659 iconAppChipView.setSplitTranslationY(0); 660 iconAppChipView.setRotation(getDegreesRotated()); 661 } 662 663 @Override setSplitIconParams(View primaryIconView, View secondaryIconView, int taskIconHeight, int primarySnapshotWidth, int primarySnapshotHeight, int groupedTaskViewHeight, int groupedTaskViewWidth, boolean isRtl, DeviceProfile deviceProfile, SplitBounds splitConfig)664 public void setSplitIconParams(View primaryIconView, View secondaryIconView, 665 int taskIconHeight, int primarySnapshotWidth, int primarySnapshotHeight, 666 int groupedTaskViewHeight, int groupedTaskViewWidth, boolean isRtl, 667 DeviceProfile deviceProfile, SplitBounds splitConfig) { 668 FrameLayout.LayoutParams primaryIconParams = 669 (FrameLayout.LayoutParams) primaryIconView.getLayoutParams(); 670 FrameLayout.LayoutParams secondaryIconParams = enableOverviewIconMenu() 671 ? (FrameLayout.LayoutParams) secondaryIconView.getLayoutParams() 672 : new FrameLayout.LayoutParams(primaryIconParams); 673 674 if (enableOverviewIconMenu()) { 675 IconAppChipView primaryAppChipView = (IconAppChipView) primaryIconView; 676 IconAppChipView secondaryAppChipView = (IconAppChipView) secondaryIconView; 677 primaryIconParams.gravity = TOP | START; 678 secondaryIconParams.gravity = TOP | START; 679 secondaryIconParams.topMargin = primaryIconParams.topMargin; 680 secondaryIconParams.setMarginStart(primaryIconParams.getMarginStart()); 681 if (deviceProfile.isLeftRightSplit) { 682 if (isRtl) { 683 int secondarySnapshotWidth = groupedTaskViewWidth - primarySnapshotWidth; 684 primaryAppChipView.setSplitTranslationX(-secondarySnapshotWidth); 685 } else { 686 secondaryAppChipView.setSplitTranslationX(primarySnapshotWidth); 687 } 688 } else { 689 primaryAppChipView.setSplitTranslationX(0); 690 secondaryAppChipView.setSplitTranslationX(0); 691 int dividerThickness = Math.min(splitConfig.visualDividerBounds.width(), 692 splitConfig.visualDividerBounds.height()); 693 secondaryAppChipView.setSplitTranslationY( 694 primarySnapshotHeight + (deviceProfile.isTablet ? 0 : dividerThickness)); 695 } 696 } else if (deviceProfile.isLeftRightSplit) { 697 // We calculate the "midpoint" of the thumbnail area, and place the icons there. 698 // This is the place where the thumbnail area splits by default, in a near-50/50 split. 699 // It is usually not exactly 50/50, due to insets/screen cutouts. 700 int fullscreenInsetThickness = deviceProfile.isSeascape() 701 ? deviceProfile.getInsets().right 702 : deviceProfile.getInsets().left; 703 int fullscreenMidpointFromBottom = ((deviceProfile.widthPx 704 - fullscreenInsetThickness) / 2); 705 float midpointFromEndPct = (float) fullscreenMidpointFromBottom 706 / deviceProfile.widthPx; 707 float insetPct = (float) fullscreenInsetThickness / deviceProfile.widthPx; 708 int spaceAboveSnapshots = 0; 709 int overviewThumbnailAreaThickness = groupedTaskViewWidth - spaceAboveSnapshots; 710 int bottomToMidpointOffset = (int) (overviewThumbnailAreaThickness 711 * midpointFromEndPct); 712 int insetOffset = (int) (overviewThumbnailAreaThickness * insetPct); 713 714 if (deviceProfile.isSeascape()) { 715 primaryIconParams.gravity = TOP | (isRtl ? END : START); 716 secondaryIconParams.gravity = TOP | (isRtl ? END : START); 717 if (splitConfig.initiatedFromSeascape) { 718 // if the split was initiated from seascape, 719 // the task on the right (secondary) is slightly larger 720 primaryIconView.setTranslationX(bottomToMidpointOffset - taskIconHeight); 721 secondaryIconView.setTranslationX(bottomToMidpointOffset); 722 } else { 723 // if not, 724 // the task on the left (primary) is slightly larger 725 primaryIconView.setTranslationX(bottomToMidpointOffset + insetOffset 726 - taskIconHeight); 727 secondaryIconView.setTranslationX(bottomToMidpointOffset + insetOffset); 728 } 729 } else { 730 primaryIconParams.gravity = TOP | (isRtl ? START : END); 731 secondaryIconParams.gravity = TOP | (isRtl ? START : END); 732 if (!splitConfig.initiatedFromSeascape) { 733 // if the split was initiated from landscape, 734 // the task on the left (primary) is slightly larger 735 primaryIconView.setTranslationX(-bottomToMidpointOffset); 736 secondaryIconView.setTranslationX(-bottomToMidpointOffset + taskIconHeight); 737 } else { 738 // if not, 739 // the task on the right (secondary) is slightly larger 740 primaryIconView.setTranslationX(-bottomToMidpointOffset - insetOffset); 741 secondaryIconView.setTranslationX(-bottomToMidpointOffset - insetOffset 742 + taskIconHeight); 743 } 744 } 745 } else { 746 primaryIconParams.gravity = TOP | CENTER_HORIZONTAL; 747 // shifts icon half a width left (height is used here since icons are square) 748 primaryIconView.setTranslationX(-(taskIconHeight / 2f)); 749 secondaryIconParams.gravity = TOP | CENTER_HORIZONTAL; 750 secondaryIconView.setTranslationX(taskIconHeight / 2f); 751 } 752 if (!enableOverviewIconMenu()) { 753 primaryIconView.setTranslationY(0); 754 secondaryIconView.setTranslationY(0); 755 } 756 757 primaryIconView.setLayoutParams(primaryIconParams); 758 secondaryIconView.setLayoutParams(secondaryIconParams); 759 } 760 761 @Override getDefaultSplitPosition(DeviceProfile deviceProfile)762 public int getDefaultSplitPosition(DeviceProfile deviceProfile) { 763 if (!deviceProfile.isTablet) { 764 throw new IllegalStateException("Default position available only for large screens"); 765 } 766 if (deviceProfile.isLeftRightSplit) { 767 return STAGE_POSITION_BOTTOM_OR_RIGHT; 768 } else { 769 return STAGE_POSITION_TOP_OR_LEFT; 770 } 771 } 772 773 @Override getSplitSelectTaskOffset(FloatProperty primary, FloatProperty secondary, DeviceProfile deviceProfile)774 public Pair<FloatProperty, FloatProperty> getSplitSelectTaskOffset(FloatProperty primary, 775 FloatProperty secondary, DeviceProfile deviceProfile) { 776 if (deviceProfile.isLeftRightSplit) { // or seascape 777 return new Pair<>(primary, secondary); 778 } else { 779 return new Pair<>(secondary, primary); 780 } 781 } 782 783 @Override getFloatingTaskOffscreenTranslationTarget(View floatingTask, RectF onScreenRect, @StagePosition int stagePosition, DeviceProfile dp)784 public float getFloatingTaskOffscreenTranslationTarget(View floatingTask, RectF onScreenRect, 785 @StagePosition int stagePosition, DeviceProfile dp) { 786 if (dp.isLeftRightSplit) { 787 float currentTranslationX = floatingTask.getTranslationX(); 788 return stagePosition == STAGE_POSITION_TOP_OR_LEFT 789 ? currentTranslationX - onScreenRect.width() 790 : currentTranslationX + onScreenRect.width(); 791 } else { 792 float currentTranslationY = floatingTask.getTranslationY(); 793 return currentTranslationY - onScreenRect.height(); 794 } 795 } 796 797 @Override setFloatingTaskPrimaryTranslation(View floatingTask, float translation, DeviceProfile dp)798 public void setFloatingTaskPrimaryTranslation(View floatingTask, float translation, 799 DeviceProfile dp) { 800 if (dp.isLeftRightSplit) { 801 floatingTask.setTranslationX(translation); 802 } else { 803 floatingTask.setTranslationY(translation); 804 } 805 806 } 807 808 @Override getFloatingTaskPrimaryTranslation(View floatingTask, DeviceProfile dp)809 public float getFloatingTaskPrimaryTranslation(View floatingTask, DeviceProfile dp) { 810 return dp.isLeftRightSplit 811 ? floatingTask.getTranslationX() 812 : floatingTask.getTranslationY(); 813 } 814 815 @NonNull 816 @Override getHandlerTypeForLogging()817 public LauncherAtom.TaskSwitcherContainer.OrientationHandler getHandlerTypeForLogging() { 818 return LauncherAtom.TaskSwitcherContainer.OrientationHandler.PORTRAIT; 819 } 820 } 821