Merge "desktop-exploded-view: Implement layout algorithm" into main
This commit is contained in:
committed by
Android (Google) Code Review
commit
126566a096
+272
-54
@@ -17,77 +17,295 @@
|
||||
package com.android.quickstep.recents.domain.usecase
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.util.Size
|
||||
import android.graphics.RectF
|
||||
import androidx.core.graphics.toRect
|
||||
import com.android.quickstep.recents.domain.model.DesktopTaskBoundsData
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.sqrt
|
||||
|
||||
/**
|
||||
* This usecase is responsible for organizing desktop windows in a non-overlapping way. Note: this
|
||||
* is currently a placeholder implementation.
|
||||
*/
|
||||
/** This usecase is responsible for organizing desktop windows in a non-overlapping way. */
|
||||
class OrganizeDesktopTasksUseCase {
|
||||
/**
|
||||
* Run to layout [taskBounds] within the screen [desktopBounds]. Layout is done in 2 stages:
|
||||
* 1. Optimal height is determined. In this stage height is bisected to find maximum height
|
||||
* which still allows all the windows to fit.
|
||||
* 2. Row widths are balanced. In this stage the available width is reduced until some windows
|
||||
* are no longer fitting or until the difference between the narrowest and the widest rows
|
||||
* starts growing. Overall this achieves the goals of maximum size for previews (or maximum
|
||||
* row height which is equivalent assuming fixed height), balanced rows and minimal wasted
|
||||
* space.
|
||||
*/
|
||||
fun run(
|
||||
desktopSize: Size,
|
||||
desktopBounds: Rect,
|
||||
taskBounds: List<DesktopTaskBoundsData>,
|
||||
): List<DesktopTaskBoundsData> {
|
||||
return getRects(desktopSize, taskBounds.size).zip(taskBounds) { rect, task ->
|
||||
shrinkRect(rect, 0.8f)
|
||||
DesktopTaskBoundsData(task.taskId, fitRect(task.bounds, rect))
|
||||
if (desktopBounds.isEmpty || taskBounds.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
// Filter out [taskBounds] with empty rects before calculating layout.
|
||||
val validTaskBounds = taskBounds.filterNot { it.bounds.isEmpty }
|
||||
|
||||
if (validTaskBounds.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val availableLayoutBounds = desktopBounds.getLayoutEffectiveBounds()
|
||||
val resultRects = findOptimalHeightAndBalancedWidth(availableLayoutBounds, validTaskBounds)
|
||||
|
||||
centerTaskWindows(
|
||||
availableLayoutBounds,
|
||||
resultRects.maxOf { it.bottom }.toInt(),
|
||||
resultRects,
|
||||
)
|
||||
|
||||
val result = mutableListOf<DesktopTaskBoundsData>()
|
||||
for (i in validTaskBounds.indices) {
|
||||
result.add(DesktopTaskBoundsData(validTaskBounds[i].taskId, resultRects[i].toRect()))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun shrinkRect(bounds: Rect, fraction: Float) {
|
||||
val xMargin = (bounds.width() * ((1.0f - fraction) / 2.0f)).toInt()
|
||||
val yMargin = (bounds.height() * ((1.0f - fraction) / 2.0f)).toInt()
|
||||
bounds.inset(xMargin, yMargin, xMargin, yMargin)
|
||||
}
|
||||
/** Calculates the effective bounds for layout by applying insets to the raw desktop bounds. */
|
||||
private fun Rect.getLayoutEffectiveBounds() =
|
||||
Rect(this).apply { inset(OVERVIEW_INSET_TOP_BOTTOM, OVERVIEW_INSET_LEFT_RIGHT) }
|
||||
|
||||
/** Generates `tasks` number of non-overlapping rects that fit into `desktopSize`. */
|
||||
private fun getRects(desktopSize: Size, tasks: Int): List<Rect> {
|
||||
val (xSlots, ySlots) =
|
||||
when (tasks) {
|
||||
2 -> Pair(2, 1)
|
||||
3,
|
||||
4 -> Pair(2, 2)
|
||||
5,
|
||||
6 -> Pair(3, 2)
|
||||
else -> {
|
||||
val sides = ceil(sqrt(tasks.toDouble())).toInt()
|
||||
Pair(sides, sides)
|
||||
/**
|
||||
* Determines the optimal height for task windows and balances the row widths to minimize wasted
|
||||
* space. Returns the bounds for each task window after layout.
|
||||
*/
|
||||
private fun findOptimalHeightAndBalancedWidth(
|
||||
availableLayoutBounds: Rect,
|
||||
validTaskBounds: List<DesktopTaskBoundsData>,
|
||||
): List<RectF> {
|
||||
// Right bound of the narrowest row.
|
||||
var minRight: Int
|
||||
// Right bound of the widest row.
|
||||
var maxRight: Int
|
||||
|
||||
// Keep track of the difference between the narrowest and the widest row.
|
||||
// Initially this is set to the worst it can ever be assuming the windows fit.
|
||||
var widthDiff = availableLayoutBounds.width()
|
||||
|
||||
// Initially allow the windows to occupy all available width. Shrink this available space
|
||||
// horizontally to find the breakdown into rows that achieves the minimal [widthDiff].
|
||||
var rightBound = availableLayoutBounds.right
|
||||
|
||||
// Determine the optimal height bisecting between [lowHeight] and [highHeight]. Once this
|
||||
// optimal height is known, [heightFixed] is set to `true` and the rows are balanced by
|
||||
// repeatedly squeezing the widest row to cause windows to overflow to the subsequent rows.
|
||||
var lowHeight = VERTICAL_SPACE_BETWEEN_TASKS
|
||||
var highHeight = maxOf(lowHeight, availableLayoutBounds.height() + 1)
|
||||
var optimalHeight = 0.5f * (lowHeight + highHeight)
|
||||
var heightFixed = false
|
||||
|
||||
// Repeatedly try to fit the windows [resultRects] within [rightBound]. If a maximum
|
||||
// [optimalHeight] is found such that all window [resultRects] fit, this fitting continues
|
||||
// while shrinking the [rightBound] in order to balance the rows. If the windows fit the
|
||||
// [rightBound] would have been decremented at least once so it needs to be incremented once
|
||||
// before getting out of this loop and one additional pass made to actually fit the
|
||||
// [resultRects]. If the [resultRects] cannot fit (e.g. there are too many windows) the
|
||||
// bisection will still finish and we might increment the [rightBound] one pixel extra
|
||||
// which is acceptable since there is an unused margin on the right.
|
||||
var makeLastAdjustment = false
|
||||
var resultRects: List<RectF>
|
||||
|
||||
while (true) {
|
||||
val fitWindowResult =
|
||||
fitWindowRectsInBounds(
|
||||
Rect(availableLayoutBounds).apply { right = rightBound },
|
||||
validTaskBounds,
|
||||
minOf(MAXIMUM_TASK_HEIGHT, optimalHeight.toInt()),
|
||||
)
|
||||
val allWindowsFit = fitWindowResult.allWindowsFit
|
||||
resultRects = fitWindowResult.calculatedBounds
|
||||
minRight = fitWindowResult.minRight
|
||||
maxRight = fitWindowResult.maxRight
|
||||
|
||||
if (heightFixed) {
|
||||
if (!allWindowsFit) {
|
||||
// Revert the previous change to [rightBound] and do one last pass.
|
||||
rightBound++
|
||||
makeLastAdjustment = true
|
||||
break
|
||||
}
|
||||
// Break if all the windows are zero-width at the current scale.
|
||||
if (maxRight <= availableLayoutBounds.left) {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
// Find the optimal row height bisecting between [lowHeight] and [highHeight].
|
||||
if (allWindowsFit) {
|
||||
lowHeight = optimalHeight.toInt()
|
||||
} else {
|
||||
highHeight = optimalHeight.toInt()
|
||||
}
|
||||
optimalHeight = 0.5f * (lowHeight + highHeight)
|
||||
// When height can no longer be improved, start balancing the rows.
|
||||
if (optimalHeight.toInt() == lowHeight) {
|
||||
heightFixed = true
|
||||
}
|
||||
}
|
||||
|
||||
// The width and height of one of the boxes.
|
||||
val boxWidth = desktopSize.width / xSlots
|
||||
val boxHeight = desktopSize.height / ySlots
|
||||
|
||||
return (0 until tasks).map {
|
||||
val x = it % xSlots
|
||||
val y = it / xSlots
|
||||
Rect(x * boxWidth, y * boxHeight, (x + 1) * boxWidth, (y + 1) * boxHeight)
|
||||
if (allWindowsFit && heightFixed) {
|
||||
if (maxRight - minRight <= widthDiff) {
|
||||
// Row alignment is getting better. Try to shrink the [rightBound] in order to
|
||||
// squeeze the widest row.
|
||||
rightBound = maxRight - 1
|
||||
widthDiff = maxRight - minRight
|
||||
} else {
|
||||
// Row alignment is getting worse.
|
||||
// Revert the previous change to [rightBound] and do one last pass.
|
||||
rightBound++
|
||||
makeLastAdjustment = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Once the windows no longer fit, the change to [rightBound] was reverted. Perform one last
|
||||
// pass to position the [resultRects].
|
||||
if (makeLastAdjustment) {
|
||||
val fitWindowResult =
|
||||
fitWindowRectsInBounds(
|
||||
Rect(availableLayoutBounds).apply { right = rightBound },
|
||||
validTaskBounds,
|
||||
minOf(MAXIMUM_TASK_HEIGHT, optimalHeight.toInt()),
|
||||
)
|
||||
resultRects = fitWindowResult.calculatedBounds
|
||||
}
|
||||
|
||||
return resultRects
|
||||
}
|
||||
|
||||
/** Centers and fits `rect` into `bounds`, while preserving the former's aspect ratio. */
|
||||
private fun fitRect(rect: Rect, bounds: Rect): Rect {
|
||||
val boundsAspect = bounds.width().toFloat() / bounds.height()
|
||||
val rectAspect = rect.width().toFloat() / rect.height()
|
||||
/**
|
||||
* Data structure to hold the returned result of [fitWindowRectsInBounds] function.
|
||||
* [allWindowsFit] specifies whether all windows can be fit into the provided layout bounds.
|
||||
* [calculatedBounds] specifies the output bounds for all provided task windows. [minRight]
|
||||
* specifies the right bound of the narrowest row. [maxRight] specifies the right bound of the
|
||||
* widest rows.
|
||||
*/
|
||||
data class FitWindowResult(
|
||||
val allWindowsFit: Boolean,
|
||||
val calculatedBounds: List<RectF>,
|
||||
val minRight: Int,
|
||||
val maxRight: Int,
|
||||
)
|
||||
|
||||
if (rectAspect > boundsAspect) {
|
||||
// The width is the limiting dimension.
|
||||
val scale = bounds.width().toFloat() / rect.width()
|
||||
val width = bounds.width()
|
||||
val height = (rect.height() * scale).toInt()
|
||||
val top = (bounds.top + bounds.height() / 2.0f - height / 2.0f).toInt()
|
||||
return Rect(bounds.left, top, bounds.left + width, top + height)
|
||||
} else {
|
||||
// The height is the limiting dimension.
|
||||
val scale = bounds.height().toFloat() / rect.height()
|
||||
val width = (rect.width() * scale).toInt()
|
||||
val height = bounds.height()
|
||||
val left = (bounds.left + bounds.width() / 2.0f - width / 2.0f).toInt()
|
||||
return Rect(left, bounds.top, left + width, bounds.top + height)
|
||||
/**
|
||||
* Attempts to fit all [taskBounds] inside [layoutBounds]. The method ensures that the returned
|
||||
* output bounds list has appropriate size and populates it with the values placing task windows
|
||||
* next to each other left-to-right in rows of equal [optimalWindowHeight].
|
||||
*/
|
||||
private fun fitWindowRectsInBounds(
|
||||
layoutBounds: Rect,
|
||||
taskBounds: List<DesktopTaskBoundsData>,
|
||||
optimalWindowHeight: Int,
|
||||
): FitWindowResult {
|
||||
val numTasks = taskBounds.size
|
||||
val outRects = mutableListOf<RectF>()
|
||||
|
||||
// Start in the top-left corner of [layoutBounds].
|
||||
var left = layoutBounds.left
|
||||
var top = layoutBounds.top
|
||||
|
||||
// Right bound of the narrowest row.
|
||||
var minRight = layoutBounds.right
|
||||
// Right bound of the widest row.
|
||||
var maxRight = layoutBounds.left
|
||||
|
||||
var allWindowsFit = true
|
||||
for (i in 0 until numTasks) {
|
||||
val taskBounds = taskBounds[i].bounds
|
||||
|
||||
// Use the height to calculate the width
|
||||
val scale = optimalWindowHeight / taskBounds.height().toFloat()
|
||||
val width = (taskBounds.width() * scale).toInt()
|
||||
val optimalRowHeight = optimalWindowHeight + VERTICAL_SPACE_BETWEEN_TASKS
|
||||
|
||||
if ((left + width + HORIZONTAL_SPACE_BETWEEN_TASKS) > layoutBounds.right) {
|
||||
// Move to the next row if possible.
|
||||
minRight = minOf(minRight, left)
|
||||
maxRight = maxOf(maxRight, left)
|
||||
top += optimalRowHeight
|
||||
|
||||
// Check if the new row reaches the bottom or if the first item in the new
|
||||
// row does not fit within the available width.
|
||||
if (
|
||||
(top + optimalRowHeight) > layoutBounds.bottom ||
|
||||
layoutBounds.left + width + HORIZONTAL_SPACE_BETWEEN_TASKS >
|
||||
layoutBounds.right
|
||||
) {
|
||||
allWindowsFit = false
|
||||
break
|
||||
}
|
||||
left = layoutBounds.left
|
||||
}
|
||||
|
||||
// Position the current rect.
|
||||
outRects.add(
|
||||
RectF(
|
||||
left.toFloat(),
|
||||
top.toFloat(),
|
||||
(left + width).toFloat(),
|
||||
(top + optimalWindowHeight).toFloat(),
|
||||
)
|
||||
)
|
||||
|
||||
// Increment horizontal position.
|
||||
left += (width + HORIZONTAL_SPACE_BETWEEN_TASKS)
|
||||
}
|
||||
|
||||
// Update the narrowest and widest row width for the last row.
|
||||
minRight = minOf(minRight, left)
|
||||
maxRight = maxOf(maxRight, left)
|
||||
|
||||
return FitWindowResult(allWindowsFit, outRects, minRight, maxRight)
|
||||
}
|
||||
|
||||
/** Centers task windows in the center of Overview. */
|
||||
private fun centerTaskWindows(layoutBounds: Rect, maxBottom: Int, outWindowRects: List<RectF>) {
|
||||
if (outWindowRects.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val currentRowUnionRange = RectF(outWindowRects[0])
|
||||
var currentRowY = outWindowRects[0].top
|
||||
var currentRowFirstItemIndex = 0
|
||||
val offsetY = (layoutBounds.bottom - maxBottom) / 2f
|
||||
|
||||
// Batch process to center overview desktop task windows within the same row.
|
||||
fun batchCenterDesktopTaskWindows(endIndex: Int) {
|
||||
// Calculate the shift amount required to center the desktop task items.
|
||||
val rangeCenterX = (currentRowUnionRange.left + currentRowUnionRange.right) / 2f
|
||||
val currentDiffX = (layoutBounds.centerX() - rangeCenterX).coerceAtLeast(0f)
|
||||
for (j in currentRowFirstItemIndex until endIndex) {
|
||||
outWindowRects[j].offset(currentDiffX, offsetY)
|
||||
}
|
||||
}
|
||||
|
||||
outWindowRects.forEachIndexed { index, rect ->
|
||||
if (rect.top != currentRowY) {
|
||||
// As a new row begins processing, batch-shift the previous row's rects
|
||||
// and reset its parameters.
|
||||
batchCenterDesktopTaskWindows(index)
|
||||
currentRowUnionRange.set(rect)
|
||||
currentRowY = rect.top
|
||||
currentRowFirstItemIndex = index
|
||||
}
|
||||
|
||||
// Extend the range by adding the [rect]'s width and extra in-between items
|
||||
// spacing.
|
||||
currentRowUnionRange.right = rect.right
|
||||
}
|
||||
|
||||
// Post-processing rects in the last row.
|
||||
batchCenterDesktopTaskWindows(outWindowRects.size)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val VERTICAL_SPACE_BETWEEN_TASKS = 24
|
||||
const val HORIZONTAL_SPACE_BETWEEN_TASKS = 24
|
||||
const val OVERVIEW_INSET_TOP_BOTTOM = 16
|
||||
const val OVERVIEW_INSET_LEFT_RIGHT = 16
|
||||
const val MAXIMUM_TASK_HEIGHT = 800
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package com.android.quickstep.recents.ui.viewmodel
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.util.Size
|
||||
import com.android.quickstep.recents.domain.model.DesktopTaskBoundsData
|
||||
import com.android.quickstep.recents.domain.usecase.OrganizeDesktopTasksUseCase
|
||||
@@ -36,6 +37,9 @@ class DesktopTaskViewModel(private val organizeDesktopTasksUseCase: OrganizeDesk
|
||||
*/
|
||||
fun organizeDesktopTasks(desktopSize: Size, defaultPositions: List<DesktopTaskBoundsData>) {
|
||||
organizedDesktopTaskPositions =
|
||||
organizeDesktopTasksUseCase.run(desktopSize, defaultPositions)
|
||||
organizeDesktopTasksUseCase.run(
|
||||
desktopBounds = Rect(0, 0, desktopSize.width, desktopSize.height),
|
||||
taskBounds = defaultPositions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user