1 /* 2 * Copyright (C) 2025 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.quickstep.util 17 18 import com.android.launcher3.util.IntArray 19 import kotlin.math.abs 20 import kotlin.math.max 21 22 /** Helper class for navigating RecentsView grid tasks via arrow keys and tab. */ 23 class TaskGridNavHelper( 24 private val topIds: IntArray, 25 bottomIds: IntArray, 26 largeTileIds: List<Int>, 27 hasAddDesktopButton: Boolean, 28 ) { 29 private val topRowIds = mutableListOf<Int>() 30 private val bottomRowIds = mutableListOf<Int>() 31 32 init { 33 // Add AddDesktopButton and lage tiles to both rows. 34 if (hasAddDesktopButton) { 35 topRowIds += ADD_DESK_PLACEHOLDER_ID 36 bottomRowIds += ADD_DESK_PLACEHOLDER_ID 37 } 38 topRowIds += largeTileIds 39 bottomRowIds += largeTileIds 40 41 // Add row ids to their respective rows. 42 topRowIds += topIds 43 bottomRowIds += bottomIds 44 45 // Fill in the shorter array with the ids from the longer one. 46 topRowIds += bottomRowIds.takeLast(max(bottomRowIds.size - topRowIds.size, 0)) 47 bottomRowIds += topRowIds.takeLast(max(topRowIds.size - bottomRowIds.size, 0)) 48 49 // Add the clear all button to the end of both arrays. 50 topRowIds += CLEAR_ALL_PLACEHOLDER_ID 51 bottomRowIds += CLEAR_ALL_PLACEHOLDER_ID 52 } 53 54 /** Returns the id of the next page in the grid or -1 for the clear all button. */ getNextGridPagenull55 fun getNextGridPage( 56 currentPageTaskViewId: Int, 57 delta: Int, 58 direction: TaskNavDirection, 59 cycle: Boolean, 60 ): Int { 61 val inTop = topRowIds.contains(currentPageTaskViewId) 62 val index = 63 if (inTop) topRowIds.indexOf(currentPageTaskViewId) 64 else bottomRowIds.indexOf(currentPageTaskViewId) 65 val maxSize = max(topRowIds.size, bottomRowIds.size) 66 val nextIndex = index + delta 67 68 return when (direction) { 69 TaskNavDirection.UP, 70 TaskNavDirection.DOWN -> { 71 if (inTop) bottomRowIds[index] else topRowIds[index] 72 } 73 TaskNavDirection.LEFT -> { 74 val boundedIndex = 75 if (cycle) nextIndex % maxSize else nextIndex.coerceAtMost(maxSize - 1) 76 if (inTop) topRowIds[boundedIndex] else bottomRowIds[boundedIndex] 77 } 78 TaskNavDirection.RIGHT -> { 79 val boundedIndex = 80 if (cycle) (if (nextIndex < 0) maxSize - 1 else nextIndex) 81 else nextIndex.coerceAtLeast(0) 82 val inOriginalTop = topIds.contains(currentPageTaskViewId) 83 if (inOriginalTop) topRowIds[boundedIndex] else bottomRowIds[boundedIndex] 84 } 85 TaskNavDirection.TAB -> { 86 val boundedIndex = 87 if (cycle) (if (nextIndex < 0) maxSize - 1 else nextIndex % maxSize) 88 else nextIndex.coerceAtMost(maxSize - 1) 89 if (delta >= 0) { 90 if (inTop && topRowIds[index] != bottomRowIds[index]) bottomRowIds[index] 91 else topRowIds[boundedIndex] 92 } else { 93 if (topRowIds.contains(currentPageTaskViewId)) { 94 if (boundedIndex < 0) { 95 // If no cycling, always return the first task. 96 topRowIds[0] 97 } else { 98 bottomRowIds[boundedIndex] 99 } 100 } else { 101 // Go up to top if there is task above 102 if (topRowIds[index] != bottomRowIds[index]) topRowIds[index] 103 else bottomRowIds[boundedIndex] 104 } 105 } 106 } 107 else -> currentPageTaskViewId 108 } 109 } 110 111 /** 112 * Returns a sequence of pairs of (TaskView ID, offset) in the grid, ordered according to tab 113 * navigation, starting from the initial TaskView ID, towards the start or end of the grid. 114 * 115 * <p>A positive delta moves forward in the tab order towards the end of the grid, while a 116 * negative value moves backward towards the beginning. The offset is the distance between 117 * columns the tasks are in. 118 */ gridTaskViewIdOffsetPairInTabOrderSequencenull119 fun gridTaskViewIdOffsetPairInTabOrderSequence( 120 initialTaskViewId: Int, 121 towardsStart: Boolean, 122 ): Sequence<Pair<Int, Int>> = sequence { 123 val draggedTaskViewColumn = getColumn(initialTaskViewId) 124 var nextTaskViewId: Int = initialTaskViewId 125 var previousTaskViewId: Int = Int.MIN_VALUE 126 while (nextTaskViewId != previousTaskViewId && nextTaskViewId >= 0) { 127 previousTaskViewId = nextTaskViewId 128 nextTaskViewId = 129 getNextGridPage( 130 nextTaskViewId, 131 if (towardsStart) -1 else 1, 132 TaskNavDirection.TAB, 133 cycle = false, 134 ) 135 if (nextTaskViewId >= 0 && nextTaskViewId != previousTaskViewId) { 136 val columnOffset = abs(getColumn(nextTaskViewId) - draggedTaskViewColumn) 137 yield(Pair(nextTaskViewId, columnOffset)) 138 } 139 } 140 } 141 142 /** Returns the column of a task's id in the grid. */ getColumnnull143 private fun getColumn(taskViewId: Int): Int = 144 if (topRowIds.contains(taskViewId)) topRowIds.indexOf(taskViewId) 145 else bottomRowIds.indexOf(taskViewId) 146 147 enum class TaskNavDirection { 148 UP, 149 DOWN, 150 LEFT, 151 RIGHT, 152 TAB, 153 } 154 155 companion object { 156 const val CLEAR_ALL_PLACEHOLDER_ID: Int = -1 157 const val ADD_DESK_PLACEHOLDER_ID: Int = -2 158 } 159 } 160