FocusHelper.java revision 31178b8237ccb6af666df60ef60c116c8afdf316
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; 25import android.widget.ScrollView; 26 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 * Handles key events in the all apps screen. 70 */ 71 static boolean handleAppsCustomizeKeyEvent(View v, int keyCode, KeyEvent e) { 72 boolean consume = FocusLogic.shouldConsume(keyCode); 73 if (e.getAction() == KeyEvent.ACTION_UP) { 74 return consume; 75 } 76 if (DEBUG) { 77 Log.v(TAG, String.format("Handle ALL APPS keyevent=[%s].", 78 KeyEvent.keyCodeToString(keyCode))); 79 } 80 81 // Initialize variables. 82 ViewGroup parentLayout; 83 ViewGroup itemContainer; 84 int countX; 85 int countY; 86 if (v.getParent() instanceof ShortcutAndWidgetContainer) { 87 itemContainer = (ViewGroup) v.getParent(); 88 parentLayout = (ViewGroup) itemContainer.getParent(); 89 countX = ((CellLayout) parentLayout).getCountX(); 90 countY = ((CellLayout) parentLayout).getCountY(); 91 } else if (v.getParent() instanceof ViewGroup) { 92 itemContainer = parentLayout = (ViewGroup) v.getParent(); 93 countX = ((PagedViewGridLayout) parentLayout).getCellCountX(); 94 countY = ((PagedViewGridLayout) parentLayout).getCellCountY(); 95 } else { 96 throw new IllegalStateException( 97 "Parent of the focused item inside all apps screen is not a supported type."); 98 } 99 final int iconIndex = itemContainer.indexOfChild(v); 100 final PagedView container = (PagedView) parentLayout.getParent(); 101 final int pageIndex = container.indexToPage(container.indexOfChild(parentLayout)); 102 final int pageCount = container.getChildCount(); 103 ViewGroup newParent = null; 104 View child = null; 105 int[][] matrix = FocusLogic.createFullMatrix(countX, countY, true); 106 107 // Process focus. 108 int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, countY, matrix, 109 iconIndex, pageIndex, pageCount); 110 if (newIconIndex == FocusLogic.NOOP) { 111 return consume; 112 } 113 switch (newIconIndex) { 114 case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM: 115 newParent = getAppsCustomizePage(container, pageIndex - 1); 116 if (newParent != null) { 117 container.snapToPage(pageIndex - 1); 118 child = newParent.getChildAt(0); 119 } 120 case FocusLogic.PREVIOUS_PAGE_LAST_ITEM: 121 newParent = getAppsCustomizePage(container, pageIndex - 1); 122 if (newParent != null) { 123 container.snapToPage(pageIndex - 1); 124 child = newParent.getChildAt(newParent.getChildCount() - 1); 125 } 126 break; 127 case FocusLogic.NEXT_PAGE_FIRST_ITEM: 128 newParent = getAppsCustomizePage(container, pageIndex + 1); 129 if (newParent != null) { 130 container.snapToPage(pageIndex + 1); 131 child = newParent.getChildAt(0); 132 } 133 break; 134 case FocusLogic.CURRENT_PAGE_FIRST_ITEM: 135 child = container.getChildAt(0); 136 break; 137 case FocusLogic.CURRENT_PAGE_LAST_ITEM: 138 child = itemContainer.getChildAt(itemContainer.getChildCount() - 1); 139 break; 140 default: // Go to some item on the current page. 141 child = itemContainer.getChildAt(newIconIndex); 142 break; 143 } 144 if (child != null) { 145 child.requestFocus(); 146 playSoundEffect(keyCode, v); 147 } 148 return consume; 149 } 150 151 /** 152 * Handles key events in the workspace hot seat (bottom of the screen). 153 * <p>Currently we don't special case for the phone UI in different orientations, even though 154 * the hotseat is on the side in landscape mode. This is to ensure that accessibility 155 * consistency is maintained across rotations. 156 */ 157 static boolean handleHotseatButtonKeyEvent(View v, int keyCode, KeyEvent e) { 158 boolean consume = FocusLogic.shouldConsume(keyCode); 159 if (e.getAction() == KeyEvent.ACTION_UP || !consume) { 160 return consume; 161 } 162 int orientation = v.getResources().getConfiguration().orientation; 163 164 if (DEBUG) { 165 Log.v(TAG, String.format( 166 "Handle HOTSEAT BUTTONS keyevent=[%s] on hotseat buttons, orientation=%d", 167 KeyEvent.keyCodeToString(keyCode), orientation)); 168 } 169 170 // Initialize the variables. 171 final ShortcutAndWidgetContainer hotseatParent = (ShortcutAndWidgetContainer) v.getParent(); 172 final CellLayout hotseatLayout = (CellLayout) hotseatParent.getParent(); 173 Hotseat hotseat = (Hotseat) hotseatLayout.getParent(); 174 175 Workspace workspace = (Workspace) v.getRootView().findViewById(R.id.workspace); 176 int pageIndex = workspace.getCurrentPage(); 177 int pageCount = workspace.getChildCount(); 178 int countX = -1; 179 int countY = -1; 180 int iconIndex = findIndexOfView(hotseatParent, v); 181 182 final CellLayout iconLayout = (CellLayout) workspace.getChildAt(pageIndex); 183 final ViewGroup iconParent = iconLayout.getShortcutsAndWidgets(); 184 185 ViewGroup parent = null; 186 int[][] matrix = null; 187 188 if (keyCode == KeyEvent.KEYCODE_DPAD_UP && 189 orientation == Configuration.ORIENTATION_PORTRAIT) { 190 matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, orientation, 191 hotseat.getAllAppsButtonRank(), true /* include all apps icon */); 192 iconIndex += iconParent.getChildCount(); 193 countX = iconLayout.getCountX(); 194 countY = iconLayout.getCountY() + hotseatLayout.getCountY(); 195 parent = iconParent; 196 } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && 197 orientation == Configuration.ORIENTATION_LANDSCAPE) { 198 matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, orientation, 199 hotseat.getAllAppsButtonRank(), true /* include all apps icon */); 200 iconIndex += iconParent.getChildCount(); 201 countX = iconLayout.getCountX() + hotseatLayout.getCountX(); 202 countY = iconLayout.getCountY(); 203 parent = iconParent; 204 } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && 205 orientation == Configuration.ORIENTATION_LANDSCAPE) { 206 keyCode = KeyEvent.KEYCODE_PAGE_DOWN; 207 }else { 208 // For other KEYCODE_DPAD_LEFT and KEYCODE_DPAD_RIGHT navigation, do not use the 209 // matrix extended with hotseat. 210 matrix = FocusLogic.createSparseMatrix(hotseatLayout); 211 countX = hotseatLayout.getCountX(); 212 countY = hotseatLayout.getCountY(); 213 parent = hotseatParent; 214 } 215 216 // Process the focus. 217 int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, countY, matrix, 218 iconIndex, pageIndex, pageCount); 219 220 View newIcon = null; 221 if (newIconIndex == FocusLogic.NEXT_PAGE_FIRST_ITEM) { 222 parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); 223 newIcon = parent.getChildAt(0); 224 // TODO(hyunyoungs): handle cases where the child is not an icon but 225 // a folder or a widget. 226 workspace.snapToPage(pageIndex + 1); 227 } 228 if (parent == iconParent && newIconIndex >= iconParent.getChildCount()) { 229 newIconIndex -= iconParent.getChildCount(); 230 } 231 if (parent != null) { 232 if (newIcon == null && newIconIndex >=0) { 233 newIcon = parent.getChildAt(newIconIndex); 234 } 235 if (newIcon != null) { 236 newIcon.requestFocus(); 237 playSoundEffect(keyCode, v); 238 } 239 } 240 return consume; 241 } 242 243 /** 244 * Handles key events in a workspace containing icons. 245 */ 246 static boolean handleIconKeyEvent(View v, int keyCode, KeyEvent e) { 247 boolean consume = FocusLogic.shouldConsume(keyCode); 248 if (e.getAction() == KeyEvent.ACTION_UP || !consume) { 249 return consume; 250 } 251 int orientation = v.getResources().getConfiguration().orientation; 252 if (DEBUG) { 253 Log.v(TAG, String.format("Handle WORKSPACE ICONS keyevent=[%s] orientation=%d", 254 KeyEvent.keyCodeToString(keyCode), orientation)); 255 } 256 257 // Initialize the variables. 258 ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent(); 259 final CellLayout iconLayout = (CellLayout) parent.getParent(); 260 final Workspace workspace = (Workspace) iconLayout.getParent(); 261 final ViewGroup launcher = (ViewGroup) workspace.getParent(); 262 final ViewGroup tabs = (ViewGroup) launcher.findViewById(R.id.search_drop_target_bar); 263 final Hotseat hotseat = (Hotseat) launcher.findViewById(R.id.hotseat); 264 int pageIndex = workspace.indexOfChild(iconLayout); 265 int pageCount = workspace.getChildCount(); 266 int countX = iconLayout.getCountX(); 267 int countY = iconLayout.getCountY(); 268 final int iconIndex = findIndexOfView(parent, v); 269 270 CellLayout hotseatLayout = (CellLayout) hotseat.getChildAt(0); 271 ShortcutAndWidgetContainer hotseatParent = hotseatLayout.getShortcutsAndWidgets(); 272 int[][] matrix; 273 274 // KEYCODE_DPAD_DOWN in portrait (KEYCODE_DPAD_RIGHT in landscape) is the only key allowed 275 // to take a user to the hotseat. For other dpad navigation, do not use the matrix extended 276 // with the hotseat. 277 if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN && 278 orientation == Configuration.ORIENTATION_PORTRAIT) { 279 matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, orientation, 280 hotseat.getAllAppsButtonRank(), false /* all apps icon is ignored */); 281 countY = countY + 1; 282 } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && 283 orientation == Configuration.ORIENTATION_LANDSCAPE) { 284 matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, orientation, 285 hotseat.getAllAppsButtonRank(), false /* all apps icon is ignored */); 286 countX = countX + 1; 287 } else if (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) { 288 workspace.removeWorkspaceItem(v); 289 return consume; 290 } else { 291 matrix = FocusLogic.createSparseMatrix(iconLayout); 292 } 293 294 // Process the focus. 295 int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, countY, matrix, 296 iconIndex, pageIndex, pageCount); 297 View newIcon = null; 298 switch (newIconIndex) { 299 case FocusLogic.NOOP: 300 if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { 301 newIcon = tabs; 302 } 303 break; 304 case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM: 305 parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); 306 newIcon = parent.getChildAt(0); 307 workspace.snapToPage(pageIndex - 1); 308 case FocusLogic.PREVIOUS_PAGE_LAST_ITEM: 309 parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); 310 newIcon = parent.getChildAt(parent.getChildCount() - 1); 311 workspace.snapToPage(pageIndex - 1); 312 break; 313 case FocusLogic.NEXT_PAGE_FIRST_ITEM: 314 parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); 315 newIcon = parent.getChildAt(0); 316 workspace.snapToPage(pageIndex + 1); 317 break; 318 case FocusLogic.CURRENT_PAGE_FIRST_ITEM: 319 newIcon = parent.getChildAt(0); 320 break; 321 case FocusLogic.CURRENT_PAGE_LAST_ITEM: 322 newIcon = parent.getChildAt(parent.getChildCount() - 1); 323 break; 324 default: 325 // current page, some item. 326 if (0 <= newIconIndex && newIconIndex < parent.getChildCount()) { 327 newIcon = parent.getChildAt(newIconIndex); 328 } else if (parent.getChildCount() <= newIconIndex && 329 newIconIndex < parent.getChildCount() + hotseatParent.getChildCount()) { 330 newIcon = hotseatParent.getChildAt(newIconIndex - parent.getChildCount()); 331 } 332 break; 333 } 334 if (newIcon != null) { 335 newIcon.requestFocus(); 336 playSoundEffect(keyCode, v); 337 } 338 return consume; 339 } 340 341 /** 342 * Handles key events for items in a Folder. 343 */ 344 static boolean handleFolderKeyEvent(View v, int keyCode, KeyEvent e) { 345 boolean consume = FocusLogic.shouldConsume(keyCode); 346 if (e.getAction() == KeyEvent.ACTION_UP || !consume) { 347 return consume; 348 } 349 if (DEBUG) { 350 Log.v(TAG, String.format("Handle FOLDER keyevent=[%s].", 351 KeyEvent.keyCodeToString(keyCode))); 352 } 353 354 // Initialize the variables. 355 ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent(); 356 final CellLayout layout = (CellLayout) parent.getParent(); 357 final ScrollView scrollView = (ScrollView) layout.getParent(); 358 final Folder folder = (Folder) scrollView.getParent(); 359 View title = folder.mFolderName; 360 Workspace workspace = (Workspace) v.getRootView().findViewById(R.id.workspace); 361 final int countX = layout.getCountX(); 362 final int countY = layout.getCountY(); 363 final int iconIndex = findIndexOfView(parent, v); 364 int pageIndex = workspace.indexOfChild(layout); 365 int pageCount = workspace.getChildCount(); 366 int[][] map = FocusLogic.createFullMatrix(countX, countY, true /* incremental order */); 367 368 // Process the focus. 369 int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, countY, map, iconIndex, 370 pageIndex, pageCount); 371 View newIcon = null; 372 switch (newIconIndex) { 373 case FocusLogic.NOOP: 374 if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { 375 newIcon = title; 376 } 377 break; 378 case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM: 379 case FocusLogic.PREVIOUS_PAGE_LAST_ITEM: 380 case FocusLogic.NEXT_PAGE_FIRST_ITEM: 381 case FocusLogic.CURRENT_PAGE_FIRST_ITEM: 382 case FocusLogic.CURRENT_PAGE_LAST_ITEM: 383 if (DEBUG) { 384 Log.v(TAG, "Page advance handling not supported on folder icons."); 385 } 386 break; 387 default: // current page some item. 388 newIcon = parent.getChildAt(newIconIndex); 389 break; 390 } 391 if (newIcon != null) { 392 newIcon.requestFocus(); 393 playSoundEffect(keyCode, v); 394 } 395 return consume; 396 } 397 398 // 399 // Helper methods. 400 // 401 402 /** 403 * Returns the Viewgroup containing page contents for the page at the index specified. 404 */ 405 private static ViewGroup getAppsCustomizePage(ViewGroup container, int index) { 406 ViewGroup page = (ViewGroup) ((PagedView) container).getPageAt(index); 407 if (page instanceof CellLayout) { 408 // There are two layers, a PagedViewCellLayout and PagedViewCellLayoutChildren 409 page = ((CellLayout) page).getShortcutsAndWidgets(); 410 } 411 return page; 412 } 413 414 /** 415 * Private helper method to get the CellLayoutChildren given a CellLayout index. 416 */ 417 private static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex( 418 ViewGroup container, int i) { 419 CellLayout parent = (CellLayout) container.getChildAt(i); 420 return parent.getShortcutsAndWidgets(); 421 } 422 423 private static int findIndexOfView(ViewGroup parent, View v) { 424 for (int i = 0; i < parent.getChildCount(); i++) { 425 if (v != null && v.equals(parent.getChildAt(i))) { 426 return i; 427 } 428 } 429 return -1; 430 } 431 432 /** 433 * Helper method to be used for playing sound effects. 434 */ 435 private static void playSoundEffect(int keyCode, View v) { 436 switch (keyCode) { 437 case KeyEvent.KEYCODE_DPAD_LEFT: 438 v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); 439 break; 440 case KeyEvent.KEYCODE_DPAD_RIGHT: 441 v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); 442 break; 443 case KeyEvent.KEYCODE_DPAD_DOWN: 444 case KeyEvent.KEYCODE_PAGE_DOWN: 445 case KeyEvent.KEYCODE_MOVE_END: 446 v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); 447 break; 448 case KeyEvent.KEYCODE_DPAD_UP: 449 case KeyEvent.KEYCODE_PAGE_UP: 450 case KeyEvent.KEYCODE_MOVE_HOME: 451 v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); 452 break; 453 default: 454 break; 455 } 456 } 457} 458