Home | History | Annotate | Download | only in util
      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.Launcher;
     27 import com.android.launcher3.LauncherAppState;
     28 import com.android.launcher3.ShortcutAndWidgetContainer;
     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     // 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