Files
Lawnchair/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
T
Jeremy Sim 633b94abf1 Fix icon positioning for flexible split in Overview
In flexible split ratios like 90:10 and 10:90, we hide the Overview icon of the smaller app completely. This resulted in a bug where the single remaining visible icon was positioned badly.

Fixed this by adding a check in updateIconPlacement() so that we can skip the two-icon positioning and center the visible icon alone. Also fixed the cases where split select is initiated on a split tile (from Taskbar). Now the correct icon should be shown and centered.

Fixes: 391865942
Flag: com.android.wm.shell.enable_flexible_two_app_split
Test: Single icon is positioned correctly in the middle. When split select is initiated, the right icon is shown and centered. When app chip menus are enabled, the (existing) correct behavior is not changed.
Change-Id: I79842222fc325a7661cbabbb54d277389a317504
2025-02-24 11:48:50 -08:00

423 lines
17 KiB
Kotlin

/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.quickstep.orientation
import android.annotation.SuppressLint
import android.content.res.Resources
import android.graphics.Point
import android.graphics.PointF
import android.graphics.Rect
import android.util.Pair
import android.view.Gravity
import android.view.Surface
import android.view.View
import android.view.View.MeasureSpec
import android.widget.FrameLayout
import androidx.core.util.component1
import androidx.core.util.component2
import androidx.core.view.updateLayoutParams
import com.android.launcher3.DeviceProfile
import com.android.launcher3.Flags
import com.android.launcher3.R
import com.android.launcher3.Utilities
import com.android.launcher3.logger.LauncherAtom
import com.android.launcher3.touch.SingleAxisSwipeDetector
import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT
import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT
import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED
import com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN
import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds
import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption
import com.android.launcher3.views.BaseDragLayer
import com.android.quickstep.views.IconAppChipView
class SeascapePagedViewHandler : LandscapePagedViewHandler() {
override fun rotateInsets(insets: Rect, outInsets: Rect) {
outInsets.set(insets.top, insets.right, insets.bottom, insets.left)
}
override val secondaryTranslationDirectionFactor: Int = -1
override fun getSplitTranslationDirectionFactor(
stagePosition: Int,
deviceProfile: DeviceProfile
): Int = if (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) -1 else 1
override fun getRecentsRtlSetting(resources: Resources): Boolean = Utilities.isRtl(resources)
override val degreesRotated: Float = 270f
override val rotation: Int = Surface.ROTATION_270
override fun adjustFloatingIconStartVelocity(velocity: PointF) =
velocity.set(velocity.y, -velocity.x)
override fun getTaskMenuX(
x: Float,
thumbnailView: View,
deviceProfile: DeviceProfile,
taskInsetMargin: Float,
taskViewIcon: View
): Float = x + taskInsetMargin
override fun getTaskMenuY(
y: Float,
thumbnailView: View,
stagePosition: Int,
taskMenuView: View,
taskInsetMargin: Float,
taskViewIcon: View
): Float {
if (Flags.enableOverviewIconMenu()) {
return y
}
val lp = taskMenuView.layoutParams as BaseDragLayer.LayoutParams
val taskMenuWidth = lp.width
return if (stagePosition == STAGE_POSITION_UNDEFINED) {
y + taskInsetMargin + (thumbnailView.measuredHeight + taskMenuWidth) / 2f
} else {
y + taskMenuWidth + taskInsetMargin
}
}
override fun getTaskMenuHeight(
taskInsetMargin: Float,
deviceProfile: DeviceProfile,
taskMenuX: Float,
taskMenuY: Float
): Int = (deviceProfile.availableWidthPx - taskInsetMargin - taskMenuX).toInt()
override fun setSplitTaskSwipeRect(
dp: DeviceProfile,
outRect: Rect,
splitInfo: SplitBounds,
desiredStagePosition: Int
) {
val topLeftTaskPercent = splitInfo.leftTopTaskPercent
val dividerBarPercent = splitInfo.dividerPercent
// In seascape, the primary thumbnail is counterintuitively placed at the physical bottom of
// the screen. This is to preserve consistency when the user rotates: From the user's POV,
// the primary should always be on the left.
if (desiredStagePosition == STAGE_POSITION_TOP_OR_LEFT) {
outRect.top += (outRect.height() * (1 - topLeftTaskPercent)).toInt()
} else {
outRect.bottom -= (outRect.height() * (topLeftTaskPercent + dividerBarPercent)).toInt()
}
}
override fun updateDwbBannerLayout(
taskViewWidth: Int,
taskViewHeight: Int,
isGroupedTaskView: Boolean,
deviceProfile: DeviceProfile,
snapshotViewWidth: Int,
snapshotViewHeight: Int,
banner: View
) {
banner.pivotX = 0f
banner.pivotY = 0f
banner.rotation = degreesRotated
banner.updateLayoutParams<FrameLayout.LayoutParams> {
gravity = Gravity.BOTTOM or if (banner.isLayoutRtl) Gravity.END else Gravity.START
width =
if (isGroupedTaskView) {
snapshotViewHeight
} else {
taskViewHeight - deviceProfile.overviewTaskThumbnailTopMarginPx
}
}
}
override fun getDwbBannerTranslations(
taskViewWidth: Int,
taskViewHeight: Int,
splitBounds: SplitBounds?,
deviceProfile: DeviceProfile,
thumbnailViews: Array<View>,
desiredTaskId: Int,
banner: View
): Pair<Float, Float> {
val snapshotParams = thumbnailViews[0].layoutParams as FrameLayout.LayoutParams
val translationX: Float = (taskViewWidth - banner.height).toFloat()
val translationY: Float
if (splitBounds == null) {
translationY = banner.height.toFloat()
} else {
if (desiredTaskId == splitBounds.leftTopTaskId) {
val bottomRightTaskPlusDividerPercent =
splitBounds.rightBottomTaskPercent + splitBounds.dividerPercent
translationY =
banner.height -
(taskViewHeight - snapshotParams.topMargin) *
bottomRightTaskPlusDividerPercent
} else {
translationY = banner.height.toFloat()
}
}
return Pair(translationX, translationY)
}
override fun getDistanceToBottomOfRect(dp: DeviceProfile, rect: Rect): Int =
dp.widthPx - rect.right
override fun getSplitPositionOptions(dp: DeviceProfile): List<SplitPositionOption> =
// Add "right" option which is actually the top
listOf(
SplitPositionOption(
R.drawable.ic_split_horizontal,
R.string.recent_task_option_split_screen,
STAGE_POSITION_BOTTOM_OR_RIGHT,
STAGE_TYPE_MAIN
)
)
override fun setSplitInstructionsParams(
out: View,
dp: DeviceProfile,
splitInstructionsHeight: Int,
splitInstructionsWidth: Int
) {
out.pivotX = 0f
out.pivotY = splitInstructionsHeight.toFloat()
out.rotation = degreesRotated
val distanceToEdge =
out.resources.getDimensionPixelSize(
R.dimen.split_instructions_bottom_margin_phone_landscape
)
// Adjust for any insets on the right edge
val insetCorrectionX = dp.insets.right
// Center the view in case of unbalanced insets on top or bottom of screen
val insetCorrectionY = (dp.insets.bottom - dp.insets.top) / 2
out.translationX = (splitInstructionsWidth - distanceToEdge + insetCorrectionX).toFloat()
out.translationY =
(-splitInstructionsHeight + splitInstructionsWidth) / 2f + insetCorrectionY
// Setting gravity to RIGHT instead of the lint-recommended END because we always want this
// view to be screen-right when phone is in seascape, regardless of the RtL setting.
val lp = out.layoutParams as FrameLayout.LayoutParams
lp.gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL
out.layoutParams = lp
}
override fun setTaskIconParams(
iconParams: FrameLayout.LayoutParams,
taskIconMargin: Int,
taskIconHeight: Int,
thumbnailTopMargin: Int,
isRtl: Boolean
) {
iconParams.gravity =
if (isRtl) {
Gravity.END or Gravity.CENTER_VERTICAL
} else {
Gravity.START or Gravity.CENTER_VERTICAL
}
iconParams.setMargins(-taskIconHeight - taskIconMargin / 2, thumbnailTopMargin / 2, 0, 0)
}
override fun setIconAppChipChildrenParams(
iconParams: FrameLayout.LayoutParams,
chipChildMarginStart: Int
) {
iconParams.setMargins(0, 0, 0, 0)
iconParams.marginStart = chipChildMarginStart
iconParams.gravity = Gravity.START or Gravity.CENTER_VERTICAL
}
override fun setIconAppChipMenuParams(
iconAppChipView: IconAppChipView,
iconMenuParams: FrameLayout.LayoutParams,
iconMenuMargin: Int,
thumbnailTopMargin: Int
) {
val isRtl = iconAppChipView.layoutDirection == View.LAYOUT_DIRECTION_RTL
val iconCenter = iconAppChipView.getHeight() / 2f
if (isRtl) {
iconMenuParams.gravity = Gravity.TOP or Gravity.END
iconMenuParams.topMargin = iconMenuMargin
iconMenuParams.marginEnd = thumbnailTopMargin
// Use half menu height to place the pivot within the X/Y center of icon in the menu.
iconAppChipView.pivotX = iconMenuParams.width / 2f
iconAppChipView.pivotY = iconMenuParams.width / 2f
} else {
iconMenuParams.gravity = Gravity.BOTTOM or Gravity.START
iconMenuParams.topMargin = 0
iconMenuParams.marginEnd = 0
iconAppChipView.pivotX = iconCenter
iconAppChipView.pivotY = iconCenter - iconMenuMargin
}
iconMenuParams.marginStart = 0
iconMenuParams.bottomMargin = 0
iconAppChipView.setSplitTranslationY(0f)
iconAppChipView.setRotation(degreesRotated)
}
/**
* @param inSplitSelection Whether user currently has a task from this task group staged for
* split screen. Currently this state is not reachable in fake seascape.
*/
override fun measureGroupedTaskViewThumbnailBounds(
primarySnapshot: View,
secondarySnapshot: View,
parentWidth: Int,
parentHeight: Int,
splitBoundsConfig: SplitBounds,
dp: DeviceProfile,
isRtl: Boolean,
inSplitSelection: Boolean
) {
val primaryParams = primarySnapshot.layoutParams as FrameLayout.LayoutParams
val secondaryParams = secondarySnapshot.layoutParams as FrameLayout.LayoutParams
// Swap the margins that are set in TaskView#setRecentsOrientedState()
secondaryParams.topMargin = dp.overviewTaskThumbnailTopMarginPx
primaryParams.topMargin = 0
// Measure and layout the thumbnails bottom up, since the primary is on the visual left
// (portrait bottom) and secondary is on the right (portrait top)
val spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx
val totalThumbnailHeight = parentHeight - spaceAboveSnapshot
val dividerBar = getDividerBarSize(totalThumbnailHeight, splitBoundsConfig)
val (taskViewFirst, taskViewSecond) =
getGroupedTaskViewSizes(dp, splitBoundsConfig, parentWidth, parentHeight)
secondarySnapshot.translationY = 0f
primarySnapshot.translationY =
(taskViewSecond.y + spaceAboveSnapshot + dividerBar).toFloat()
primarySnapshot.measure(
MeasureSpec.makeMeasureSpec(taskViewFirst.x, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(taskViewFirst.y, MeasureSpec.EXACTLY)
)
secondarySnapshot.measure(
MeasureSpec.makeMeasureSpec(taskViewSecond.x, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(taskViewSecond.y, MeasureSpec.EXACTLY)
)
}
override fun getGroupedTaskViewSizes(
dp: DeviceProfile,
splitBoundsConfig: SplitBounds,
parentWidth: Int,
parentHeight: Int
): Pair<Point, Point> {
// Measure and layout the thumbnails bottom up, since the primary is on the visual left
// (portrait bottom) and secondary is on the right (portrait top)
val spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx
val totalThumbnailHeight = parentHeight - spaceAboveSnapshot
val dividerBar = getDividerBarSize(totalThumbnailHeight, splitBoundsConfig)
val taskPercent = splitBoundsConfig.leftTopTaskPercent
val firstTaskViewSize = Point(parentWidth, (totalThumbnailHeight * taskPercent).toInt())
val secondTaskViewSize =
Point(parentWidth, totalThumbnailHeight - firstTaskViewSize.y - dividerBar)
return Pair(firstTaskViewSize, secondTaskViewSize)
}
/* ---------- The following are only used by TaskViewTouchHandler. ---------- */
override val upDownSwipeDirection: SingleAxisSwipeDetector.Direction =
SingleAxisSwipeDetector.HORIZONTAL
override fun getUpDirection(isRtl: Boolean): Int =
if (isRtl) SingleAxisSwipeDetector.DIRECTION_POSITIVE
else SingleAxisSwipeDetector.DIRECTION_NEGATIVE
override fun getDownDirection(isRtl: Boolean): Int =
if (isRtl) SingleAxisSwipeDetector.DIRECTION_NEGATIVE
else SingleAxisSwipeDetector.DIRECTION_POSITIVE
override fun isGoingUp(displacement: Float, isRtl: Boolean): Boolean =
if (isRtl) displacement > 0 else displacement < 0
override fun getTaskDragDisplacementFactor(isRtl: Boolean): Int = if (isRtl) -1 else 1
/* -------------------- */
override fun getSplitIconsPosition(
taskIconHeight: Int,
primarySnapshotHeight: Int,
totalThumbnailHeight: Int,
isRtl: Boolean,
overviewTaskMarginPx: Int,
dividerSize: Int,
oneIconHiddenDueToSmallWidth: Boolean,
): SplitIconPositions {
return if (Flags.enableOverviewIconMenu()) {
if (isRtl) {
SplitIconPositions(
topLeftY = totalThumbnailHeight - primarySnapshotHeight,
bottomRightY = 0,
)
} else {
SplitIconPositions(
topLeftY = 0,
bottomRightY = -(primarySnapshotHeight + dividerSize),
)
}
} else {
// In seascape, the icons are initially placed at the bottom start of the
// display (portrait locked). The values defined here are used to translate the icons
// from the bottom to the almost-center of the screen using the bottom margin.
// The primary snapshot is placed at the bottom, thus we translate the icons using
// the size of the primary snapshot minus the icon size for the top-left icon.
if (oneIconHiddenDueToSmallWidth) {
// Center both icons
val centerY = primarySnapshotHeight + ((dividerSize - taskIconHeight) / 2)
SplitIconPositions(
topLeftY = centerY,
bottomRightY = centerY,
)
} else {
SplitIconPositions(
topLeftY = primarySnapshotHeight - taskIconHeight,
bottomRightY = primarySnapshotHeight + dividerSize,
)
}
}
}
/**
* Updates icon view gravity and translation for split tasks
*
* @param iconView View to be updated
* @param translationY the translationY that should be applied
* @param isRtl Whether the layout direction is RTL (or false for LTR).
*/
@SuppressLint("RtlHardcoded")
override fun updateSplitIconsPosition(iconView: View, translationY: Int, isRtl: Boolean) {
val layoutParams = iconView.layoutParams as FrameLayout.LayoutParams
if (Flags.enableOverviewIconMenu()) {
val appChipView = iconView as IconAppChipView
layoutParams.gravity =
if (isRtl) Gravity.TOP or Gravity.END else Gravity.BOTTOM or Gravity.START
appChipView.layoutParams = layoutParams
appChipView.setSplitTranslationX(0f)
appChipView.setSplitTranslationY(translationY.toFloat())
} else {
layoutParams.gravity = Gravity.BOTTOM or Gravity.LEFT
iconView.translationX = 0f
iconView.translationY = 0f
layoutParams.bottomMargin = translationY
iconView.layoutParams = layoutParams
}
}
@Override
override fun getHandlerTypeForLogging(): LauncherAtom.TaskSwitcherContainer.OrientationHandler =
LauncherAtom.TaskSwitcherContainer.OrientationHandler.SEASCAPE
}