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