blob: bc91911749bde649a8d09b2ba6aa0ba49b1b493d [file] [edit]
/*
* 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: Float
val dividerBarPercent: Float
if (splitInfo.appsStackedVertically) {
topLeftTaskPercent = splitInfo.topTaskPercent
dividerBarPercent = splitInfo.dividerHeightPercent
} else {
topLeftTaskPercent = splitInfo.leftTaskPercent
dividerBarPercent = splitInfo.dividerWidthPercent
}
// 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 =
if (splitBounds.appsStackedVertically) {
1f - splitBounds.topTaskPercent
} else {
1f - splitBounds.leftTaskPercent
}
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 =
if (splitBoundsConfig.appsStackedVertically) {
splitBoundsConfig.topTaskPercent
} else {
splitBoundsConfig.leftTaskPercent
}
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 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,
): 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.
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
}