360 lines
15 KiB
Kotlin
360 lines
15 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.views
|
|
|
|
import android.graphics.Rect
|
|
import android.view.View
|
|
import androidx.core.view.children
|
|
import androidx.dynamicanimation.animation.FloatPropertyCompat
|
|
import androidx.dynamicanimation.animation.SpringAnimation
|
|
import androidx.dynamicanimation.animation.SpringForce
|
|
import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
|
|
import com.android.launcher3.Flags.enableSeparateExternalDisplayTasks
|
|
import com.android.launcher3.R
|
|
import com.android.launcher3.touch.SingleAxisSwipeDetector
|
|
import com.android.launcher3.util.DynamicResource
|
|
import com.android.launcher3.util.IntArray
|
|
import com.android.quickstep.util.GroupTask
|
|
import com.android.quickstep.util.isExternalDisplay
|
|
import com.android.quickstep.views.RecentsView.RUNNING_TASK_ATTACH_ALPHA
|
|
import com.android.systemui.shared.recents.model.ThumbnailData
|
|
import java.util.function.BiConsumer
|
|
import kotlin.math.abs
|
|
|
|
/**
|
|
* Helper class for [RecentsView]. This util class contains refactored and extracted functions from
|
|
* RecentsView to facilitate the implementation of unit tests.
|
|
*/
|
|
class RecentsViewUtils(private val recentsView: RecentsView<*, *>) {
|
|
val taskViews = TaskViewsIterable(recentsView)
|
|
|
|
/** Takes a screenshot of all [taskView] and return map of taskId to the screenshot */
|
|
fun screenshotTasks(taskView: TaskView): Map<Int, ThumbnailData> {
|
|
val recentsAnimationController = recentsView.recentsAnimationController ?: return emptyMap()
|
|
return taskView.taskContainers.associate {
|
|
it.task.key.id to recentsAnimationController.screenshotTask(it.task.key.id)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sorts task groups to move desktop tasks to the end of the list.
|
|
*
|
|
* @param tasks List of group tasks to be sorted.
|
|
* @return Sorted list of GroupTasks to be used in the RecentsView.
|
|
*/
|
|
fun sortDesktopTasksToFront(tasks: List<GroupTask>): List<GroupTask> {
|
|
val (desktopTasks, otherTasks) = tasks.partition { it.taskViewType == TaskViewType.DESKTOP }
|
|
return otherTasks + desktopTasks
|
|
}
|
|
|
|
fun sortExternalDisplayTasksToFront(tasks: List<GroupTask>): List<GroupTask> {
|
|
val (externalDisplayTasks, otherTasks) =
|
|
tasks.partition { it.tasks.firstOrNull().isExternalDisplay }
|
|
return otherTasks + externalDisplayTasks
|
|
}
|
|
|
|
class TaskViewsIterable(val recentsView: RecentsView<*, *>) : Iterable<TaskView> {
|
|
/** Iterates TaskViews when its index inside the RecentsView is needed. */
|
|
fun forEachWithIndexInParent(consumer: BiConsumer<Int, TaskView>) {
|
|
recentsView.children.forEachIndexed { index, child ->
|
|
(child as? TaskView)?.let { consumer.accept(index, it) }
|
|
}
|
|
}
|
|
|
|
override fun iterator(): Iterator<TaskView> =
|
|
recentsView.children.mapNotNull { it as? TaskView }.iterator()
|
|
}
|
|
|
|
/** Counts [TaskView]s that are [DesktopTaskView] instances. */
|
|
fun getDesktopTaskViewCount(): Int = taskViews.count { it is DesktopTaskView }
|
|
|
|
/** Counts [TaskView]s that are not [DesktopTaskView] instances. */
|
|
fun getNonDesktopTaskViewCount(): Int = taskViews.count { it !is DesktopTaskView }
|
|
|
|
/** Returns a list of all large TaskView Ids from [TaskView]s */
|
|
fun getLargeTaskViewIds(): List<Int> = taskViews.filter { it.isLargeTile }.map { it.taskViewId }
|
|
|
|
/** Counts [TaskView]s that are large tiles. */
|
|
fun getLargeTileCount(): Int = taskViews.count { it.isLargeTile }
|
|
|
|
/** Returns the first TaskView that should be displayed as a large tile. */
|
|
fun getFirstLargeTaskView(): TaskView? =
|
|
taskViews.firstOrNull {
|
|
it.isLargeTile && !(recentsView.isSplitSelectionActive && it is DesktopTaskView)
|
|
}
|
|
|
|
/** Returns the expected focus task. */
|
|
fun getExpectedFocusedTask(): TaskView? =
|
|
if (enableLargeDesktopWindowingTile()) taskViews.firstOrNull { it !is DesktopTaskView }
|
|
else taskViews.firstOrNull()
|
|
|
|
/**
|
|
* Returns the [TaskView] that should be the current page during task binding, in the following
|
|
* priorities:
|
|
* 1. Running task
|
|
* 2. Focused task
|
|
* 3. First non-desktop task
|
|
* 4. Last desktop task
|
|
* 5. null otherwise
|
|
*/
|
|
fun getExpectedCurrentTask(runningTaskView: TaskView?, focusedTaskView: TaskView?): TaskView? =
|
|
runningTaskView
|
|
?: focusedTaskView
|
|
?: taskViews.firstOrNull {
|
|
it !is DesktopTaskView &&
|
|
!(enableSeparateExternalDisplayTasks() && it.isExternalDisplay)
|
|
}
|
|
?: taskViews.lastOrNull()
|
|
|
|
private fun getDeviceProfile() = (recentsView.mContainer as RecentsViewContainer).deviceProfile
|
|
|
|
fun getRunningTaskExpectedIndex(runningTaskView: TaskView): Int {
|
|
val firstTaskViewIndex = recentsView.indexOfChild(getFirstTaskView())
|
|
return if (getDeviceProfile().isTablet) {
|
|
var index = firstTaskViewIndex
|
|
if (enableLargeDesktopWindowingTile() && runningTaskView !is DesktopTaskView) {
|
|
// For fullsreen tasks, skip over Desktop tasks in its section
|
|
index +=
|
|
if (enableSeparateExternalDisplayTasks()) {
|
|
if (runningTaskView.isExternalDisplay) {
|
|
taskViews.count { it is DesktopTaskView && it.isExternalDisplay }
|
|
} else {
|
|
taskViews.count { it is DesktopTaskView && !it.isExternalDisplay }
|
|
}
|
|
} else {
|
|
getDesktopTaskViewCount()
|
|
}
|
|
}
|
|
if (enableSeparateExternalDisplayTasks() && !runningTaskView.isExternalDisplay) {
|
|
// For main display section, skip over external display tasks
|
|
index += taskViews.count { it.isExternalDisplay }
|
|
}
|
|
index
|
|
} else {
|
|
val currentIndex: Int = recentsView.indexOfChild(runningTaskView)
|
|
return if (currentIndex != -1) {
|
|
currentIndex // Keep the position if running task already in layout.
|
|
} else {
|
|
// New running task are added to the front to begin with.
|
|
firstTaskViewIndex
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Returns the first TaskView if it exists, or null otherwise. */
|
|
fun getFirstTaskView(): TaskView? = taskViews.firstOrNull()
|
|
|
|
/** Returns the last TaskView if it exists, or null otherwise. */
|
|
fun getLastTaskView(): TaskView? = taskViews.lastOrNull()
|
|
|
|
/** Returns the first TaskView that is not large */
|
|
fun getFirstSmallTaskView(): TaskView? = taskViews.firstOrNull { !it.isLargeTile }
|
|
|
|
/** Returns the last TaskView that should be displayed as a large tile. */
|
|
fun getLastLargeTaskView(): TaskView? = taskViews.lastOrNull { it.isLargeTile }
|
|
|
|
/**
|
|
* Gets the list of accessibility children. Currently all the children of RecentsViews are
|
|
* added, and in the reverse order to the list.
|
|
*/
|
|
fun getAccessibilityChildren(): List<View> = recentsView.children.toList().reversed()
|
|
|
|
@JvmOverloads
|
|
/** Returns the first [TaskView], with some tasks possibly hidden in the carousel. */
|
|
fun getFirstTaskViewInCarousel(
|
|
nonRunningTaskCarouselHidden: Boolean,
|
|
runningTaskView: TaskView? = recentsView.runningTaskView,
|
|
): TaskView? =
|
|
taskViews.firstOrNull {
|
|
it.isVisibleInCarousel(runningTaskView, nonRunningTaskCarouselHidden)
|
|
}
|
|
|
|
/** Returns the last [TaskView], with some tasks possibly hidden in the carousel. */
|
|
fun getLastTaskViewInCarousel(nonRunningTaskCarouselHidden: Boolean): TaskView? =
|
|
taskViews.lastOrNull {
|
|
it.isVisibleInCarousel(recentsView.runningTaskView, nonRunningTaskCarouselHidden)
|
|
}
|
|
|
|
/** Returns if any small tasks are fully visible */
|
|
fun isAnySmallTaskFullyVisible(): Boolean =
|
|
taskViews.any { !it.isLargeTile && recentsView.isTaskViewFullyVisible(it) }
|
|
|
|
/** Apply attachAlpha to all [TaskView] accordingly to different conditions. */
|
|
fun applyAttachAlpha(nonRunningTaskCarouselHidden: Boolean) {
|
|
taskViews.forEach { taskView ->
|
|
taskView.attachAlpha =
|
|
if (taskView == recentsView.runningTaskView) {
|
|
RUNNING_TASK_ATTACH_ALPHA.get(recentsView)
|
|
} else {
|
|
if (
|
|
taskView.isVisibleInCarousel(
|
|
recentsView.runningTaskView,
|
|
nonRunningTaskCarouselHidden,
|
|
)
|
|
)
|
|
1f
|
|
else 0f
|
|
}
|
|
}
|
|
}
|
|
|
|
fun TaskView.isVisibleInCarousel(
|
|
runningTaskView: TaskView?,
|
|
nonRunningTaskCarouselHidden: Boolean,
|
|
): Boolean =
|
|
if (!nonRunningTaskCarouselHidden) true
|
|
else getCarouselType() == runningTaskView.getCarouselType()
|
|
|
|
/** Returns the carousel type of the TaskView, and default to fullscreen if it's null. */
|
|
private fun TaskView?.getCarouselType(): TaskViewCarousel =
|
|
if (this is DesktopTaskView) TaskViewCarousel.DESKTOP else TaskViewCarousel.FULL_SCREEN
|
|
|
|
private enum class TaskViewCarousel {
|
|
FULL_SCREEN,
|
|
DESKTOP,
|
|
}
|
|
|
|
/** Returns true if there are at least one TaskView has been added to the RecentsView. */
|
|
fun hasTaskViews() = taskViews.any()
|
|
|
|
fun getTaskContainerById(taskId: Int) =
|
|
taskViews.firstNotNullOfOrNull { it.getTaskContainerById(taskId) }
|
|
|
|
private fun getRowRect(firstView: View?, lastView: View?, outRowRect: Rect) {
|
|
outRowRect.setEmpty()
|
|
firstView?.let {
|
|
it.getHitRect(TEMP_RECT)
|
|
outRowRect.union(TEMP_RECT)
|
|
}
|
|
lastView?.let {
|
|
it.getHitRect(TEMP_RECT)
|
|
outRowRect.union(TEMP_RECT)
|
|
}
|
|
}
|
|
|
|
private fun getRowRect(rowTaskViewIds: IntArray, outRowRect: Rect) {
|
|
if (rowTaskViewIds.isEmpty) {
|
|
outRowRect.setEmpty()
|
|
return
|
|
}
|
|
getRowRect(
|
|
recentsView.getTaskViewFromTaskViewId(rowTaskViewIds.get(0)),
|
|
recentsView.getTaskViewFromTaskViewId(rowTaskViewIds.get(rowTaskViewIds.size() - 1)),
|
|
outRowRect,
|
|
)
|
|
}
|
|
|
|
fun updateTaskViewDeadZoneRect(
|
|
outTaskViewRowRect: Rect,
|
|
outTopRowRect: Rect,
|
|
outBottomRowRect: Rect,
|
|
) {
|
|
if (!getDeviceProfile().isTablet) {
|
|
getRowRect(getFirstTaskView(), getLastTaskView(), outTaskViewRowRect)
|
|
return
|
|
}
|
|
getRowRect(getFirstLargeTaskView(), getLastLargeTaskView(), outTaskViewRowRect)
|
|
getRowRect(recentsView.getTopRowIdArray(), outTopRowRect)
|
|
getRowRect(recentsView.getBottomRowIdArray(), outBottomRowRect)
|
|
|
|
// Expand large tile Rect to include space between top/bottom row.
|
|
val nonEmptyRowRect =
|
|
when {
|
|
!outTopRowRect.isEmpty -> outTopRowRect
|
|
!outBottomRowRect.isEmpty -> outBottomRowRect
|
|
else -> return
|
|
}
|
|
if (recentsView.isRtl) {
|
|
if (outTaskViewRowRect.left > nonEmptyRowRect.right) {
|
|
outTaskViewRowRect.left = nonEmptyRowRect.right
|
|
}
|
|
} else {
|
|
if (outTaskViewRowRect.right < nonEmptyRowRect.left) {
|
|
outTaskViewRowRect.right = nonEmptyRowRect.left
|
|
}
|
|
}
|
|
|
|
// Expand the shorter row Rect to include the space between the 2 rows.
|
|
if (outTopRowRect.isEmpty || outBottomRowRect.isEmpty) return
|
|
if (outTopRowRect.width() <= outBottomRowRect.width()) {
|
|
if (outTopRowRect.bottom < outBottomRowRect.top) {
|
|
outTopRowRect.bottom = outBottomRowRect.top
|
|
}
|
|
} else {
|
|
if (outBottomRowRect.top > outTopRowRect.bottom) {
|
|
outBottomRowRect.top = outTopRowRect.bottom
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates the spring animations which run when a dragged task view in overview is released.
|
|
*
|
|
* <p>When a task dismiss is cancelled, the task will return to its original position via a
|
|
* spring animation.
|
|
*/
|
|
fun createTaskDismissSettlingSpringAnimation(
|
|
draggedTaskView: TaskView?,
|
|
velocity: Float,
|
|
isDismissing: Boolean,
|
|
detector: SingleAxisSwipeDetector,
|
|
dismissLength: Int,
|
|
onEndRunnable: () -> Unit,
|
|
): SpringAnimation? {
|
|
draggedTaskView ?: return null
|
|
val taskDismissFloatProperty =
|
|
FloatPropertyCompat.createFloatPropertyCompat(
|
|
draggedTaskView.secondaryDismissTranslationProperty
|
|
)
|
|
val rp = DynamicResource.provider(recentsView.mContainer)
|
|
return SpringAnimation(draggedTaskView, taskDismissFloatProperty)
|
|
.setSpring(
|
|
SpringForce()
|
|
.setDampingRatio(rp.getFloat(R.dimen.dismiss_task_trans_y_damping_ratio))
|
|
.setStiffness(rp.getFloat(R.dimen.dismiss_task_trans_y_stiffness))
|
|
)
|
|
.setStartVelocity(if (detector.isFling(velocity)) velocity else 0f)
|
|
.addUpdateListener { animation, value, _ ->
|
|
if (isDismissing && abs(value) >= abs(dismissLength)) {
|
|
// TODO(b/393553524): Remove 0 alpha, instead animate task fully off screen.
|
|
draggedTaskView.alpha = 0f
|
|
animation.cancel()
|
|
} else if (draggedTaskView.isRunningTask && recentsView.enableDrawingLiveTile) {
|
|
recentsView.runActionOnRemoteHandles { remoteTargetHandle ->
|
|
remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation.value =
|
|
taskDismissFloatProperty.getValue(draggedTaskView)
|
|
}
|
|
recentsView.redrawLiveTile()
|
|
}
|
|
}
|
|
.addEndListener { _, _, _, _ ->
|
|
if (isDismissing) {
|
|
recentsView.dismissTask(
|
|
draggedTaskView,
|
|
/* animateTaskView = */ false,
|
|
/* removeTask = */ true,
|
|
)
|
|
}
|
|
onEndRunnable()
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
val TEMP_RECT = Rect()
|
|
}
|
|
}
|