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