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