• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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