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