FocusHelper.java revision 18bfaafd3da45dce7b5f73eaa1665f228353338c
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; 18 19import android.content.res.Configuration; 20import android.util.Log; 21import android.view.KeyEvent; 22import android.view.SoundEffectConstants; 23import android.view.View; 24import android.view.ViewGroup; 25 26import com.android.launcher3.FocusHelper.PagedViewKeyListener; 27import com.android.launcher3.util.FocusLogic; 28 29/** 30 * A keyboard listener we set on all the workspace icons. 31 */ 32class IconKeyEventListener implements View.OnKeyListener { 33 @Override 34 public boolean onKey(View v, int keyCode, KeyEvent event) { 35 return FocusHelper.handleIconKeyEvent(v, keyCode, event); 36 } 37} 38 39/** 40 * A keyboard listener we set on all the workspace icons. 41 */ 42class FolderKeyEventListener implements View.OnKeyListener { 43 @Override 44 public boolean onKey(View v, int keyCode, KeyEvent event) { 45 return FocusHelper.handleFolderKeyEvent(v, keyCode, event); 46 } 47} 48 49/** 50 * A keyboard listener we set on all the hotseat buttons. 51 */ 52class HotseatIconKeyEventListener implements View.OnKeyListener { 53 @Override 54 public boolean onKey(View v, int keyCode, KeyEvent event) { 55 return FocusHelper.handleHotseatButtonKeyEvent(v, keyCode, event); 56 } 57} 58 59public class FocusHelper { 60 61 private static final String TAG = "FocusHelper"; 62 private static final boolean DEBUG = false; 63 64 // 65 // Key code handling methods. 66 // 67 68 /** 69 * A keyboard listener for scrollable folders 70 */ 71 public static class PagedFolderKeyEventListener extends PagedViewKeyListener { 72 73 private final Folder mFolder; 74 75 public PagedFolderKeyEventListener(Folder folder) { 76 mFolder = folder; 77 } 78 79 @Override 80 public void handleNoopKey(int keyCode, View v) { 81 if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { 82 mFolder.mFolderName.requestFocus(); 83 playSoundEffect(keyCode, v); 84 } 85 } 86 } 87 88 /** 89 * Handles key events in the all apps screen. 90 */ 91 public static class PagedViewKeyListener implements View.OnKeyListener { 92 93 @Override 94 public boolean onKey(View v, int keyCode, KeyEvent e) { 95 boolean consume = FocusLogic.shouldConsume(keyCode); 96 if (e.getAction() == KeyEvent.ACTION_UP) { 97 return consume; 98 } 99 if (DEBUG) { 100 Log.v(TAG, String.format("Handle ALL APPS keyevent=[%s].", 101 KeyEvent.keyCodeToString(keyCode))); 102 } 103 104 // Initialize variables. 105 ViewGroup parentLayout; 106 ViewGroup itemContainer; 107 int countX; 108 int countY; 109 if (v.getParent() instanceof ShortcutAndWidgetContainer) { 110 itemContainer = (ViewGroup) v.getParent(); 111 parentLayout = (ViewGroup) itemContainer.getParent(); 112 countX = ((CellLayout) parentLayout).getCountX(); 113 countY = ((CellLayout) parentLayout).getCountY(); 114 } else { 115 if (LauncherAppState.isDogfoodBuild()) { 116 throw new IllegalStateException("Parent of the focused item is not supported."); 117 } else { 118 return false; 119 } 120 } 121 122 final int iconIndex = itemContainer.indexOfChild(v); 123 final PagedView container = (PagedView) parentLayout.getParent(); 124 final int pageIndex = container.indexToPage(container.indexOfChild(parentLayout)); 125 final int pageCount = container.getChildCount(); 126 ViewGroup newParent = null; 127 View child = null; 128 // TODO(hyunyoungs): this matrix is not applicable on the last page. 129 int[][] matrix = FocusLogic.createFullMatrix(countX, countY, true); 130 131 // Process focus. 132 int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, countY, matrix, 133 iconIndex, pageIndex, pageCount); 134 if (newIconIndex == FocusLogic.NOOP) { 135 handleNoopKey(keyCode, v); 136 return consume; 137 } 138 switch (newIconIndex) { 139 case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN: 140 newParent = getAppsCustomizePage(container, pageIndex -1); 141 if (newParent != null) { 142 int row = FocusLogic.findRow(matrix, iconIndex); 143 container.snapToPage(pageIndex - 1); 144 // no need to create a new matrix. 145 child = newParent.getChildAt(matrix[countX-1][row]); 146 } 147 break; 148 case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM: 149 newParent = getAppsCustomizePage(container, pageIndex - 1); 150 if (newParent != null) { 151 container.snapToPage(pageIndex - 1); 152 child = newParent.getChildAt(0); 153 } 154 break; 155 case FocusLogic.PREVIOUS_PAGE_LAST_ITEM: 156 newParent = getAppsCustomizePage(container, pageIndex - 1); 157 if (newParent != null) { 158 container.snapToPage(pageIndex - 1); 159 child = newParent.getChildAt(newParent.getChildCount() - 1); 160 } 161 break; 162 case FocusLogic.NEXT_PAGE_FIRST_ITEM: 163 newParent = getAppsCustomizePage(container, pageIndex + 1); 164 if (newParent != null) { 165 container.snapToPage(pageIndex + 1); 166 child = newParent.getChildAt(0); 167 } 168 break; 169 case FocusLogic.NEXT_PAGE_LEFT_COLUMN: 170 newParent = getAppsCustomizePage(container, pageIndex + 1); 171 if (newParent != null) { 172 container.snapToPage(pageIndex + 1); 173 int row = FocusLogic.findRow(matrix, iconIndex); 174 child = newParent.getChildAt(matrix[0][row]); 175 } 176 break; 177 case FocusLogic.CURRENT_PAGE_FIRST_ITEM: 178 child = container.getChildAt(0); 179 break; 180 case FocusLogic.CURRENT_PAGE_LAST_ITEM: 181 child = itemContainer.getChildAt(itemContainer.getChildCount() - 1); 182 break; 183 default: // Go to some item on the current page. 184 child = itemContainer.getChildAt(newIconIndex); 185 break; 186 } 187 if (child != null) { 188 child.requestFocus(); 189 playSoundEffect(keyCode, v); 190 } else { 191 handleNoopKey(keyCode, v); 192 } 193 return consume; 194 } 195 196 public void handleNoopKey(int keyCode, View v) { } 197 } 198 199 /** 200 * Handles key events in the workspace hot seat (bottom of the screen). 201 * <p>Currently we don't special case for the phone UI in different orientations, even though 202 * the hotseat is on the side in landscape mode. This is to ensure that accessibility 203 * consistency is maintained across rotations. 204 */ 205 static boolean handleHotseatButtonKeyEvent(View v, int keyCode, KeyEvent e) { 206 boolean consume = FocusLogic.shouldConsume(keyCode); 207 if (e.getAction() == KeyEvent.ACTION_UP || !consume) { 208 return consume; 209 } 210 211 LauncherAppState app = LauncherAppState.getInstance(); 212 DeviceProfile profile = app.getDynamicGrid().getDeviceProfile(); 213 if (DEBUG) { 214 Log.v(TAG, String.format( 215 "Handle HOTSEAT BUTTONS keyevent=[%s] on hotseat buttons, isVertical=%s", 216 KeyEvent.keyCodeToString(keyCode), profile.isVerticalBarLayout())); 217 } 218 219 // Initialize the variables. 220 final ShortcutAndWidgetContainer hotseatParent = (ShortcutAndWidgetContainer) v.getParent(); 221 final CellLayout hotseatLayout = (CellLayout) hotseatParent.getParent(); 222 Hotseat hotseat = (Hotseat) hotseatLayout.getParent(); 223 224 Workspace workspace = (Workspace) v.getRootView().findViewById(R.id.workspace); 225 int pageIndex = workspace.getCurrentPage(); 226 int pageCount = workspace.getChildCount(); 227 int countX = -1; 228 int countY = -1; 229 int iconIndex = findIndexOfView(hotseatParent, v); 230 int iconRank = ((CellLayout.LayoutParams) hotseatLayout.getShortcutsAndWidgets() 231 .getChildAt(iconIndex).getLayoutParams()).cellX; 232 233 final CellLayout iconLayout = (CellLayout) workspace.getChildAt(pageIndex); 234 final ViewGroup iconParent = iconLayout.getShortcutsAndWidgets(); 235 236 ViewGroup parent = null; 237 int[][] matrix = null; 238 239 if (keyCode == KeyEvent.KEYCODE_DPAD_UP && 240 !profile.isVerticalBarLayout()) { 241 matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, 242 true /* hotseat horizontal */, hotseat.getAllAppsButtonRank(), 243 iconRank == hotseat.getAllAppsButtonRank() /* include all apps icon */); 244 iconIndex += iconParent.getChildCount(); 245 countX = iconLayout.getCountX(); 246 countY = iconLayout.getCountY() + hotseatLayout.getCountY(); 247 parent = iconParent; 248 } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && 249 profile.isVerticalBarLayout()) { 250 matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, 251 false /* hotseat horizontal */, hotseat.getAllAppsButtonRank(), 252 iconRank == hotseat.getAllAppsButtonRank() /* include all apps icon */); 253 iconIndex += iconParent.getChildCount(); 254 countX = iconLayout.getCountX() + hotseatLayout.getCountX(); 255 countY = iconLayout.getCountY(); 256 parent = iconParent; 257 } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && 258 profile.isVerticalBarLayout()) { 259 keyCode = KeyEvent.KEYCODE_PAGE_DOWN; 260 }else { 261 // For other KEYCODE_DPAD_LEFT and KEYCODE_DPAD_RIGHT navigation, do not use the 262 // matrix extended with hotseat. 263 matrix = FocusLogic.createSparseMatrix(hotseatLayout); 264 countX = hotseatLayout.getCountX(); 265 countY = hotseatLayout.getCountY(); 266 parent = hotseatParent; 267 } 268 269 // Process the focus. 270 int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, countY, matrix, 271 iconIndex, pageIndex, pageCount); 272 273 View newIcon = null; 274 if (newIconIndex == FocusLogic.NEXT_PAGE_FIRST_ITEM) { 275 parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); 276 newIcon = parent.getChildAt(0); 277 // TODO(hyunyoungs): handle cases where the child is not an icon but 278 // a folder or a widget. 279 workspace.snapToPage(pageIndex + 1); 280 } 281 if (parent == iconParent && newIconIndex >= iconParent.getChildCount()) { 282 newIconIndex -= iconParent.getChildCount(); 283 } 284 if (parent != null) { 285 if (newIcon == null && newIconIndex >=0) { 286 newIcon = parent.getChildAt(newIconIndex); 287 } 288 if (newIcon != null) { 289 newIcon.requestFocus(); 290 playSoundEffect(keyCode, v); 291 } 292 } 293 return consume; 294 } 295 296 /** 297 * Handles key events in a workspace containing icons. 298 */ 299 static boolean handleIconKeyEvent(View v, int keyCode, KeyEvent e) { 300 boolean consume = FocusLogic.shouldConsume(keyCode); 301 if (e.getAction() == KeyEvent.ACTION_UP || !consume) { 302 return consume; 303 } 304 305 LauncherAppState app = LauncherAppState.getInstance(); 306 DeviceProfile profile = app.getDynamicGrid().getDeviceProfile(); 307 308 if (DEBUG) { 309 Log.v(TAG, String.format("Handle WORKSPACE ICONS keyevent=[%s] isVerticalBar=%s", 310 KeyEvent.keyCodeToString(keyCode), profile.isVerticalBarLayout())); 311 } 312 313 // Initialize the variables. 314 ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent(); 315 CellLayout iconLayout = (CellLayout) parent.getParent(); 316 final Workspace workspace = (Workspace) iconLayout.getParent(); 317 final ViewGroup launcher = (ViewGroup) workspace.getParent(); 318 final ViewGroup tabs = (ViewGroup) launcher.findViewById(R.id.search_drop_target_bar); 319 final Hotseat hotseat = (Hotseat) launcher.findViewById(R.id.hotseat); 320 int pageIndex = workspace.indexOfChild(iconLayout); 321 int pageCount = workspace.getChildCount(); 322 int countX = iconLayout.getCountX(); 323 int countY = iconLayout.getCountY(); 324 final int iconIndex = findIndexOfView(parent, v); 325 326 CellLayout hotseatLayout = (CellLayout) hotseat.getChildAt(0); 327 ShortcutAndWidgetContainer hotseatParent = hotseatLayout.getShortcutsAndWidgets(); 328 int[][] matrix; 329 330 // KEYCODE_DPAD_DOWN in portrait (KEYCODE_DPAD_RIGHT in landscape) is the only key allowed 331 // to take a user to the hotseat. For other dpad navigation, do not use the matrix extended 332 // with the hotseat. 333 if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN && !profile.isVerticalBarLayout()) { 334 matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, true /* horizontal */, 335 hotseat.getAllAppsButtonRank(), false /* all apps icon is ignored */); 336 countY = countY + 1; 337 } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && 338 profile.isVerticalBarLayout()) { 339 matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, false /* horizontal */, 340 hotseat.getAllAppsButtonRank(), false /* all apps icon is ignored */); 341 countX = countX + 1; 342 } else if (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) { 343 workspace.removeWorkspaceItem(v); 344 return consume; 345 } else { 346 matrix = FocusLogic.createSparseMatrix(iconLayout); 347 } 348 349 // Process the focus. 350 int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, countY, matrix, 351 iconIndex, pageIndex, pageCount); 352 View newIcon = null; 353 switch (newIconIndex) { 354 case FocusLogic.NOOP: 355 if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { 356 newIcon = tabs; 357 } 358 break; 359 case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN: 360 int row = FocusLogic.findRow(matrix, iconIndex); 361 parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); 362 if (parent != null) { 363 iconLayout = (CellLayout) parent.getParent(); 364 matrix = FocusLogic.createSparseMatrix(iconLayout, 365 iconLayout.getCountX(), row); 366 newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX + 1, countY, matrix, 367 FocusLogic.PIVOT, pageIndex - 1, pageCount); 368 newIcon = parent.getChildAt(newIconIndex); 369 } 370 break; 371 case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM: 372 parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); 373 newIcon = parent.getChildAt(0); 374 workspace.snapToPage(pageIndex - 1); 375 break; 376 case FocusLogic.PREVIOUS_PAGE_LAST_ITEM: 377 parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); 378 newIcon = parent.getChildAt(parent.getChildCount() - 1); 379 workspace.snapToPage(pageIndex - 1); 380 break; 381 case FocusLogic.NEXT_PAGE_FIRST_ITEM: 382 parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); 383 newIcon = parent.getChildAt(0); 384 workspace.snapToPage(pageIndex + 1); 385 break; 386 case FocusLogic.NEXT_PAGE_LEFT_COLUMN: 387 row = FocusLogic.findRow(matrix, iconIndex); 388 parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); 389 if (parent != null) { 390 iconLayout = (CellLayout) parent.getParent(); 391 matrix = FocusLogic.createSparseMatrix(iconLayout, -1, row); 392 newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX + 1, countY, matrix, 393 FocusLogic.PIVOT, pageIndex, pageCount); 394 newIcon = parent.getChildAt(newIconIndex); 395 } 396 break; 397 case FocusLogic.CURRENT_PAGE_FIRST_ITEM: 398 newIcon = parent.getChildAt(0); 399 break; 400 case FocusLogic.CURRENT_PAGE_LAST_ITEM: 401 newIcon = parent.getChildAt(parent.getChildCount() - 1); 402 break; 403 default: 404 // current page, some item. 405 if (0 <= newIconIndex && newIconIndex < parent.getChildCount()) { 406 newIcon = parent.getChildAt(newIconIndex); 407 } else if (parent.getChildCount() <= newIconIndex && 408 newIconIndex < parent.getChildCount() + hotseatParent.getChildCount()) { 409 newIcon = hotseatParent.getChildAt(newIconIndex - parent.getChildCount()); 410 } 411 break; 412 } 413 if (newIcon != null) { 414 newIcon.requestFocus(); 415 playSoundEffect(keyCode, v); 416 } 417 return consume; 418 } 419 420 /** 421 * Handles key events for items in a Folder. 422 */ 423 static boolean handleFolderKeyEvent(View v, int keyCode, KeyEvent e) { 424 boolean consume = FocusLogic.shouldConsume(keyCode); 425 if (e.getAction() == KeyEvent.ACTION_UP || !consume) { 426 return consume; 427 } 428 if (DEBUG) { 429 Log.v(TAG, String.format("Handle FOLDER keyevent=[%s].", 430 KeyEvent.keyCodeToString(keyCode))); 431 } 432 433 // Initialize the variables. 434 ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent(); 435 final CellLayout layout = (CellLayout) parent.getParent(); 436 final Folder folder = (Folder) layout.getParent().getParent(); 437 View title = folder.mFolderName; 438 Workspace workspace = (Workspace) v.getRootView().findViewById(R.id.workspace); 439 final int countX = layout.getCountX(); 440 final int countY = layout.getCountY(); 441 final int iconIndex = findIndexOfView(parent, v); 442 int pageIndex = workspace.indexOfChild(layout); 443 int pageCount = workspace.getChildCount(); 444 int[][] map = FocusLogic.createFullMatrix(countX, countY, true /* incremental order */); 445 446 // Process the focus. 447 int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, countY, map, iconIndex, 448 pageIndex, pageCount); 449 View newIcon = null; 450 switch (newIconIndex) { 451 case FocusLogic.NOOP: 452 if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { 453 newIcon = title; 454 } 455 break; 456 case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM: 457 case FocusLogic.PREVIOUS_PAGE_LAST_ITEM: 458 case FocusLogic.NEXT_PAGE_FIRST_ITEM: 459 case FocusLogic.CURRENT_PAGE_FIRST_ITEM: 460 case FocusLogic.CURRENT_PAGE_LAST_ITEM: 461 if (DEBUG) { 462 Log.v(TAG, "Page advance handling not supported on folder icons."); 463 } 464 break; 465 default: // current page some item. 466 newIcon = parent.getChildAt(newIconIndex); 467 break; 468 } 469 if (newIcon != null) { 470 newIcon.requestFocus(); 471 playSoundEffect(keyCode, v); 472 } 473 return consume; 474 } 475 476 // 477 // Helper methods. 478 // 479 480 /** 481 * Returns the Viewgroup containing page contents for the page at the index specified. 482 */ 483 private static ViewGroup getAppsCustomizePage(ViewGroup container, int index) { 484 ViewGroup page = (ViewGroup) ((PagedView) container).getPageAt(index); 485 if (page instanceof CellLayout) { 486 // There are two layers, a PagedViewCellLayout and PagedViewCellLayoutChildren 487 page = ((CellLayout) page).getShortcutsAndWidgets(); 488 } 489 return page; 490 } 491 492 /** 493 * Private helper method to get the CellLayoutChildren given a CellLayout index. 494 */ 495 private static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex( 496 ViewGroup container, int i) { 497 CellLayout parent = (CellLayout) container.getChildAt(i); 498 return parent.getShortcutsAndWidgets(); 499 } 500 501 private static int findIndexOfView(ViewGroup parent, View v) { 502 for (int i = 0; i < parent.getChildCount(); i++) { 503 if (v != null && v.equals(parent.getChildAt(i))) { 504 return i; 505 } 506 } 507 return -1; 508 } 509 510 /** 511 * Helper method to be used for playing sound effects. 512 */ 513 private static void playSoundEffect(int keyCode, View v) { 514 switch (keyCode) { 515 case KeyEvent.KEYCODE_DPAD_LEFT: 516 v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); 517 break; 518 case KeyEvent.KEYCODE_DPAD_RIGHT: 519 v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); 520 break; 521 case KeyEvent.KEYCODE_DPAD_DOWN: 522 case KeyEvent.KEYCODE_PAGE_DOWN: 523 case KeyEvent.KEYCODE_MOVE_END: 524 v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); 525 break; 526 case KeyEvent.KEYCODE_DPAD_UP: 527 case KeyEvent.KEYCODE_PAGE_UP: 528 case KeyEvent.KEYCODE_MOVE_HOME: 529 v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); 530 break; 531 default: 532 break; 533 } 534 } 535} 536