633b94abf1
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
423 lines
17 KiB
Kotlin
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
|
|
}
|