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 package com.android.quickstep.orientation 17 18 import android.annotation.SuppressLint 19 import android.content.res.Resources 20 import android.graphics.Point 21 import android.graphics.PointF 22 import android.graphics.Rect 23 import android.graphics.RectF 24 import android.graphics.drawable.ShapeDrawable 25 import android.util.FloatProperty 26 import android.util.Pair 27 import android.view.Gravity 28 import android.view.MotionEvent 29 import android.view.Surface 30 import android.view.VelocityTracker 31 import android.view.View 32 import android.view.View.MeasureSpec 33 import android.view.ViewGroup 34 import android.view.accessibility.AccessibilityEvent 35 import android.widget.FrameLayout 36 import android.widget.LinearLayout 37 import androidx.annotation.VisibleForTesting 38 import androidx.core.util.component1 39 import androidx.core.util.component2 40 import com.android.launcher3.DeviceProfile 41 import com.android.launcher3.Flags 42 import com.android.launcher3.LauncherAnimUtils 43 import com.android.launcher3.R 44 import com.android.launcher3.Utilities 45 import com.android.launcher3.logger.LauncherAtom.TaskSwitcherContainer 46 import com.android.launcher3.touch.PagedOrientationHandler.ChildBounds 47 import com.android.launcher3.touch.PagedOrientationHandler.Float2DAction 48 import com.android.launcher3.touch.PagedOrientationHandler.Int2DAction 49 import com.android.launcher3.touch.SingleAxisSwipeDetector 50 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT 51 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT 52 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED 53 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN 54 import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds 55 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption 56 import com.android.launcher3.util.SplitConfigurationOptions.StagePosition 57 import com.android.launcher3.views.BaseDragLayer 58 import com.android.quickstep.views.IconAppChipView 59 import kotlin.math.max 60 61 open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { getPrimaryValuenull62 override fun <T> getPrimaryValue(x: T, y: T): T = y 63 64 override fun <T> getSecondaryValue(x: T, y: T): T = x 65 66 override fun getPrimaryValue(x: Int, y: Int): Int = y 67 68 override fun getSecondaryValue(x: Int, y: Int): Int = x 69 70 override fun getPrimaryValue(x: Float, y: Float): Float = y 71 72 override fun getSecondaryValue(x: Float, y: Float): Float = x 73 74 override val isLayoutNaturalToLauncher: Boolean = false 75 76 override fun adjustFloatingIconStartVelocity(velocity: PointF) { 77 val oldX = velocity.x 78 val oldY = velocity.y 79 velocity.set(-oldY, oldX) 80 } 81 fixBoundsForHomeAnimStartRectnull82 override fun fixBoundsForHomeAnimStartRect(outStartRect: RectF, deviceProfile: DeviceProfile) { 83 // We don't need to check the "top" value here because the startRect is in the orientation 84 // of the app, not of the fixed portrait launcher. 85 if (outStartRect.left > deviceProfile.heightPx) { 86 outStartRect.offsetTo(0f, outStartRect.top) 87 } else if (outStartRect.left < -deviceProfile.heightPx) { 88 outStartRect.offsetTo(0f, outStartRect.top) 89 } 90 } 91 setPrimarynull92 override fun <T> setPrimary(target: T, action: Int2DAction<T>, param: Int) = 93 action.call(target, 0, param) 94 95 override fun <T> setPrimary(target: T, action: Float2DAction<T>, param: Float) = 96 action.call(target, 0f, param) 97 98 override fun <T> setSecondary(target: T, action: Float2DAction<T>, param: Float) = 99 action.call(target, param, 0f) 100 101 override fun <T> set( 102 target: T, 103 action: Int2DAction<T>, 104 primaryParam: Int, 105 secondaryParam: Int 106 ) = action.call(target, secondaryParam, primaryParam) 107 108 override fun getPrimaryDirection(event: MotionEvent, pointerIndex: Int): Float = 109 event.getY(pointerIndex) 110 111 override fun getPrimaryVelocity(velocityTracker: VelocityTracker, pointerId: Int): Float = 112 velocityTracker.getYVelocity(pointerId) 113 114 override fun getMeasuredSize(view: View): Int = view.measuredHeight 115 116 override fun getPrimarySize(view: View): Int = view.height 117 118 override fun getPrimarySize(rect: RectF): Float = rect.height() 119 120 override fun getStart(rect: RectF): Float = rect.top 121 122 override fun getEnd(rect: RectF): Float = rect.bottom 123 124 override fun rotateInsets(insets: Rect, outInsets: Rect) { 125 outInsets.set(insets.bottom, insets.left, insets.top, insets.right) 126 } 127 getClearAllSidePaddingnull128 override fun getClearAllSidePadding(view: View, isRtl: Boolean): Int = 129 if (isRtl) view.paddingBottom / 2 else -view.paddingTop / 2 130 131 override fun getSecondaryDimension(view: View): Int = view.width 132 133 override val primaryViewTranslate: FloatProperty<View> = LauncherAnimUtils.VIEW_TRANSLATE_Y 134 135 override val secondaryViewTranslate: FloatProperty<View> = LauncherAnimUtils.VIEW_TRANSLATE_X 136 137 override fun getPrimaryScroll(view: View): Int = view.scrollY 138 139 override fun getPrimaryScale(view: View): Float = view.scaleY 140 141 override fun setMaxScroll(event: AccessibilityEvent, maxScroll: Int) { 142 event.maxScrollY = maxScroll 143 } 144 getRecentsRtlSettingnull145 override fun getRecentsRtlSetting(resources: Resources): Boolean = !Utilities.isRtl(resources) 146 147 override val degreesRotated: Float = 90f 148 149 override val rotation: Int = Surface.ROTATION_90 150 151 override fun setPrimaryScale(view: View, scale: Float) { 152 view.scaleY = scale 153 } 154 setSecondaryScalenull155 override fun setSecondaryScale(view: View, scale: Float) { 156 view.scaleX = scale 157 } 158 getChildStartnull159 override fun getChildStart(view: View): Int = view.top 160 161 override fun getCenterForPage(view: View, insets: Rect): Int = 162 (view.paddingLeft + view.measuredWidth + insets.left - insets.right - view.paddingRight) / 2 163 164 override fun getScrollOffsetStart(view: View, insets: Rect): Int = insets.top + view.paddingTop 165 166 override fun getScrollOffsetEnd(view: View, insets: Rect): Int = 167 view.height - view.paddingBottom - insets.bottom 168 169 override val secondaryTranslationDirectionFactor: Int = 1 170 171 override fun getSplitTranslationDirectionFactor( 172 stagePosition: Int, 173 deviceProfile: DeviceProfile 174 ): Int = if (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) -1 else 1 175 176 override fun getTaskMenuX( 177 x: Float, 178 thumbnailView: View, 179 deviceProfile: DeviceProfile, 180 taskInsetMargin: Float, 181 taskViewIcon: View 182 ): Float = thumbnailView.measuredWidth + x - taskInsetMargin 183 184 override fun getTaskMenuY( 185 y: Float, 186 thumbnailView: View, 187 stagePosition: Int, 188 taskMenuView: View, 189 taskInsetMargin: Float, 190 taskViewIcon: View 191 ): Float { 192 val layoutParams = taskMenuView.layoutParams as BaseDragLayer.LayoutParams 193 var taskMenuY = y + taskInsetMargin 194 195 if (stagePosition == STAGE_POSITION_UNDEFINED) { 196 taskMenuY += (thumbnailView.measuredHeight - layoutParams.width) / 2f 197 } 198 199 return taskMenuY 200 } 201 getTaskMenuWidthnull202 override fun getTaskMenuWidth( 203 thumbnailView: View, 204 deviceProfile: DeviceProfile, 205 @StagePosition stagePosition: Int 206 ): Int = 207 when { 208 Flags.enableOverviewIconMenu() -> 209 thumbnailView.resources.getDimensionPixelSize( 210 R.dimen.task_thumbnail_icon_menu_expanded_width 211 ) 212 stagePosition == STAGE_POSITION_UNDEFINED -> thumbnailView.measuredWidth 213 else -> thumbnailView.measuredHeight 214 } 215 getTaskMenuHeightnull216 override fun getTaskMenuHeight( 217 taskInsetMargin: Float, 218 deviceProfile: DeviceProfile, 219 taskMenuX: Float, 220 taskMenuY: Float 221 ): Int = (taskMenuX - taskInsetMargin).toInt() 222 223 override fun setTaskOptionsMenuLayoutOrientation( 224 deviceProfile: DeviceProfile, 225 taskMenuLayout: LinearLayout, 226 dividerSpacing: Int, 227 dividerDrawable: ShapeDrawable 228 ) { 229 taskMenuLayout.orientation = LinearLayout.VERTICAL 230 dividerDrawable.intrinsicHeight = dividerSpacing 231 taskMenuLayout.dividerDrawable = dividerDrawable 232 } 233 setLayoutParamsForTaskMenuOptionItemnull234 override fun setLayoutParamsForTaskMenuOptionItem( 235 lp: LinearLayout.LayoutParams, 236 viewGroup: LinearLayout, 237 deviceProfile: DeviceProfile 238 ) { 239 // Phone fake landscape 240 viewGroup.orientation = LinearLayout.HORIZONTAL 241 lp.width = ViewGroup.LayoutParams.MATCH_PARENT 242 lp.height = ViewGroup.LayoutParams.WRAP_CONTENT 243 } 244 getDwbLayoutTranslationsnull245 override fun getDwbLayoutTranslations( 246 taskViewWidth: Int, 247 taskViewHeight: Int, 248 splitBounds: SplitBounds?, 249 deviceProfile: DeviceProfile, 250 thumbnailViews: Array<View>, 251 desiredTaskId: Int, 252 banner: View 253 ): Pair<Float, Float> { 254 val snapshotParams = thumbnailViews[0].layoutParams as FrameLayout.LayoutParams 255 val isRtl = banner.layoutDirection == View.LAYOUT_DIRECTION_RTL 256 val translationX = banner.height.toFloat() 257 258 val bannerParams = banner.layoutParams as FrameLayout.LayoutParams 259 bannerParams.gravity = Gravity.TOP or if (isRtl) Gravity.END else Gravity.START 260 banner.pivotX = 0f 261 banner.pivotY = 0f 262 banner.rotation = degreesRotated 263 264 if (splitBounds == null) { 265 // Single, fullscreen case 266 bannerParams.width = taskViewHeight - snapshotParams.topMargin 267 return Pair(translationX, snapshotParams.topMargin.toFloat()) 268 } 269 270 // Set correct width and translations 271 val translationY: Float 272 if (desiredTaskId == splitBounds.leftTopTaskId) { 273 bannerParams.width = thumbnailViews[0].measuredHeight 274 translationY = snapshotParams.topMargin.toFloat() 275 } else { 276 bannerParams.width = thumbnailViews[1].measuredHeight 277 val topLeftTaskPlusDividerPercent = 278 if (splitBounds.appsStackedVertically) { 279 splitBounds.topTaskPercent + splitBounds.dividerHeightPercent 280 } else { 281 splitBounds.leftTaskPercent + splitBounds.dividerWidthPercent 282 } 283 translationY = 284 snapshotParams.topMargin + 285 (taskViewHeight - snapshotParams.topMargin) * topLeftTaskPlusDividerPercent 286 } 287 288 return Pair(translationX, translationY) 289 } 290 291 /* ---------- The following are only used by TaskViewTouchHandler. ---------- */ 292 override val upDownSwipeDirection: SingleAxisSwipeDetector.Direction = 293 SingleAxisSwipeDetector.HORIZONTAL 294 getUpDirectionnull295 override fun getUpDirection(isRtl: Boolean): Int = 296 if (isRtl) SingleAxisSwipeDetector.DIRECTION_NEGATIVE 297 else SingleAxisSwipeDetector.DIRECTION_POSITIVE 298 299 override fun isGoingUp(displacement: Float, isRtl: Boolean): Boolean = 300 if (isRtl) displacement < 0 else displacement > 0 301 302 override fun getTaskDragDisplacementFactor(isRtl: Boolean): Int = if (isRtl) 1 else -1 303 /* -------------------- */ 304 305 override fun getChildBounds( 306 child: View, 307 childStart: Int, 308 pageCenter: Int, 309 layoutChild: Boolean 310 ): ChildBounds { 311 val childHeight = child.measuredHeight 312 val childWidth = child.measuredWidth 313 val childBottom = childStart + childHeight 314 val childLeft = pageCenter - childWidth / 2 315 if (layoutChild) { 316 child.layout(childLeft, childStart, childLeft + childWidth, childBottom) 317 } 318 return ChildBounds(childHeight, childWidth, childBottom, childLeft) 319 } 320 getDistanceToBottomOfRectnull321 override fun getDistanceToBottomOfRect(dp: DeviceProfile, rect: Rect): Int = rect.left 322 323 override fun getSplitPositionOptions(dp: DeviceProfile): List<SplitPositionOption> = 324 // Add "left" side of phone which is actually the top 325 listOf( 326 SplitPositionOption( 327 R.drawable.ic_split_horizontal, 328 R.string.recent_task_option_split_screen, 329 STAGE_POSITION_TOP_OR_LEFT, 330 STAGE_TYPE_MAIN 331 ) 332 ) 333 334 override fun getInitialSplitPlaceholderBounds( 335 placeholderHeight: Int, 336 placeholderInset: Int, 337 dp: DeviceProfile, 338 @StagePosition stagePosition: Int, 339 out: Rect 340 ) { 341 // In fake land/seascape, the placeholder always needs to go to the "top" of the device, 342 // which is the same bounds as 0 rotation. 343 val width = dp.widthPx 344 val insetSizeAdjustment = getPlaceholderSizeAdjustment(dp) 345 out.set(0, 0, width, placeholderHeight + insetSizeAdjustment) 346 out.inset(placeholderInset, 0) 347 348 // Adjust the top to account for content off screen. This will help to animate the view in 349 // with rounded corners. 350 val screenWidth = dp.widthPx 351 val screenHeight = dp.heightPx 352 val totalHeight = 353 (1.0f * screenHeight / 2 * (screenWidth - 2 * placeholderInset) / screenWidth).toInt() 354 out.top -= totalHeight - placeholderHeight 355 } 356 updateSplitIconParamsnull357 override fun updateSplitIconParams( 358 out: View, 359 onScreenRectCenterX: Float, 360 onScreenRectCenterY: Float, 361 fullscreenScaleX: Float, 362 fullscreenScaleY: Float, 363 drawableWidth: Int, 364 drawableHeight: Int, 365 dp: DeviceProfile, 366 @StagePosition stagePosition: Int 367 ) { 368 val insetAdjustment = getPlaceholderSizeAdjustment(dp) / 2f 369 out.x = (onScreenRectCenterX / fullscreenScaleX - 1.0f * drawableWidth / 2) 370 out.y = 371 ((onScreenRectCenterY + insetAdjustment) / fullscreenScaleY - 1.0f * drawableHeight / 2) 372 } 373 374 /** 375 * The split placeholder comes with a default inset to buffer the icon from the top of the 376 * screen. But if the device already has a large inset (from cutouts etc), use that instead. 377 */ getPlaceholderSizeAdjustmentnull378 private fun getPlaceholderSizeAdjustment(dp: DeviceProfile?): Int = 379 max((dp!!.insets.top - dp.splitPlaceholderInset).toDouble(), 0.0).toInt() 380 381 override fun setSplitInstructionsParams( 382 out: View, 383 dp: DeviceProfile, 384 splitInstructionsHeight: Int, 385 splitInstructionsWidth: Int 386 ) { 387 out.pivotX = 0f 388 out.pivotY = splitInstructionsHeight.toFloat() 389 out.rotation = degreesRotated 390 val distanceToEdge = 391 out.resources.getDimensionPixelSize( 392 R.dimen.split_instructions_bottom_margin_phone_landscape 393 ) 394 // Adjust for any insets on the left edge 395 val insetCorrectionX = dp.insets.left 396 // Center the view in case of unbalanced insets on top or bottom of screen 397 val insetCorrectionY = (dp.insets.bottom - dp.insets.top) / 2 398 out.translationX = (distanceToEdge - insetCorrectionX).toFloat() 399 out.translationY = 400 (-splitInstructionsHeight - splitInstructionsWidth) / 2f + insetCorrectionY 401 // Setting gravity to LEFT instead of the lint-recommended START because we always want this 402 // view to be screen-left when phone is in landscape, regardless of the RtL setting. 403 val lp = out.layoutParams as FrameLayout.LayoutParams 404 lp.gravity = Gravity.LEFT or Gravity.CENTER_VERTICAL 405 out.layoutParams = lp 406 } 407 getFinalSplitPlaceholderBoundsnull408 override fun getFinalSplitPlaceholderBounds( 409 splitDividerSize: Int, 410 dp: DeviceProfile, 411 @StagePosition stagePosition: Int, 412 out1: Rect, 413 out2: Rect 414 ) { 415 // In fake land/seascape, the window bounds are always top and bottom half 416 val screenHeight = dp.heightPx 417 val screenWidth = dp.widthPx 418 out1.set(0, 0, screenWidth, screenHeight / 2 - splitDividerSize) 419 out2.set(0, screenHeight / 2 + splitDividerSize, screenWidth, screenHeight) 420 } 421 setSplitTaskSwipeRectnull422 override fun setSplitTaskSwipeRect( 423 dp: DeviceProfile, 424 outRect: Rect, 425 splitInfo: SplitBounds, 426 desiredStagePosition: Int 427 ) { 428 val topLeftTaskPercent: Float 429 val dividerBarPercent: Float 430 if (splitInfo.appsStackedVertically) { 431 topLeftTaskPercent = splitInfo.topTaskPercent 432 dividerBarPercent = splitInfo.dividerHeightPercent 433 } else { 434 topLeftTaskPercent = splitInfo.leftTaskPercent 435 dividerBarPercent = splitInfo.dividerWidthPercent 436 } 437 438 if (desiredStagePosition == STAGE_POSITION_TOP_OR_LEFT) { 439 outRect.bottom = outRect.top + (outRect.height() * topLeftTaskPercent).toInt() 440 } else { 441 outRect.top += (outRect.height() * (topLeftTaskPercent + dividerBarPercent)).toInt() 442 } 443 } 444 measureGroupedTaskViewThumbnailBoundsnull445 override fun measureGroupedTaskViewThumbnailBounds( 446 primarySnapshot: View, 447 secondarySnapshot: View, 448 parentWidth: Int, 449 parentHeight: Int, 450 splitBoundsConfig: SplitBounds, 451 dp: DeviceProfile, 452 isRtl: Boolean 453 ) { 454 val primaryParams = primarySnapshot.layoutParams as FrameLayout.LayoutParams 455 val secondaryParams = secondarySnapshot.layoutParams as FrameLayout.LayoutParams 456 457 // Swap the margins that are set in TaskView#setRecentsOrientedState() 458 secondaryParams.topMargin = dp.overviewTaskThumbnailTopMarginPx 459 primaryParams.topMargin = 0 460 461 // Measure and layout the thumbnails bottom up, since the primary is on the visual left 462 // (portrait bottom) and secondary is on the right (portrait top) 463 val spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx 464 val totalThumbnailHeight = parentHeight - spaceAboveSnapshot 465 val dividerBar = getDividerBarSize(totalThumbnailHeight, splitBoundsConfig) 466 467 val (taskViewFirst, taskViewSecond) = 468 getGroupedTaskViewSizes(dp, splitBoundsConfig, parentWidth, parentHeight) 469 470 primarySnapshot.translationY = spaceAboveSnapshot.toFloat() 471 primarySnapshot.measure( 472 MeasureSpec.makeMeasureSpec(taskViewFirst.x, MeasureSpec.EXACTLY), 473 MeasureSpec.makeMeasureSpec(taskViewFirst.y, MeasureSpec.EXACTLY) 474 ) 475 val translationY = taskViewFirst.y + spaceAboveSnapshot + dividerBar 476 secondarySnapshot.translationY = (translationY - spaceAboveSnapshot).toFloat() 477 secondarySnapshot.measure( 478 MeasureSpec.makeMeasureSpec(taskViewSecond.x, MeasureSpec.EXACTLY), 479 MeasureSpec.makeMeasureSpec(taskViewSecond.y, MeasureSpec.EXACTLY) 480 ) 481 } 482 getGroupedTaskViewSizesnull483 override fun getGroupedTaskViewSizes( 484 dp: DeviceProfile, 485 splitBoundsConfig: SplitBounds, 486 parentWidth: Int, 487 parentHeight: Int 488 ): Pair<Point, Point> { 489 val spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx 490 val totalThumbnailHeight = parentHeight - spaceAboveSnapshot 491 val dividerBar = getDividerBarSize(totalThumbnailHeight, splitBoundsConfig) 492 493 val taskPercent = 494 if (splitBoundsConfig.appsStackedVertically) { 495 splitBoundsConfig.topTaskPercent 496 } else { 497 splitBoundsConfig.leftTaskPercent 498 } 499 val firstTaskViewSize = Point(parentWidth, (totalThumbnailHeight * taskPercent).toInt()) 500 val secondTaskViewSize = 501 Point(parentWidth, totalThumbnailHeight - firstTaskViewSize.y - dividerBar) 502 return Pair(firstTaskViewSize, secondTaskViewSize) 503 } 504 setTaskIconParamsnull505 override fun setTaskIconParams( 506 iconParams: FrameLayout.LayoutParams, 507 taskIconMargin: Int, 508 taskIconHeight: Int, 509 thumbnailTopMargin: Int, 510 isRtl: Boolean 511 ) { 512 iconParams.gravity = 513 if (isRtl) { 514 Gravity.START or Gravity.CENTER_VERTICAL 515 } else { 516 Gravity.END or Gravity.CENTER_VERTICAL 517 } 518 iconParams.rightMargin = -taskIconHeight - taskIconMargin / 2 519 iconParams.leftMargin = 0 520 iconParams.topMargin = thumbnailTopMargin / 2 521 iconParams.bottomMargin = 0 522 } 523 setIconAppChipChildrenParamsnull524 override fun setIconAppChipChildrenParams( 525 iconParams: FrameLayout.LayoutParams, 526 chipChildMarginStart: Int 527 ) { 528 iconParams.gravity = Gravity.START or Gravity.CENTER_VERTICAL 529 iconParams.marginStart = chipChildMarginStart 530 iconParams.topMargin = 0 531 } 532 setIconAppChipMenuParamsnull533 override fun setIconAppChipMenuParams( 534 iconAppChipView: IconAppChipView, 535 iconMenuParams: FrameLayout.LayoutParams, 536 iconMenuMargin: Int, 537 thumbnailTopMargin: Int 538 ) { 539 val isRtl = iconAppChipView.layoutDirection == View.LAYOUT_DIRECTION_RTL 540 541 if (isRtl) { 542 iconMenuParams.gravity = Gravity.START or Gravity.BOTTOM 543 iconMenuParams.marginStart = iconMenuMargin 544 iconMenuParams.bottomMargin = iconMenuMargin 545 iconAppChipView.pivotX = iconMenuParams.width - iconMenuParams.height / 2f 546 iconAppChipView.pivotY = iconMenuParams.height / 2f 547 } else { 548 iconMenuParams.gravity = Gravity.END or Gravity.TOP 549 iconMenuParams.marginStart = 0 550 iconMenuParams.bottomMargin = 0 551 iconAppChipView.pivotX = iconMenuParams.width / 2f 552 iconAppChipView.pivotY = iconMenuParams.width / 2f 553 } 554 555 iconMenuParams.topMargin = iconMenuMargin 556 iconMenuParams.marginEnd = iconMenuMargin 557 iconAppChipView.setSplitTranslationY(0f) 558 iconAppChipView.setRotation(degreesRotated) 559 } 560 setSplitIconParamsnull561 override fun setSplitIconParams( 562 primaryIconView: View, 563 secondaryIconView: View, 564 taskIconHeight: Int, 565 primarySnapshotWidth: Int, 566 primarySnapshotHeight: Int, 567 groupedTaskViewHeight: Int, 568 groupedTaskViewWidth: Int, 569 isRtl: Boolean, 570 deviceProfile: DeviceProfile, 571 splitConfig: SplitBounds 572 ) { 573 val spaceAboveSnapshot = deviceProfile.overviewTaskThumbnailTopMarginPx 574 val totalThumbnailHeight = groupedTaskViewHeight - spaceAboveSnapshot 575 val dividerBar: Int = getDividerBarSize(totalThumbnailHeight, splitConfig) 576 577 val (topLeftY, bottomRightY) = 578 getSplitIconsPosition( 579 taskIconHeight, 580 primarySnapshotHeight, 581 totalThumbnailHeight, 582 isRtl, 583 deviceProfile.overviewTaskMarginPx, 584 dividerBar 585 ) 586 587 updateSplitIconsPosition(primaryIconView, topLeftY, isRtl) 588 updateSplitIconsPosition(secondaryIconView, bottomRightY, isRtl) 589 } 590 getDefaultSplitPositionnull591 override fun getDefaultSplitPosition(deviceProfile: DeviceProfile): Int { 592 throw IllegalStateException("Default position not available in fake landscape") 593 } 594 getSplitSelectTaskOffsetnull595 override fun <T> getSplitSelectTaskOffset( 596 primary: FloatProperty<T>, 597 secondary: FloatProperty<T>, 598 deviceProfile: DeviceProfile 599 ): Pair<FloatProperty<T>, FloatProperty<T>> = Pair(primary, secondary) 600 601 override fun getFloatingTaskOffscreenTranslationTarget( 602 floatingTask: View, 603 onScreenRect: RectF, 604 @StagePosition stagePosition: Int, 605 dp: DeviceProfile 606 ): Float = floatingTask.translationY - onScreenRect.height() 607 608 override fun setFloatingTaskPrimaryTranslation( 609 floatingTask: View, 610 translation: Float, 611 dp: DeviceProfile 612 ) { 613 floatingTask.translationY = translation 614 } 615 getFloatingTaskPrimaryTranslationnull616 override fun getFloatingTaskPrimaryTranslation(floatingTask: View, dp: DeviceProfile): Float = 617 floatingTask.translationY 618 619 override fun getHandlerTypeForLogging(): TaskSwitcherContainer.OrientationHandler = 620 TaskSwitcherContainer.OrientationHandler.LANDSCAPE 621 622 /** 623 * Retrieves split icons position 624 * 625 * @param taskIconHeight The height of the task icon. 626 * @param primarySnapshotHeight The height for the primary snapshot (i.e., top-left snapshot). 627 * @param totalThumbnailHeight The total height for the group task view. 628 * @param isRtl Whether the layout direction is RTL (or false for LTR). 629 * @param overviewTaskMarginPx The space under the focused task icon provided by Device Profile. 630 * @param dividerSize The size of the divider for the group task view. 631 * @return The top-left and right-bottom positions for the icon views. 632 */ 633 @VisibleForTesting 634 open fun getSplitIconsPosition( 635 taskIconHeight: Int, 636 primarySnapshotHeight: Int, 637 totalThumbnailHeight: Int, 638 isRtl: Boolean, 639 overviewTaskMarginPx: Int, 640 dividerSize: Int, 641 ): SplitIconPositions { 642 return if (Flags.enableOverviewIconMenu()) { 643 if (isRtl) { 644 SplitIconPositions(0, -(totalThumbnailHeight - primarySnapshotHeight)) 645 } else { 646 SplitIconPositions(0, primarySnapshotHeight + dividerSize) 647 } 648 } else { 649 val topLeftY = primarySnapshotHeight + overviewTaskMarginPx 650 SplitIconPositions( 651 topLeftY = topLeftY, 652 bottomRightY = topLeftY + dividerSize + taskIconHeight 653 ) 654 } 655 } 656 657 /** 658 * Updates icon view gravity and translation for split tasks 659 * 660 * @param iconView View to be updated 661 * @param translationY the translationY that should be applied 662 * @param isRtl Whether the layout direction is RTL (or false for LTR). 663 */ 664 @SuppressLint("RtlHardcoded") 665 @VisibleForTesting updateSplitIconsPositionnull666 open fun updateSplitIconsPosition(iconView: View, translationY: Int, isRtl: Boolean) { 667 val layoutParams = iconView.layoutParams as FrameLayout.LayoutParams 668 669 if (Flags.enableOverviewIconMenu()) { 670 val appChipView = iconView as IconAppChipView 671 layoutParams.gravity = 672 if (isRtl) Gravity.BOTTOM or Gravity.START else Gravity.TOP or Gravity.END 673 appChipView.layoutParams = layoutParams 674 appChipView.setSplitTranslationX(0f) 675 appChipView.setSplitTranslationY(translationY.toFloat()) 676 } else { 677 layoutParams.gravity = Gravity.TOP or Gravity.RIGHT 678 layoutParams.topMargin = translationY 679 iconView.translationX = 0f 680 iconView.translationY = 0f 681 iconView.layoutParams = layoutParams 682 } 683 } 684 685 /** 686 * It calculates the divider's size in the group task view. 687 * 688 * @param totalThumbnailHeight The total height for the group task view 689 * @param splitConfig Contains information about sizes and proportions for split task. 690 * @return The divider size for the group task view. 691 */ getDividerBarSizenull692 protected fun getDividerBarSize(totalThumbnailHeight: Int, splitConfig: SplitBounds): Int { 693 return Math.round( 694 totalThumbnailHeight * 695 if (splitConfig.appsStackedVertically) splitConfig.dividerHeightPercent 696 else splitConfig.dividerWidthPercent 697 ) 698 } 699 700 /** 701 * Data structure to keep the y position to be used for the split task icon views translation. 702 * 703 * @param topLeftY The y-axis position for the task view position on the Top or Left side. 704 * @param bottomRightY The y-axis position for the task view position on the Bottom or Right 705 * side. 706 */ 707 data class SplitIconPositions(val topLeftY: Int, val bottomRightY: Int) 708 } 709