1 /* 2 * Copyright (C) 2015 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 17 package com.android.launcher3.util; 18 19 import android.util.Log; 20 import android.view.KeyEvent; 21 import android.view.View; 22 import android.view.ViewGroup; 23 24 import com.android.launcher3.CellLayout; 25 import com.android.launcher3.DeviceProfile; 26 import com.android.launcher3.ShortcutAndWidgetContainer; 27 import com.android.launcher3.config.FeatureFlags; 28 29 import java.util.Arrays; 30 31 /** 32 * Calculates the next item that a {@link KeyEvent} should change the focus to. 33 *<p> 34 * Note, this utility class calculates everything regards to icon index and its (x,y) coordinates. 35 * Currently supports: 36 * <ul> 37 * <li> full matrix of cells that are 1x1 38 * <li> sparse matrix of cells that are 1x1 39 * [ 1][ ][ 2][ ] 40 * [ ][ ][ 3][ ] 41 * [ ][ 4][ ][ ] 42 * [ ][ 5][ 6][ 7] 43 * </ul> 44 * *<p> 45 * For testing, one can use a BT keyboard, or use following adb command. 46 * ex. $ adb shell input keyevent 20 // KEYCODE_DPAD_LEFT 47 */ 48 public class FocusLogic { 49 50 private static final String TAG = "FocusLogic"; 51 private static final boolean DEBUG = false; 52 53 /** Item and page index related constant used by {@link #handleKeyEvent}. */ 54 public static final int NOOP = -1; 55 56 public static final int PREVIOUS_PAGE_RIGHT_COLUMN = -2; 57 public static final int PREVIOUS_PAGE_FIRST_ITEM = -3; 58 public static final int PREVIOUS_PAGE_LAST_ITEM = -4; 59 public static final int PREVIOUS_PAGE_LEFT_COLUMN = -5; 60 61 public static final int CURRENT_PAGE_FIRST_ITEM = -6; 62 public static final int CURRENT_PAGE_LAST_ITEM = -7; 63 64 public static final int NEXT_PAGE_FIRST_ITEM = -8; 65 public static final int NEXT_PAGE_LEFT_COLUMN = -9; 66 public static final int NEXT_PAGE_RIGHT_COLUMN = -10; 67 68 public static final int ALL_APPS_COLUMN = -11; 69 70 // Matrix related constant. 71 public static final int EMPTY = -1; 72 public static final int PIVOT = 100; 73 74 /** 75 * Returns true only if this utility class handles the key code. 76 */ 77 public static boolean shouldConsume(int keyCode) { 78 return (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT || 79 keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_DOWN || 80 keyCode == KeyEvent.KEYCODE_MOVE_HOME || keyCode == KeyEvent.KEYCODE_MOVE_END || 81 keyCode == KeyEvent.KEYCODE_PAGE_UP || keyCode == KeyEvent.KEYCODE_PAGE_DOWN); 82 } 83 84 public static int handleKeyEvent(int keyCode, int [][] map, int iconIdx, int pageIndex, 85 int pageCount, boolean isRtl) { 86 87 int cntX = map == null ? -1 : map.length; 88 int cntY = map == null ? -1 : map[0].length; 89 90 if (DEBUG) { 91 Log.v(TAG, String.format( 92 "handleKeyEvent START: cntX=%d, cntY=%d, iconIdx=%d, pageIdx=%d, pageCnt=%d", 93 cntX, cntY, iconIdx, pageIndex, pageCount)); 94 } 95 96 int newIndex = NOOP; 97 switch (keyCode) { 98 case KeyEvent.KEYCODE_DPAD_LEFT: 99 newIndex = handleDpadHorizontal(iconIdx, cntX, cntY, map, -1 /*increment*/, isRtl); 100 if (!isRtl && newIndex == NOOP && pageIndex > 0) { 101 newIndex = PREVIOUS_PAGE_RIGHT_COLUMN; 102 } else if (isRtl && newIndex == NOOP && pageIndex < pageCount - 1) { 103 newIndex = NEXT_PAGE_RIGHT_COLUMN; 104 } 105 break; 106 case KeyEvent.KEYCODE_DPAD_RIGHT: 107 newIndex = handleDpadHorizontal(iconIdx, cntX, cntY, map, 1 /*increment*/, isRtl); 108 if (!isRtl && newIndex == NOOP && pageIndex < pageCount - 1) { 109 newIndex = NEXT_PAGE_LEFT_COLUMN; 110 } else if (isRtl && newIndex == NOOP && pageIndex > 0) { 111 newIndex = PREVIOUS_PAGE_LEFT_COLUMN; 112 } 113 break; 114 case KeyEvent.KEYCODE_DPAD_DOWN: 115 newIndex = handleDpadVertical(iconIdx, cntX, cntY, map, 1 /*increment*/); 116 break; 117 case KeyEvent.KEYCODE_DPAD_UP: 118 newIndex = handleDpadVertical(iconIdx, cntX, cntY, map, -1 /*increment*/); 119 break; 120 case KeyEvent.KEYCODE_MOVE_HOME: 121 newIndex = handleMoveHome(); 122 break; 123 case KeyEvent.KEYCODE_MOVE_END: 124 newIndex = handleMoveEnd(); 125 break; 126 case KeyEvent.KEYCODE_PAGE_DOWN: 127 newIndex = handlePageDown(pageIndex, pageCount); 128 break; 129 case KeyEvent.KEYCODE_PAGE_UP: 130 newIndex = handlePageUp(pageIndex); 131 break; 132 default: 133 break; 134 } 135 136 if (DEBUG) { 137 Log.v(TAG, String.format("handleKeyEvent FINISH: index [%d -> %s]", 138 iconIdx, getStringIndex(newIndex))); 139 } 140 return newIndex; 141 } 142 143 /** 144 * Returns a matrix of size (m x n) that has been initialized with {@link #EMPTY}. 145 * 146 * @param m number of columns in the matrix 147 * @param n number of rows in the matrix 148 */ 149 // TODO: get rid of dynamic matrix creation. 150 private static int[][] createFullMatrix(int m, int n) { 151 int[][] matrix = new int [m][n]; 152 153 for (int i=0; i < m;i++) { 154 Arrays.fill(matrix[i], EMPTY); 155 } 156 return matrix; 157 } 158 159 /** 160 * Returns a matrix of size same as the {@link CellLayout} dimension that is initialized with the 161 * index of the child view. 162 */ 163 // TODO: get rid of the dynamic matrix creation 164 public static int[][] createSparseMatrix(CellLayout layout) { 165 ShortcutAndWidgetContainer parent = layout.getShortcutsAndWidgets(); 166 final int m = layout.getCountX(); 167 final int n = layout.getCountY(); 168 final boolean invert = parent.invertLayoutHorizontally(); 169 170 int[][] matrix = createFullMatrix(m, n); 171 172 // Iterate thru the children. 173 for (int i = 0; i < parent.getChildCount(); i++ ) { 174 View cell = parent.getChildAt(i); 175 if (!cell.isFocusable()) { 176 continue; 177 } 178 int cx = ((CellLayout.LayoutParams) cell.getLayoutParams()).cellX; 179 int cy = ((CellLayout.LayoutParams) cell.getLayoutParams()).cellY; 180 int x = invert ? (m - cx - 1) : cx; 181 if (x < m && cy < n) { // check if view fits into matrix, else skip 182 matrix[x][cy] = i; 183 } 184 } 185 if (DEBUG) { 186 printMatrix(matrix); 187 } 188 return matrix; 189 } 190 191 /** 192 * Creates a sparse matrix that merges the icon and hotseat view group using the cell layout. 193 * The size of the returning matrix is [icon column count x (icon + hotseat row count)] 194 * in portrait orientation. In landscape, [(icon + hotseat) column count x (icon row count)] 195 */ 196 // TODO: get rid of the dynamic matrix creation 197 public static int[][] createSparseMatrixWithHotseat( 198 CellLayout iconLayout, CellLayout hotseatLayout, DeviceProfile dp) { 199 200 ViewGroup iconParent = iconLayout.getShortcutsAndWidgets(); 201 ViewGroup hotseatParent = hotseatLayout.getShortcutsAndWidgets(); 202 203 boolean isHotseatHorizontal = !dp.isVerticalBarLayout(); 204 boolean moreIconsInHotseatThanWorkspace = !FeatureFlags.NO_ALL_APPS_ICON && 205 (isHotseatHorizontal 206 ? hotseatLayout.getCountX() > iconLayout.getCountX() 207 : hotseatLayout.getCountY() > iconLayout.getCountY()); 208 209 int m, n; 210 if (isHotseatHorizontal) { 211 m = hotseatLayout.getCountX(); 212 n = iconLayout.getCountY() + hotseatLayout.getCountY(); 213 } else { 214 m = iconLayout.getCountX() + hotseatLayout.getCountX(); 215 n = hotseatLayout.getCountY(); 216 } 217 int[][] matrix = createFullMatrix(m, n); 218 if (moreIconsInHotseatThanWorkspace) { 219 int allappsiconRank = dp.inv.getAllAppsButtonRank(); 220 if (isHotseatHorizontal) { 221 for (int j = 0; j < n; j++) { 222 matrix[allappsiconRank][j] = ALL_APPS_COLUMN; 223 } 224 } else { 225 for (int j = 0; j < m; j++) { 226 matrix[j][allappsiconRank] = ALL_APPS_COLUMN; 227 } 228 } 229 } 230 // Iterate thru the children of the workspace. 231 for (int i = 0; i < iconParent.getChildCount(); i++) { 232 View cell = iconParent.getChildAt(i); 233 if (!cell.isFocusable()) { 234 continue; 235 } 236 int cx = ((CellLayout.LayoutParams) cell.getLayoutParams()).cellX; 237 int cy = ((CellLayout.LayoutParams) cell.getLayoutParams()).cellY; 238 if (moreIconsInHotseatThanWorkspace) { 239 int allappsiconRank = dp.inv.getAllAppsButtonRank(); 240 if (isHotseatHorizontal && cx >= allappsiconRank) { 241 // Add 1 to account for the All Apps button. 242 cx++; 243 } 244 if (!isHotseatHorizontal && cy >= allappsiconRank) { 245 // Add 1 to account for the All Apps button. 246 cy++; 247 } 248 } 249 matrix[cx][cy] = i; 250 } 251 252 // Iterate thru the children of the hotseat. 253 for (int i = hotseatParent.getChildCount() - 1; i >= 0; i--) { 254 if (isHotseatHorizontal) { 255 int cx = ((CellLayout.LayoutParams) 256 hotseatParent.getChildAt(i).getLayoutParams()).cellX; 257 matrix[cx][iconLayout.getCountY()] = iconParent.getChildCount() + i; 258 } else { 259 int cy = ((CellLayout.LayoutParams) 260 hotseatParent.getChildAt(i).getLayoutParams()).cellY; 261 matrix[iconLayout.getCountX()][cy] = iconParent.getChildCount() + i; 262 } 263 } 264 if (DEBUG) { 265 printMatrix(matrix); 266 } 267 return matrix; 268 } 269 270 /** 271 * Creates a sparse matrix that merges the icon of previous/next page and last column of 272 * current page. When left key is triggered on the leftmost column, sparse matrix is created 273 * that combines previous page matrix and an extra column on the right. Likewise, when right 274 * key is triggered on the rightmost column, sparse matrix is created that combines this column 275 * on the 0th column and the next page matrix. 276 * 277 * @param pivotX x coordinate of the focused item in the current page 278 * @param pivotY y coordinate of the focused item in the current page 279 */ 280 // TODO: get rid of the dynamic matrix creation 281 public static int[][] createSparseMatrixWithPivotColumn(CellLayout iconLayout, 282 int pivotX, int pivotY) { 283 284 ViewGroup iconParent = iconLayout.getShortcutsAndWidgets(); 285 286 int[][] matrix = createFullMatrix(iconLayout.getCountX() + 1, iconLayout.getCountY()); 287 288 // Iterate thru the children of the top parent. 289 for (int i = 0; i < iconParent.getChildCount(); i++) { 290 View cell = iconParent.getChildAt(i); 291 if (!cell.isFocusable()) { 292 continue; 293 } 294 int cx = ((CellLayout.LayoutParams) cell.getLayoutParams()).cellX; 295 int cy = ((CellLayout.LayoutParams) cell.getLayoutParams()).cellY; 296 if (pivotX < 0) { 297 matrix[cx - pivotX][cy] = i; 298 } else { 299 matrix[cx][cy] = i; 300 } 301 } 302 303 if (pivotX < 0) { 304 matrix[0][pivotY] = PIVOT; 305 } else { 306 matrix[pivotX][pivotY] = PIVOT; 307 } 308 if (DEBUG) { 309 printMatrix(matrix); 310 } 311 return matrix; 312 } 313 314 // 315 // key event handling methods. 316 // 317 318 /** 319 * Calculates icon that has is closest to the horizontal axis in reference to the cur icon. 320 * 321 * Example of the check order for KEYCODE_DPAD_RIGHT: 322 * [ ][ ][13][14][15] 323 * [ ][ 6][ 8][10][12] 324 * [ X][ 1][ 2][ 3][ 4] 325 * [ ][ 5][ 7][ 9][11] 326 */ 327 // TODO: add unit tests to verify all permutation. 328 private static int handleDpadHorizontal(int iconIdx, int cntX, int cntY, 329 int[][] matrix, int increment, boolean isRtl) { 330 if(matrix == null) { 331 throw new IllegalStateException("Dpad navigation requires a matrix."); 332 } 333 int newIconIndex = NOOP; 334 335 int xPos = -1; 336 int yPos = -1; 337 // Figure out the location of the icon. 338 for (int i = 0; i < cntX; i++) { 339 for (int j = 0; j < cntY; j++) { 340 if (matrix[i][j] == iconIdx) { 341 xPos = i; 342 yPos = j; 343 } 344 } 345 } 346 if (DEBUG) { 347 Log.v(TAG, String.format("\thandleDpadHorizontal: \t[x, y]=[%d, %d] iconIndex=%d", 348 xPos, yPos, iconIdx)); 349 } 350 351 // Rule1: check first in the horizontal direction 352 for (int x = xPos + increment; 0 <= x && x < cntX; x += increment) { 353 if ((newIconIndex = inspectMatrix(x, yPos, cntX, cntY, matrix)) != NOOP 354 && newIconIndex != ALL_APPS_COLUMN) { 355 return newIconIndex; 356 } 357 } 358 359 // Rule2: check (x1-n, yPos + increment), (x1-n, yPos - increment) 360 // (x2-n, yPos + 2*increment), (x2-n, yPos - 2*increment) 361 int nextYPos1; 362 int nextYPos2; 363 boolean haveCrossedAllAppsColumn1 = false; 364 boolean haveCrossedAllAppsColumn2 = false; 365 int x = -1; 366 for (int coeff = 1; coeff < cntY; coeff++) { 367 nextYPos1 = yPos + coeff * increment; 368 nextYPos2 = yPos - coeff * increment; 369 x = xPos + increment * coeff; 370 if (inspectMatrix(x, nextYPos1, cntX, cntY, matrix) == ALL_APPS_COLUMN) { 371 haveCrossedAllAppsColumn1 = true; 372 } 373 if (inspectMatrix(x, nextYPos2, cntX, cntY, matrix) == ALL_APPS_COLUMN) { 374 haveCrossedAllAppsColumn2 = true; 375 } 376 for (; 0 <= x && x < cntX; x += increment) { 377 int offset1 = haveCrossedAllAppsColumn1 && x < cntX - 1 ? increment : 0; 378 newIconIndex = inspectMatrix(x, nextYPos1 + offset1, cntX, cntY, matrix); 379 if (newIconIndex != NOOP) { 380 return newIconIndex; 381 } 382 int offset2 = haveCrossedAllAppsColumn2 && x < cntX - 1 ? -increment : 0; 383 newIconIndex = inspectMatrix(x, nextYPos2 + offset2, cntX, cntY, matrix); 384 if (newIconIndex != NOOP) { 385 return newIconIndex; 386 } 387 } 388 } 389 390 // Rule3: if switching between pages, do a brute-force search to find an item that was 391 // missed by rules 1 and 2 (such as when going from a bottom right icon to top left) 392 if (iconIdx == PIVOT) { 393 if (isRtl) { 394 return increment < 0 ? NEXT_PAGE_FIRST_ITEM : PREVIOUS_PAGE_LAST_ITEM; 395 } 396 return increment < 0 ? PREVIOUS_PAGE_LAST_ITEM : NEXT_PAGE_FIRST_ITEM; 397 } 398 return newIconIndex; 399 } 400 401 /** 402 * Calculates icon that is closest to the vertical axis in reference to the current icon. 403 * 404 * Example of the check order for KEYCODE_DPAD_DOWN: 405 * [ ][ ][ ][ X][ ][ ][ ] 406 * [ ][ ][ 5][ 1][ 4][ ][ ] 407 * [ ][10][ 7][ 2][ 6][ 9][ ] 408 * [14][12][ 9][ 3][ 8][11][13] 409 */ 410 // TODO: add unit tests to verify all permutation. 411 private static int handleDpadVertical(int iconIndex, int cntX, int cntY, 412 int [][] matrix, int increment) { 413 int newIconIndex = NOOP; 414 if(matrix == null) { 415 throw new IllegalStateException("Dpad navigation requires a matrix."); 416 } 417 418 int xPos = -1; 419 int yPos = -1; 420 // Figure out the location of the icon. 421 for (int i = 0; i< cntX; i++) { 422 for (int j = 0; j < cntY; j++) { 423 if (matrix[i][j] == iconIndex) { 424 xPos = i; 425 yPos = j; 426 } 427 } 428 } 429 430 if (DEBUG) { 431 Log.v(TAG, String.format("\thandleDpadVertical: \t[x, y]=[%d, %d] iconIndex=%d", 432 xPos, yPos, iconIndex)); 433 } 434 435 // Rule1: check first in the dpad direction 436 for (int y = yPos + increment; 0 <= y && y <cntY && 0 <= y; y += increment) { 437 if ((newIconIndex = inspectMatrix(xPos, y, cntX, cntY, matrix)) != NOOP 438 && newIconIndex != ALL_APPS_COLUMN) { 439 return newIconIndex; 440 } 441 } 442 443 // Rule2: check (xPos + increment, y_(1-n)), (xPos - increment, y_(1-n)) 444 // (xPos + 2*increment, y_(2-n))), (xPos - 2*increment, y_(2-n)) 445 int nextXPos1; 446 int nextXPos2; 447 boolean haveCrossedAllAppsColumn1 = false; 448 boolean haveCrossedAllAppsColumn2 = false; 449 int y = -1; 450 for (int coeff = 1; coeff < cntX; coeff++) { 451 nextXPos1 = xPos + coeff * increment; 452 nextXPos2 = xPos - coeff * increment; 453 y = yPos + increment * coeff; 454 if (inspectMatrix(nextXPos1, y, cntX, cntY, matrix) == ALL_APPS_COLUMN) { 455 haveCrossedAllAppsColumn1 = true; 456 } 457 if (inspectMatrix(nextXPos2, y, cntX, cntY, matrix) == ALL_APPS_COLUMN) { 458 haveCrossedAllAppsColumn2 = true; 459 } 460 for (; 0 <= y && y < cntY; y = y + increment) { 461 int offset1 = haveCrossedAllAppsColumn1 && y < cntY - 1 ? increment : 0; 462 newIconIndex = inspectMatrix(nextXPos1 + offset1, y, cntX, cntY, matrix); 463 if (newIconIndex != NOOP) { 464 return newIconIndex; 465 } 466 int offset2 = haveCrossedAllAppsColumn2 && y < cntY - 1 ? -increment : 0; 467 newIconIndex = inspectMatrix(nextXPos2 + offset2, y, cntX, cntY, matrix); 468 if (newIconIndex != NOOP) { 469 return newIconIndex; 470 } 471 } 472 } 473 return newIconIndex; 474 } 475 476 private static int handleMoveHome() { 477 return CURRENT_PAGE_FIRST_ITEM; 478 } 479 480 private static int handleMoveEnd() { 481 return CURRENT_PAGE_LAST_ITEM; 482 } 483 484 private static int handlePageDown(int pageIndex, int pageCount) { 485 if (pageIndex < pageCount -1) { 486 return NEXT_PAGE_FIRST_ITEM; 487 } 488 return CURRENT_PAGE_LAST_ITEM; 489 } 490 491 private static int handlePageUp(int pageIndex) { 492 if (pageIndex > 0) { 493 return PREVIOUS_PAGE_FIRST_ITEM; 494 } else { 495 return CURRENT_PAGE_FIRST_ITEM; 496 } 497 } 498 499 // 500 // Helper methods. 501 // 502 503 private static boolean isValid(int xPos, int yPos, int countX, int countY) { 504 return (0 <= xPos && xPos < countX && 0 <= yPos && yPos < countY); 505 } 506 507 private static int inspectMatrix(int x, int y, int cntX, int cntY, int[][] matrix) { 508 int newIconIndex = NOOP; 509 if (isValid(x, y, cntX, cntY)) { 510 if (matrix[x][y] != -1) { 511 newIconIndex = matrix[x][y]; 512 if (DEBUG) { 513 Log.v(TAG, String.format("\t\tinspect: \t[x, y]=[%d, %d] %d", 514 x, y, matrix[x][y])); 515 } 516 return newIconIndex; 517 } 518 } 519 return newIconIndex; 520 } 521 522 /** 523 * Only used for debugging. 524 */ 525 private static String getStringIndex(int index) { 526 switch(index) { 527 case NOOP: return "NOOP"; 528 case PREVIOUS_PAGE_FIRST_ITEM: return "PREVIOUS_PAGE_FIRST"; 529 case PREVIOUS_PAGE_LAST_ITEM: return "PREVIOUS_PAGE_LAST"; 530 case PREVIOUS_PAGE_RIGHT_COLUMN:return "PREVIOUS_PAGE_RIGHT_COLUMN"; 531 case CURRENT_PAGE_FIRST_ITEM: return "CURRENT_PAGE_FIRST"; 532 case CURRENT_PAGE_LAST_ITEM: return "CURRENT_PAGE_LAST"; 533 case NEXT_PAGE_FIRST_ITEM: return "NEXT_PAGE_FIRST"; 534 case NEXT_PAGE_LEFT_COLUMN: return "NEXT_PAGE_LEFT_COLUMN"; 535 case ALL_APPS_COLUMN: return "ALL_APPS_COLUMN"; 536 default: 537 return Integer.toString(index); 538 } 539 } 540 541 /** 542 * Only used for debugging. 543 */ 544 private static void printMatrix(int[][] matrix) { 545 Log.v(TAG, "\tprintMap:"); 546 int m = matrix.length; 547 int n = matrix[0].length; 548 549 for (int j=0; j < n; j++) { 550 String colY = "\t\t"; 551 for (int i=0; i < m; i++) { 552 colY += String.format("%3d",matrix[i][j]); 553 } 554 Log.v(TAG, colY); 555 } 556 } 557 558 /** 559 * @param edgeColumn the column of the new icon. either {@link #NEXT_PAGE_LEFT_COLUMN} or 560 * {@link #NEXT_PAGE_RIGHT_COLUMN} 561 * @return the view adjacent to {@param oldView} in the {@param nextPage} of the folder. 562 */ 563 public static View getAdjacentChildInNextFolderPage( 564 ShortcutAndWidgetContainer nextPage, View oldView, int edgeColumn) { 565 final int newRow = ((CellLayout.LayoutParams) oldView.getLayoutParams()).cellY; 566 567 int column = (edgeColumn == NEXT_PAGE_LEFT_COLUMN) ^ nextPage.invertLayoutHorizontally() 568 ? 0 : (((CellLayout) nextPage.getParent()).getCountX() - 1); 569 570 for (; column >= 0; column--) { 571 for (int row = newRow; row >= 0; row--) { 572 View newView = nextPage.getChildAt(column, row); 573 if (newView != null) { 574 return newView; 575 } 576 } 577 } 578 return null; 579 } 580 } 581