1/* 2 * Copyright (C) 2011 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.view.ViewParent; 26import android.widget.ScrollView; 27 28import java.util.ArrayList; 29import java.util.Collections; 30import java.util.Comparator; 31 32/** 33 * A keyboard listener we set on all the workspace icons. 34 */ 35class IconKeyEventListener implements View.OnKeyListener { 36 public boolean onKey(View v, int keyCode, KeyEvent event) { 37 return FocusHelper.handleIconKeyEvent(v, keyCode, event); 38 } 39} 40 41/** 42 * A keyboard listener we set on all the workspace icons. 43 */ 44class FolderKeyEventListener implements View.OnKeyListener { 45 public boolean onKey(View v, int keyCode, KeyEvent event) { 46 return FocusHelper.handleFolderKeyEvent(v, keyCode, event); 47 } 48} 49 50/** 51 * A keyboard listener we set on all the hotseat buttons. 52 */ 53class HotseatIconKeyEventListener implements View.OnKeyListener { 54 public boolean onKey(View v, int keyCode, KeyEvent event) { 55 final Configuration configuration = v.getResources().getConfiguration(); 56 return FocusHelper.handleHotseatButtonKeyEvent(v, keyCode, event, configuration.orientation); 57 } 58} 59 60public class FocusHelper { 61 /** 62 * Private helper to get the parent TabHost in the view hiearchy. 63 */ 64 private static AppsCustomizeTabHost findTabHostParent(View v) { 65 ViewParent p = v.getParent(); 66 while (p != null && !(p instanceof AppsCustomizeTabHost)) { 67 p = p.getParent(); 68 } 69 return (AppsCustomizeTabHost) p; 70 } 71 72 /** 73 * Returns the Viewgroup containing page contents for the page at the index specified. 74 */ 75 private static ViewGroup getAppsCustomizePage(ViewGroup container, int index) { 76 ViewGroup page = (ViewGroup) ((PagedView) container).getPageAt(index); 77 if (page instanceof CellLayout) { 78 // There are two layers, a PagedViewCellLayout and PagedViewCellLayoutChildren 79 page = ((CellLayout) page).getShortcutsAndWidgets(); 80 } 81 return page; 82 } 83 84 /** 85 * Handles key events in a PageViewExtendedLayout containing PagedViewWidgets. 86 */ 87 static boolean handlePagedViewGridLayoutWidgetKeyEvent(PagedViewWidget w, int keyCode, 88 KeyEvent e) { 89 90 final PagedViewGridLayout parent = (PagedViewGridLayout) w.getParent(); 91 final PagedView container = (PagedView) parent.getParent(); 92 final int widgetIndex = parent.indexOfChild(w); 93 final int widgetCount = parent.getChildCount(); 94 final int pageIndex = ((PagedView) container).indexToPage(container.indexOfChild(parent)); 95 final int pageCount = container.getChildCount(); 96 final int cellCountX = parent.getCellCountX(); 97 final int cellCountY = parent.getCellCountY(); 98 final int x = widgetIndex % cellCountX; 99 final int y = widgetIndex / cellCountX; 100 101 final int action = e.getAction(); 102 final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); 103 ViewGroup newParent = null; 104 // Now that we load items in the bg asynchronously, we can't just focus 105 // child siblings willy-nilly 106 View child = null; 107 boolean wasHandled = false; 108 switch (keyCode) { 109 case KeyEvent.KEYCODE_DPAD_LEFT: 110 if (handleKeyEvent) { 111 // Select the previous widget or the last widget on the previous page 112 if (widgetIndex > 0) { 113 parent.getChildAt(widgetIndex - 1).requestFocus(); 114 } else { 115 if (pageIndex > 0) { 116 newParent = getAppsCustomizePage(container, pageIndex - 1); 117 if (newParent != null) { 118 child = newParent.getChildAt(newParent.getChildCount() - 1); 119 if (child != null) child.requestFocus(); 120 } 121 } 122 } 123 } 124 wasHandled = true; 125 break; 126 case KeyEvent.KEYCODE_DPAD_RIGHT: 127 if (handleKeyEvent) { 128 // Select the next widget or the first widget on the next page 129 if (widgetIndex < (widgetCount - 1)) { 130 parent.getChildAt(widgetIndex + 1).requestFocus(); 131 } else { 132 if (pageIndex < (pageCount - 1)) { 133 newParent = getAppsCustomizePage(container, pageIndex + 1); 134 if (newParent != null) { 135 child = newParent.getChildAt(0); 136 if (child != null) child.requestFocus(); 137 } 138 } 139 } 140 } 141 wasHandled = true; 142 break; 143 case KeyEvent.KEYCODE_DPAD_UP: 144 if (handleKeyEvent) { 145 // Select the closest icon in the previous row, otherwise select the tab bar 146 if (y > 0) { 147 int newWidgetIndex = ((y - 1) * cellCountX) + x; 148 child = parent.getChildAt(newWidgetIndex); 149 if (child != null) child.requestFocus(); 150 } 151 } 152 wasHandled = true; 153 break; 154 case KeyEvent.KEYCODE_DPAD_DOWN: 155 if (handleKeyEvent) { 156 // Select the closest icon in the previous row, otherwise do nothing 157 if (y < (cellCountY - 1)) { 158 int newWidgetIndex = Math.min(widgetCount - 1, ((y + 1) * cellCountX) + x); 159 child = parent.getChildAt(newWidgetIndex); 160 if (child != null) child.requestFocus(); 161 } 162 } 163 wasHandled = true; 164 break; 165 case KeyEvent.KEYCODE_ENTER: 166 case KeyEvent.KEYCODE_DPAD_CENTER: 167 if (handleKeyEvent) { 168 // Simulate a click on the widget 169 View.OnClickListener clickListener = (View.OnClickListener) container; 170 clickListener.onClick(w); 171 } 172 wasHandled = true; 173 break; 174 case KeyEvent.KEYCODE_PAGE_UP: 175 if (handleKeyEvent) { 176 // Select the first item on the previous page, or the first item on this page 177 // if there is no previous page 178 if (pageIndex > 0) { 179 newParent = getAppsCustomizePage(container, pageIndex - 1); 180 if (newParent != null) { 181 child = newParent.getChildAt(0); 182 } 183 } else { 184 child = parent.getChildAt(0); 185 } 186 if (child != null) child.requestFocus(); 187 } 188 wasHandled = true; 189 break; 190 case KeyEvent.KEYCODE_PAGE_DOWN: 191 if (handleKeyEvent) { 192 // Select the first item on the next page, or the last item on this page 193 // if there is no next page 194 if (pageIndex < (pageCount - 1)) { 195 newParent = getAppsCustomizePage(container, pageIndex + 1); 196 if (newParent != null) { 197 child = newParent.getChildAt(0); 198 } 199 } else { 200 child = parent.getChildAt(widgetCount - 1); 201 } 202 if (child != null) child.requestFocus(); 203 } 204 wasHandled = true; 205 break; 206 case KeyEvent.KEYCODE_MOVE_HOME: 207 if (handleKeyEvent) { 208 // Select the first item on this page 209 child = parent.getChildAt(0); 210 if (child != null) child.requestFocus(); 211 } 212 wasHandled = true; 213 break; 214 case KeyEvent.KEYCODE_MOVE_END: 215 if (handleKeyEvent) { 216 // Select the last item on this page 217 parent.getChildAt(widgetCount - 1).requestFocus(); 218 } 219 wasHandled = true; 220 break; 221 default: break; 222 } 223 return wasHandled; 224 } 225 226 /** 227 * Handles key events in a PageViewCellLayout containing PagedViewIcons. 228 */ 229 static boolean handleAppsCustomizeKeyEvent(View v, int keyCode, KeyEvent e) { 230 ViewGroup parentLayout; 231 ViewGroup itemContainer; 232 int countX; 233 int countY; 234 if (v.getParent() instanceof ShortcutAndWidgetContainer) { 235 itemContainer = (ViewGroup) v.getParent(); 236 parentLayout = (ViewGroup) itemContainer.getParent(); 237 countX = ((CellLayout) parentLayout).getCountX(); 238 countY = ((CellLayout) parentLayout).getCountY(); 239 } else { 240 itemContainer = parentLayout = (ViewGroup) v.getParent(); 241 countX = ((PagedViewGridLayout) parentLayout).getCellCountX(); 242 countY = ((PagedViewGridLayout) parentLayout).getCellCountY(); 243 } 244 245 // Note we have an extra parent because of the 246 // PagedViewCellLayout/PagedViewCellLayoutChildren relationship 247 final PagedView container = (PagedView) parentLayout.getParent(); 248 final int iconIndex = itemContainer.indexOfChild(v); 249 final int itemCount = itemContainer.getChildCount(); 250 final int pageIndex = ((PagedView) container).indexToPage(container.indexOfChild(parentLayout)); 251 final int pageCount = container.getChildCount(); 252 253 final int x = iconIndex % countX; 254 final int y = iconIndex / countX; 255 256 final int action = e.getAction(); 257 final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); 258 ViewGroup newParent = null; 259 // Side pages do not always load synchronously, so check before focusing child siblings 260 // willy-nilly 261 View child = null; 262 boolean wasHandled = false; 263 switch (keyCode) { 264 case KeyEvent.KEYCODE_DPAD_LEFT: 265 if (handleKeyEvent) { 266 // Select the previous icon or the last icon on the previous page 267 if (iconIndex > 0) { 268 itemContainer.getChildAt(iconIndex - 1).requestFocus(); 269 v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); 270 } else { 271 if (pageIndex > 0) { 272 newParent = getAppsCustomizePage(container, pageIndex - 1); 273 if (newParent != null) { 274 container.snapToPage(pageIndex - 1); 275 child = newParent.getChildAt(newParent.getChildCount() - 1); 276 if (child != null) { 277 child.requestFocus(); 278 v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); 279 } 280 } 281 } 282 } 283 } 284 wasHandled = true; 285 break; 286 case KeyEvent.KEYCODE_DPAD_RIGHT: 287 if (handleKeyEvent) { 288 // Select the next icon or the first icon on the next page 289 if (iconIndex < (itemCount - 1)) { 290 itemContainer.getChildAt(iconIndex + 1).requestFocus(); 291 v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); 292 } else { 293 if (pageIndex < (pageCount - 1)) { 294 newParent = getAppsCustomizePage(container, pageIndex + 1); 295 if (newParent != null) { 296 container.snapToPage(pageIndex + 1); 297 child = newParent.getChildAt(0); 298 if (child != null) { 299 child.requestFocus(); 300 v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); 301 } 302 } 303 } 304 } 305 } 306 wasHandled = true; 307 break; 308 case KeyEvent.KEYCODE_DPAD_UP: 309 if (handleKeyEvent) { 310 // Select the closest icon in the previous row, otherwise select the tab bar 311 if (y > 0) { 312 int newiconIndex = ((y - 1) * countX) + x; 313 itemContainer.getChildAt(newiconIndex).requestFocus(); 314 v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); 315 } 316 } 317 wasHandled = true; 318 break; 319 case KeyEvent.KEYCODE_DPAD_DOWN: 320 if (handleKeyEvent) { 321 // Select the closest icon in the next row, otherwise do nothing 322 if (y < (countY - 1)) { 323 int newiconIndex = Math.min(itemCount - 1, ((y + 1) * countX) + x); 324 int newIconY = newiconIndex / countX; 325 if (newIconY != y) { 326 itemContainer.getChildAt(newiconIndex).requestFocus(); 327 v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); 328 } 329 } 330 } 331 wasHandled = true; 332 break; 333 case KeyEvent.KEYCODE_PAGE_UP: 334 if (handleKeyEvent) { 335 // Select the first icon on the previous page, or the first icon on this page 336 // if there is no previous page 337 if (pageIndex > 0) { 338 newParent = getAppsCustomizePage(container, pageIndex - 1); 339 if (newParent != null) { 340 container.snapToPage(pageIndex - 1); 341 child = newParent.getChildAt(0); 342 if (child != null) { 343 child.requestFocus(); 344 v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); 345 } 346 } 347 } else { 348 itemContainer.getChildAt(0).requestFocus(); 349 v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); 350 } 351 } 352 wasHandled = true; 353 break; 354 case KeyEvent.KEYCODE_PAGE_DOWN: 355 if (handleKeyEvent) { 356 // Select the first icon on the next page, or the last icon on this page 357 // if there is no next page 358 if (pageIndex < (pageCount - 1)) { 359 newParent = getAppsCustomizePage(container, pageIndex + 1); 360 if (newParent != null) { 361 container.snapToPage(pageIndex + 1); 362 child = newParent.getChildAt(0); 363 if (child != null) { 364 child.requestFocus(); 365 v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); 366 } 367 } 368 } else { 369 itemContainer.getChildAt(itemCount - 1).requestFocus(); 370 v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); 371 } 372 } 373 wasHandled = true; 374 break; 375 case KeyEvent.KEYCODE_MOVE_HOME: 376 if (handleKeyEvent) { 377 // Select the first icon on this page 378 itemContainer.getChildAt(0).requestFocus(); 379 v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); 380 } 381 wasHandled = true; 382 break; 383 case KeyEvent.KEYCODE_MOVE_END: 384 if (handleKeyEvent) { 385 // Select the last icon on this page 386 itemContainer.getChildAt(itemCount - 1).requestFocus(); 387 v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); 388 } 389 wasHandled = true; 390 break; 391 default: break; 392 } 393 return wasHandled; 394 } 395 396 /** 397 * Handles key events in the tab widget. 398 */ 399 static boolean handleTabKeyEvent(AccessibleTabView v, int keyCode, KeyEvent e) { 400 if (!LauncherAppState.getInstance().isScreenLarge()) return false; 401 402 final FocusOnlyTabWidget parent = (FocusOnlyTabWidget) v.getParent(); 403 final AppsCustomizeTabHost tabHost = findTabHostParent(parent); 404 final ViewGroup contents = tabHost.getContent(); 405 final int tabCount = parent.getTabCount(); 406 final int tabIndex = parent.getChildTabIndex(v); 407 408 final int action = e.getAction(); 409 final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); 410 boolean wasHandled = false; 411 switch (keyCode) { 412 case KeyEvent.KEYCODE_DPAD_LEFT: 413 if (handleKeyEvent) { 414 // Select the previous tab 415 if (tabIndex > 0) { 416 parent.getChildTabViewAt(tabIndex - 1).requestFocus(); 417 } 418 } 419 wasHandled = true; 420 break; 421 case KeyEvent.KEYCODE_DPAD_RIGHT: 422 if (handleKeyEvent) { 423 // Select the next tab, or if the last tab has a focus right id, select that 424 if (tabIndex < (tabCount - 1)) { 425 parent.getChildTabViewAt(tabIndex + 1).requestFocus(); 426 } else { 427 if (v.getNextFocusRightId() != View.NO_ID) { 428 tabHost.findViewById(v.getNextFocusRightId()).requestFocus(); 429 } 430 } 431 } 432 wasHandled = true; 433 break; 434 case KeyEvent.KEYCODE_DPAD_UP: 435 // Do nothing 436 wasHandled = true; 437 break; 438 case KeyEvent.KEYCODE_DPAD_DOWN: 439 if (handleKeyEvent) { 440 // Select the content view 441 contents.requestFocus(); 442 } 443 wasHandled = true; 444 break; 445 default: break; 446 } 447 return wasHandled; 448 } 449 450 /** 451 * Handles key events in the workspace hotseat (bottom of the screen). 452 */ 453 static boolean handleHotseatButtonKeyEvent(View v, int keyCode, KeyEvent e, int orientation) { 454 ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent(); 455 final CellLayout layout = (CellLayout) parent.getParent(); 456 457 // NOTE: currently we don't special case for the phone UI in different 458 // orientations, even though the hotseat is on the side in landscape mode. This 459 // is to ensure that accessibility consistency is maintained across rotations. 460 final int action = e.getAction(); 461 final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); 462 boolean wasHandled = false; 463 switch (keyCode) { 464 case KeyEvent.KEYCODE_DPAD_LEFT: 465 if (handleKeyEvent) { 466 ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent); 467 int myIndex = views.indexOf(v); 468 // Select the previous button, otherwise do nothing 469 if (myIndex > 0) { 470 views.get(myIndex - 1).requestFocus(); 471 v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); 472 } 473 } 474 wasHandled = true; 475 break; 476 case KeyEvent.KEYCODE_DPAD_RIGHT: 477 if (handleKeyEvent) { 478 ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent); 479 int myIndex = views.indexOf(v); 480 // Select the next button, otherwise do nothing 481 if (myIndex < views.size() - 1) { 482 views.get(myIndex + 1).requestFocus(); 483 v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); 484 } 485 } 486 wasHandled = true; 487 break; 488 case KeyEvent.KEYCODE_DPAD_UP: 489 if (handleKeyEvent) { 490 final Workspace workspace = (Workspace) 491 v.getRootView().findViewById(R.id.workspace); 492 if (workspace != null) { 493 int pageIndex = workspace.getCurrentPage(); 494 CellLayout topLayout = (CellLayout) workspace.getChildAt(pageIndex); 495 ShortcutAndWidgetContainer children = topLayout.getShortcutsAndWidgets(); 496 final View newIcon = getIconInDirection(layout, children, -1, 1); 497 // Select the first bubble text view in the current page of the workspace 498 if (newIcon != null) { 499 newIcon.requestFocus(); 500 v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); 501 } else { 502 workspace.requestFocus(); 503 } 504 } 505 } 506 wasHandled = true; 507 break; 508 case KeyEvent.KEYCODE_DPAD_DOWN: 509 // Do nothing 510 wasHandled = true; 511 break; 512 default: break; 513 } 514 return wasHandled; 515 } 516 517 /** 518 * Private helper method to get the CellLayoutChildren given a CellLayout index. 519 */ 520 private static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex( 521 ViewGroup container, int i) { 522 CellLayout parent = (CellLayout) container.getChildAt(i); 523 return parent.getShortcutsAndWidgets(); 524 } 525 526 /** 527 * Private helper method to sort all the CellLayout children in order of their (x,y) spatially 528 * from top left to bottom right. 529 */ 530 private static ArrayList<View> getCellLayoutChildrenSortedSpatially(CellLayout layout, 531 ViewGroup parent) { 532 // First we order each the CellLayout children by their x,y coordinates 533 final int cellCountX = layout.getCountX(); 534 final int count = parent.getChildCount(); 535 ArrayList<View> views = new ArrayList<View>(); 536 for (int j = 0; j < count; ++j) { 537 views.add(parent.getChildAt(j)); 538 } 539 Collections.sort(views, new Comparator<View>() { 540 @Override 541 public int compare(View lhs, View rhs) { 542 CellLayout.LayoutParams llp = (CellLayout.LayoutParams) lhs.getLayoutParams(); 543 CellLayout.LayoutParams rlp = (CellLayout.LayoutParams) rhs.getLayoutParams(); 544 int lvIndex = (llp.cellY * cellCountX) + llp.cellX; 545 int rvIndex = (rlp.cellY * cellCountX) + rlp.cellX; 546 return lvIndex - rvIndex; 547 } 548 }); 549 return views; 550 } 551 /** 552 * Private helper method to find the index of the next BubbleTextView or FolderIcon in the 553 * direction delta. 554 * 555 * @param delta either -1 or 1 depending on the direction we want to search 556 */ 557 private static View findIndexOfIcon(ArrayList<View> views, int i, int delta) { 558 // Then we find the next BubbleTextView offset by delta from i 559 final int count = views.size(); 560 int newI = i + delta; 561 while (0 <= newI && newI < count) { 562 View newV = views.get(newI); 563 if (newV instanceof BubbleTextView || newV instanceof FolderIcon) { 564 return newV; 565 } 566 newI += delta; 567 } 568 return null; 569 } 570 private static View getIconInDirection(CellLayout layout, ViewGroup parent, int i, 571 int delta) { 572 final ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent); 573 return findIndexOfIcon(views, i, delta); 574 } 575 private static View getIconInDirection(CellLayout layout, ViewGroup parent, View v, 576 int delta) { 577 final ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent); 578 return findIndexOfIcon(views, views.indexOf(v), delta); 579 } 580 /** 581 * Private helper method to find the next closest BubbleTextView or FolderIcon in the direction 582 * delta on the next line. 583 * 584 * @param delta either -1 or 1 depending on the line and direction we want to search 585 */ 586 private static View getClosestIconOnLine(CellLayout layout, ViewGroup parent, View v, 587 int lineDelta) { 588 final ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent); 589 final CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams(); 590 final int cellCountY = layout.getCountY(); 591 final int row = lp.cellY; 592 final int newRow = row + lineDelta; 593 if (0 <= newRow && newRow < cellCountY) { 594 float closestDistance = Float.MAX_VALUE; 595 int closestIndex = -1; 596 int index = views.indexOf(v); 597 int endIndex = (lineDelta < 0) ? -1 : views.size(); 598 while (index != endIndex) { 599 View newV = views.get(index); 600 CellLayout.LayoutParams tmpLp = (CellLayout.LayoutParams) newV.getLayoutParams(); 601 boolean satisfiesRow = (lineDelta < 0) ? (tmpLp.cellY < row) : (tmpLp.cellY > row); 602 if (satisfiesRow && 603 (newV instanceof BubbleTextView || newV instanceof FolderIcon)) { 604 float tmpDistance = (float) Math.sqrt(Math.pow(tmpLp.cellX - lp.cellX, 2) + 605 Math.pow(tmpLp.cellY - lp.cellY, 2)); 606 if (tmpDistance < closestDistance) { 607 closestIndex = index; 608 closestDistance = tmpDistance; 609 } 610 } 611 if (index <= endIndex) { 612 ++index; 613 } else { 614 --index; 615 } 616 } 617 if (closestIndex > -1) { 618 return views.get(closestIndex); 619 } 620 } 621 return null; 622 } 623 624 /** 625 * Handles key events in a Workspace containing. 626 */ 627 static boolean handleIconKeyEvent(View v, int keyCode, KeyEvent e) { 628 ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent(); 629 final CellLayout layout = (CellLayout) parent.getParent(); 630 final Workspace workspace = (Workspace) layout.getParent(); 631 final ViewGroup launcher = (ViewGroup) workspace.getParent(); 632 final ViewGroup tabs = (ViewGroup) launcher.findViewById(R.id.search_drop_target_bar); 633 final ViewGroup hotseat = (ViewGroup) launcher.findViewById(R.id.hotseat); 634 int pageIndex = workspace.indexOfChild(layout); 635 int pageCount = workspace.getChildCount(); 636 637 final int action = e.getAction(); 638 final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); 639 boolean wasHandled = false; 640 switch (keyCode) { 641 case KeyEvent.KEYCODE_DPAD_LEFT: 642 if (handleKeyEvent) { 643 // Select the previous icon or the last icon on the previous page if possible 644 View newIcon = getIconInDirection(layout, parent, v, -1); 645 if (newIcon != null) { 646 newIcon.requestFocus(); 647 v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); 648 } else { 649 if (pageIndex > 0) { 650 parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); 651 newIcon = getIconInDirection(layout, parent, 652 parent.getChildCount(), -1); 653 if (newIcon != null) { 654 newIcon.requestFocus(); 655 } else { 656 // Snap to the previous page 657 workspace.snapToPage(pageIndex - 1); 658 } 659 v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); 660 } 661 } 662 } 663 wasHandled = true; 664 break; 665 case KeyEvent.KEYCODE_DPAD_RIGHT: 666 if (handleKeyEvent) { 667 // Select the next icon or the first icon on the next page if possible 668 View newIcon = getIconInDirection(layout, parent, v, 1); 669 if (newIcon != null) { 670 newIcon.requestFocus(); 671 v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); 672 } else { 673 if (pageIndex < (pageCount - 1)) { 674 parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); 675 newIcon = getIconInDirection(layout, parent, -1, 1); 676 if (newIcon != null) { 677 newIcon.requestFocus(); 678 } else { 679 // Snap to the next page 680 workspace.snapToPage(pageIndex + 1); 681 } 682 v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); 683 } 684 } 685 } 686 wasHandled = true; 687 break; 688 case KeyEvent.KEYCODE_DPAD_UP: 689 if (handleKeyEvent) { 690 // Select the closest icon in the previous line, otherwise select the tab bar 691 View newIcon = getClosestIconOnLine(layout, parent, v, -1); 692 if (newIcon != null) { 693 newIcon.requestFocus(); 694 wasHandled = true; 695 } else { 696 tabs.requestFocus(); 697 } 698 v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); 699 } 700 break; 701 case KeyEvent.KEYCODE_DPAD_DOWN: 702 if (handleKeyEvent) { 703 // Select the closest icon in the next line, otherwise select the button bar 704 View newIcon = getClosestIconOnLine(layout, parent, v, 1); 705 if (newIcon != null) { 706 newIcon.requestFocus(); 707 v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); 708 wasHandled = true; 709 } else if (hotseat != null) { 710 hotseat.requestFocus(); 711 v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); 712 } 713 } 714 break; 715 case KeyEvent.KEYCODE_PAGE_UP: 716 if (handleKeyEvent) { 717 // Select the first icon on the previous page or the first icon on this page 718 // if there is no previous page 719 if (pageIndex > 0) { 720 parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); 721 View newIcon = getIconInDirection(layout, parent, -1, 1); 722 if (newIcon != null) { 723 newIcon.requestFocus(); 724 } else { 725 // Snap to the previous page 726 workspace.snapToPage(pageIndex - 1); 727 } 728 v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); 729 } else { 730 View newIcon = getIconInDirection(layout, parent, -1, 1); 731 if (newIcon != null) { 732 newIcon.requestFocus(); 733 v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); 734 } 735 } 736 } 737 wasHandled = true; 738 break; 739 case KeyEvent.KEYCODE_PAGE_DOWN: 740 if (handleKeyEvent) { 741 // Select the first icon on the next page or the last icon on this page 742 // if there is no previous page 743 if (pageIndex < (pageCount - 1)) { 744 parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); 745 View newIcon = getIconInDirection(layout, parent, -1, 1); 746 if (newIcon != null) { 747 newIcon.requestFocus(); 748 } else { 749 // Snap to the next page 750 workspace.snapToPage(pageIndex + 1); 751 } 752 v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); 753 } else { 754 View newIcon = getIconInDirection(layout, parent, 755 parent.getChildCount(), -1); 756 if (newIcon != null) { 757 newIcon.requestFocus(); 758 v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); 759 } 760 } 761 } 762 wasHandled = true; 763 break; 764 case KeyEvent.KEYCODE_MOVE_HOME: 765 if (handleKeyEvent) { 766 // Select the first icon on this page 767 View newIcon = getIconInDirection(layout, parent, -1, 1); 768 if (newIcon != null) { 769 newIcon.requestFocus(); 770 v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); 771 } 772 } 773 wasHandled = true; 774 break; 775 case KeyEvent.KEYCODE_MOVE_END: 776 if (handleKeyEvent) { 777 // Select the last icon on this page 778 View newIcon = getIconInDirection(layout, parent, 779 parent.getChildCount(), -1); 780 if (newIcon != null) { 781 newIcon.requestFocus(); 782 v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); 783 } 784 } 785 wasHandled = true; 786 break; 787 default: break; 788 } 789 return wasHandled; 790 } 791 792 /** 793 * Handles key events for items in a Folder. 794 */ 795 static boolean handleFolderKeyEvent(View v, int keyCode, KeyEvent e) { 796 ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent(); 797 final CellLayout layout = (CellLayout) parent.getParent(); 798 final ScrollView scrollView = (ScrollView) layout.getParent(); 799 final Folder folder = (Folder) scrollView.getParent(); 800 View title = folder.mFolderName; 801 802 final int action = e.getAction(); 803 final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); 804 boolean wasHandled = false; 805 switch (keyCode) { 806 case KeyEvent.KEYCODE_DPAD_LEFT: 807 if (handleKeyEvent) { 808 // Select the previous icon 809 View newIcon = getIconInDirection(layout, parent, v, -1); 810 if (newIcon != null) { 811 newIcon.requestFocus(); 812 v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); 813 } 814 } 815 wasHandled = true; 816 break; 817 case KeyEvent.KEYCODE_DPAD_RIGHT: 818 if (handleKeyEvent) { 819 // Select the next icon 820 View newIcon = getIconInDirection(layout, parent, v, 1); 821 if (newIcon != null) { 822 newIcon.requestFocus(); 823 } else { 824 title.requestFocus(); 825 } 826 v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); 827 } 828 wasHandled = true; 829 break; 830 case KeyEvent.KEYCODE_DPAD_UP: 831 if (handleKeyEvent) { 832 // Select the closest icon in the previous line 833 View newIcon = getClosestIconOnLine(layout, parent, v, -1); 834 if (newIcon != null) { 835 newIcon.requestFocus(); 836 v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); 837 } 838 } 839 wasHandled = true; 840 break; 841 case KeyEvent.KEYCODE_DPAD_DOWN: 842 if (handleKeyEvent) { 843 // Select the closest icon in the next line 844 View newIcon = getClosestIconOnLine(layout, parent, v, 1); 845 if (newIcon != null) { 846 newIcon.requestFocus(); 847 } else { 848 title.requestFocus(); 849 } 850 v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); 851 } 852 wasHandled = true; 853 break; 854 case KeyEvent.KEYCODE_MOVE_HOME: 855 if (handleKeyEvent) { 856 // Select the first icon on this page 857 View newIcon = getIconInDirection(layout, parent, -1, 1); 858 if (newIcon != null) { 859 newIcon.requestFocus(); 860 v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); 861 } 862 } 863 wasHandled = true; 864 break; 865 case KeyEvent.KEYCODE_MOVE_END: 866 if (handleKeyEvent) { 867 // Select the last icon on this page 868 View newIcon = getIconInDirection(layout, parent, 869 parent.getChildCount(), -1); 870 if (newIcon != null) { 871 newIcon.requestFocus(); 872 v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); 873 } 874 } 875 wasHandled = true; 876 break; 877 default: break; 878 } 879 return wasHandled; 880 } 881} 882