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