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