/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.launcher2; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import android.content.res.Configuration; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.widget.TabHost; import android.widget.TabWidget; import com.android.launcher.R; /** * A keyboard listener we set on all the button bar buttons. */ class ButtonBarKeyEventListener implements View.OnKeyListener { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { return FocusHelper.handleButtonBarButtonKeyEvent(v, keyCode, event); } } /** * A keyboard listener we set on the indicator buttons. */ class IndicatorKeyEventListener implements View.OnKeyListener { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { return FocusHelper.handleIndicatorButtonKeyEvent(v, keyCode, event); } } /** * A keyboard listener we set on all the dock buttons. */ class DockKeyEventListener implements View.OnKeyListener { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { final Configuration configuration = v.getResources().getConfiguration(); return FocusHelper.handleDockButtonKeyEvent(v, keyCode, event, configuration.orientation); } } /** * A keyboard listener we set on the last tab button in AllApps to jump to then * market icon and vice versa. */ class AllAppsTabKeyEventListener implements View.OnKeyListener { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { return FocusHelper.handleAllAppsTabKeyEvent(v, keyCode, event); } } public class FocusHelper { /** * Private helper to get the parent TabHost in the view hiearchy. */ private static TabHost findTabHostParent(View v) { ViewParent p = v.getParent(); while (p != null && !(p instanceof TabHost)) { p = p.getParent(); } return (TabHost) p; } /** * Handles key events in a AllApps tab between the last tab view and the shop button. */ static boolean handleAllAppsTabKeyEvent(View v, int keyCode, KeyEvent e) { final TabHost tabHost = findTabHostParent(v); final ViewGroup contents = (ViewGroup) tabHost.findViewById(com.android.internal.R.id.tabcontent); final View shop = tabHost.findViewById(R.id.market_button); final int action = e.getAction(); final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); boolean wasHandled = false; switch (keyCode) { case KeyEvent.KEYCODE_DPAD_RIGHT: if (handleKeyEvent) { // Select the shop button if we aren't on it if (v != shop) { shop.requestFocus(); } } wasHandled = true; break; case KeyEvent.KEYCODE_DPAD_DOWN: if (handleKeyEvent) { // Select the content view (down is handled by the tab key handler otherwise) if (v == shop) { contents.requestFocus(); wasHandled = true; } } break; default: break; } return wasHandled; } /** * Private helper to determine whether a view is visible. */ private static boolean isVisible(View v) { return v.getVisibility() == View.VISIBLE; } /** * Handles key events in a PageViewExtendedLayout containing PagedViewWidgets. * To be deprecated. */ static boolean handlePagedViewWidgetKeyEvent(PagedViewWidget w, int keyCode, KeyEvent e) { if (!LauncherApplication.isScreenLarge()) return false; final PagedViewExtendedLayout parent = (PagedViewExtendedLayout) w.getParent(); final ViewGroup container = (ViewGroup) parent.getParent(); final TabHost tabHost = findTabHostParent(container); final TabWidget tabs = (TabWidget) tabHost.findViewById(com.android.internal.R.id.tabs); final int widgetIndex = parent.indexOfChild(w); final int widgetCount = parent.getChildCount(); final int pageIndex = container.indexOfChild(parent); final int pageCount = container.getChildCount(); final int action = e.getAction(); final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); PagedViewExtendedLayout newParent = null; boolean wasHandled = false; switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: if (handleKeyEvent) { // Select the previous widget or the last widget on the previous page if (widgetIndex > 0) { parent.getChildAt(widgetIndex - 1).requestFocus(); } else { if (pageIndex > 0) { newParent = (PagedViewExtendedLayout) container.getChildAt(pageIndex - 1); newParent.getChildAt(newParent.getChildCount() - 1).requestFocus(); } } } wasHandled = true; break; case KeyEvent.KEYCODE_DPAD_RIGHT: if (handleKeyEvent) { // Select the next widget or the first widget on the next page if (widgetIndex < (widgetCount - 1)) { parent.getChildAt(widgetIndex + 1).requestFocus(); } else { if (pageIndex < (pageCount - 1)) { newParent = (PagedViewExtendedLayout) container.getChildAt(pageIndex + 1); newParent.getChildAt(0).requestFocus(); } } } wasHandled = true; break; case KeyEvent.KEYCODE_DPAD_UP: if (handleKeyEvent) { // Select widgets tab on the tab bar tabs.requestFocus(); } wasHandled = true; break; case KeyEvent.KEYCODE_DPAD_DOWN: if (handleKeyEvent) { // TODO: Should focus the global search bar } wasHandled = true; break; case KeyEvent.KEYCODE_ENTER: case KeyEvent.KEYCODE_DPAD_CENTER: if (handleKeyEvent) { // Simulate a click on the widget View.OnClickListener clickListener = (View.OnClickListener) container; clickListener.onClick(w); } wasHandled = true; break; case KeyEvent.KEYCODE_PAGE_UP: if (handleKeyEvent) { // Select the first item on the previous page, or the first item on this page // if there is no previous page if (pageIndex > 0) { newParent = (PagedViewExtendedLayout) container.getChildAt(pageIndex - 1); newParent.getChildAt(0).requestFocus(); } else { parent.getChildAt(0).requestFocus(); } } wasHandled = true; break; case KeyEvent.KEYCODE_PAGE_DOWN: if (handleKeyEvent) { // Select the first item on the next page, or the last item on this page // if there is no next page if (pageIndex < (pageCount - 1)) { newParent = (PagedViewExtendedLayout) container.getChildAt(pageIndex + 1); newParent.getChildAt(0).requestFocus(); } else { parent.getChildAt(widgetCount - 1).requestFocus(); } } wasHandled = true; break; case KeyEvent.KEYCODE_MOVE_HOME: if (handleKeyEvent) { // Select the first item on this page parent.getChildAt(0).requestFocus(); } wasHandled = true; break; case KeyEvent.KEYCODE_MOVE_END: if (handleKeyEvent) { // Select the last item on this page parent.getChildAt(widgetCount - 1).requestFocus(); } wasHandled = true; break; default: break; } return wasHandled; } /** * Handles key events in a PageViewExtendedLayout containing PagedViewWidgets. */ static boolean handlePagedViewGridLayoutWidgetKeyEvent(PagedViewWidget w, int keyCode, KeyEvent e) { final PagedViewGridLayout parent = (PagedViewGridLayout) w.getParent(); final ViewGroup container = (ViewGroup) parent.getParent(); final TabHost tabHost = findTabHostParent(container); final TabWidget tabs = (TabWidget) tabHost.findViewById(com.android.internal.R.id.tabs); final int widgetIndex = parent.indexOfChild(w); final int widgetCount = parent.getChildCount(); final int pageIndex = container.indexOfChild(parent); final int pageCount = container.getChildCount(); final int cellCountX = parent.getCellCountX(); final int cellCountY = parent.getCellCountY(); final int x = widgetIndex % cellCountX; final int y = widgetIndex / cellCountX; final int action = e.getAction(); final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); PagedViewGridLayout newParent = null; boolean wasHandled = false; switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: if (handleKeyEvent) { // Select the previous widget or the last widget on the previous page if (widgetIndex > 0) { parent.getChildAt(widgetIndex - 1).requestFocus(); } else { if (pageIndex > 0) { newParent = (PagedViewGridLayout) container.getChildAt(pageIndex - 1); newParent.getChildAt(newParent.getChildCount() - 1).requestFocus(); } } } wasHandled = true; break; case KeyEvent.KEYCODE_DPAD_RIGHT: if (handleKeyEvent) { // Select the next widget or the first widget on the next page if (widgetIndex < (widgetCount - 1)) { parent.getChildAt(widgetIndex + 1).requestFocus(); } else { if (pageIndex < (pageCount - 1)) { newParent = (PagedViewGridLayout) container.getChildAt(pageIndex + 1); newParent.getChildAt(0).requestFocus(); } } } wasHandled = true; break; case KeyEvent.KEYCODE_DPAD_UP: if (handleKeyEvent) { // Select the closest icon in the previous row, otherwise select the tab bar if (y > 0) { int newWidgetIndex = ((y - 1) * cellCountX) + x; parent.getChildAt(newWidgetIndex).requestFocus(); } else { tabs.requestFocus(); } } wasHandled = true; break; case KeyEvent.KEYCODE_DPAD_DOWN: if (handleKeyEvent) { // Select the closest icon in the previous row, otherwise do nothing if (y < (cellCountY - 1)) { int newWidgetIndex = Math.min(widgetCount - 1, ((y + 1) * cellCountX) + x); parent.getChildAt(newWidgetIndex).requestFocus(); } } wasHandled = true; break; case KeyEvent.KEYCODE_ENTER: case KeyEvent.KEYCODE_DPAD_CENTER: if (handleKeyEvent) { // Simulate a click on the widget View.OnClickListener clickListener = (View.OnClickListener) container; clickListener.onClick(w); } wasHandled = true; break; case KeyEvent.KEYCODE_PAGE_UP: if (handleKeyEvent) { // Select the first item on the previous page, or the first item on this page // if there is no previous page if (pageIndex > 0) { newParent = (PagedViewGridLayout) container.getChildAt(pageIndex - 1); newParent.getChildAt(0).requestFocus(); } else { parent.getChildAt(0).requestFocus(); } } wasHandled = true; break; case KeyEvent.KEYCODE_PAGE_DOWN: if (handleKeyEvent) { // Select the first item on the next page, or the last item on this page // if there is no next page if (pageIndex < (pageCount - 1)) { newParent = (PagedViewGridLayout) container.getChildAt(pageIndex + 1); newParent.getChildAt(0).requestFocus(); } else { parent.getChildAt(widgetCount - 1).requestFocus(); } } wasHandled = true; break; case KeyEvent.KEYCODE_MOVE_HOME: if (handleKeyEvent) { // Select the first item on this page parent.getChildAt(0).requestFocus(); } wasHandled = true; break; case KeyEvent.KEYCODE_MOVE_END: if (handleKeyEvent) { // Select the last item on this page parent.getChildAt(widgetCount - 1).requestFocus(); } wasHandled = true; break; default: break; } return wasHandled; } /** * Private helper method to get the PagedViewCellLayoutChildren given a PagedViewCellLayout * index. */ private static PagedViewCellLayoutChildren getPagedViewCellLayoutChildrenForIndex( ViewGroup container, int i) { ViewGroup parent = (ViewGroup) container.getChildAt(i); return (PagedViewCellLayoutChildren) parent.getChildAt(0); } /** * Handles key events in a PageViewCellLayout containing PagedViewIcons. */ static boolean handlePagedViewIconKeyEvent(PagedViewIcon v, int keyCode, KeyEvent e) { final PagedViewCellLayoutChildren parent = (PagedViewCellLayoutChildren) v.getParent(); final PagedViewCellLayout parentLayout = (PagedViewCellLayout) parent.getParent(); // Note we have an extra parent because of the // PagedViewCellLayout/PagedViewCellLayoutChildren relationship final ViewGroup container = (ViewGroup) parentLayout.getParent(); final TabHost tabHost = findTabHostParent(container); final TabWidget tabs = (TabWidget) tabHost.findViewById(com.android.internal.R.id.tabs); final int widgetIndex = parent.indexOfChild(v); final int widgetCount = parent.getChildCount(); final int pageIndex = container.indexOfChild(parentLayout); final int pageCount = container.getChildCount(); final int cellCountX = parentLayout.getCellCountX(); final int cellCountY = parentLayout.getCellCountY(); final int x = widgetIndex % cellCountX; final int y = widgetIndex / cellCountX; final int action = e.getAction(); final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); PagedViewCellLayoutChildren newParent = null; boolean wasHandled = false; switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: if (handleKeyEvent) { // Select the previous icon or the last icon on the previous page if (widgetIndex > 0) { parent.getChildAt(widgetIndex - 1).requestFocus(); } else { if (pageIndex > 0) { newParent = getPagedViewCellLayoutChildrenForIndex(container, pageIndex - 1); newParent.getChildAt(newParent.getChildCount() - 1).requestFocus(); } } } wasHandled = true; break; case KeyEvent.KEYCODE_DPAD_RIGHT: if (handleKeyEvent) { // Select the next icon or the first icon on the next page if (widgetIndex < (widgetCount - 1)) { parent.getChildAt(widgetIndex + 1).requestFocus(); } else { if (pageIndex < (pageCount - 1)) { newParent = getPagedViewCellLayoutChildrenForIndex(container, pageIndex + 1); newParent.getChildAt(0).requestFocus(); } } } wasHandled = true; break; case KeyEvent.KEYCODE_DPAD_UP: if (handleKeyEvent) { // Select the closest icon in the previous row, otherwise select the tab bar if (y > 0) { int newWidgetIndex = ((y - 1) * cellCountX) + x; parent.getChildAt(newWidgetIndex).requestFocus(); } else { tabs.requestFocus(); } } wasHandled = true; break; case KeyEvent.KEYCODE_DPAD_DOWN: if (handleKeyEvent) { // Select the closest icon in the previous row, otherwise do nothing if (y < (cellCountY - 1)) { int newWidgetIndex = Math.min(widgetCount - 1, ((y + 1) * cellCountX) + x); parent.getChildAt(newWidgetIndex).requestFocus(); } } wasHandled = true; break; case KeyEvent.KEYCODE_ENTER: case KeyEvent.KEYCODE_DPAD_CENTER: if (handleKeyEvent) { // Simulate a click on the icon View.OnClickListener clickListener = (View.OnClickListener) container; clickListener.onClick(v); } wasHandled = true; break; case KeyEvent.KEYCODE_PAGE_UP: if (handleKeyEvent) { // Select the first icon on the previous page, or the first icon on this page // if there is no previous page if (pageIndex > 0) { newParent = getPagedViewCellLayoutChildrenForIndex(container, pageIndex - 1); newParent.getChildAt(0).requestFocus(); } else { parent.getChildAt(0).requestFocus(); } } wasHandled = true; break; case KeyEvent.KEYCODE_PAGE_DOWN: if (handleKeyEvent) { // Select the first icon on the next page, or the last icon on this page // if there is no next page if (pageIndex < (pageCount - 1)) { newParent = getPagedViewCellLayoutChildrenForIndex(container, pageIndex + 1); newParent.getChildAt(0).requestFocus(); } else { parent.getChildAt(widgetCount - 1).requestFocus(); } } wasHandled = true; break; case KeyEvent.KEYCODE_MOVE_HOME: if (handleKeyEvent) { // Select the first icon on this page parent.getChildAt(0).requestFocus(); } wasHandled = true; break; case KeyEvent.KEYCODE_MOVE_END: if (handleKeyEvent) { // Select the last icon on this page parent.getChildAt(widgetCount - 1).requestFocus(); } wasHandled = true; break; default: break; } return wasHandled; } /** * Handles key events in the tab widget. */ static boolean handleTabKeyEvent(AccessibleTabView v, int keyCode, KeyEvent e) { if (!LauncherApplication.isScreenLarge()) return false; final FocusOnlyTabWidget parent = (FocusOnlyTabWidget) v.getParent(); final TabHost tabHost = findTabHostParent(parent); final ViewGroup contents = (ViewGroup) tabHost.findViewById(com.android.internal.R.id.tabcontent); final int tabCount = parent.getTabCount(); final int tabIndex = parent.getChildTabIndex(v); final int action = e.getAction(); final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); boolean wasHandled = false; switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: if (handleKeyEvent) { // Select the previous tab if (tabIndex > 0) { parent.getChildTabViewAt(tabIndex - 1).requestFocus(); } } wasHandled = true; break; case KeyEvent.KEYCODE_DPAD_RIGHT: if (handleKeyEvent) { // Select the next tab, or if the last tab has a focus right id, select that if (tabIndex < (tabCount - 1)) { parent.getChildTabViewAt(tabIndex + 1).requestFocus(); } else { if (v.getNextFocusRightId() != View.NO_ID) { tabHost.findViewById(v.getNextFocusRightId()).requestFocus(); } } } wasHandled = true; break; case KeyEvent.KEYCODE_DPAD_UP: // Do nothing wasHandled = true; break; case KeyEvent.KEYCODE_DPAD_DOWN: if (handleKeyEvent) { // Select the content view contents.requestFocus(); } wasHandled = true; break; default: break; } return wasHandled; } /** * Handles key events in the workspace button bar. */ static boolean handleButtonBarButtonKeyEvent(View v, int keyCode, KeyEvent e) { if (!LauncherApplication.isScreenLarge()) return false; final ViewGroup parent = (ViewGroup) v.getParent(); final ViewGroup launcher = (ViewGroup) parent.getParent(); final Workspace workspace = (Workspace) launcher.findViewById(R.id.workspace); final int buttonIndex = parent.indexOfChild(v); final int buttonCount = parent.getChildCount(); final int pageIndex = workspace.getCurrentPage(); final int pageCount = workspace.getChildCount(); final int firstButtonIndex = parent.indexOfChild(parent.findViewById(R.id.search_button)); final int lastButtonIndex = parent.indexOfChild(parent.findViewById(R.id.configure_button)); final int action = e.getAction(); final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); boolean wasHandled = false; switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: if (handleKeyEvent) { // Select the previous button, otherwise do nothing (since the button bar is // static) if (buttonIndex > firstButtonIndex) { int newButtonIndex = buttonIndex - 1; while (newButtonIndex >= firstButtonIndex) { View prev = parent.getChildAt(newButtonIndex); if (isVisible(prev) && prev.isFocusable()) { prev.requestFocus(); break; } --newButtonIndex; } } else { if (pageIndex > 0) { // Snap to previous page and clear focus workspace.snapToPage(pageIndex - 1); } } } wasHandled = true; break; case KeyEvent.KEYCODE_DPAD_RIGHT: if (handleKeyEvent) { // Select the next button, otherwise do nothing (since the button bar is // static) if (buttonIndex < lastButtonIndex) { int newButtonIndex = buttonIndex + 1; while (newButtonIndex <= lastButtonIndex) { View next = parent.getChildAt(newButtonIndex); if (isVisible(next) && next.isFocusable()) { next.requestFocus(); break; } ++newButtonIndex; } } else { if (pageIndex < (pageCount - 1)) { // Snap to next page and clear focus workspace.snapToPage(pageIndex + 1); } } } wasHandled = true; break; case KeyEvent.KEYCODE_DPAD_UP: // Do nothing wasHandled = true; break; case KeyEvent.KEYCODE_DPAD_DOWN: if (handleKeyEvent) { // Select the first bubble text view in the current page of the workspace final CellLayout layout = (CellLayout) workspace.getChildAt(pageIndex); final CellLayoutChildren children = layout.getChildrenLayout(); final View newIcon = getBubbleTextViewInDirection(layout, children, -1, 1); if (newIcon != null) { newIcon.requestFocus(); } else { workspace.requestFocus(); } } wasHandled = true; break; default: break; } return wasHandled; } /** * Handles key events in the prev/next indicators. */ static boolean handleIndicatorButtonKeyEvent(View v, int keyCode, KeyEvent e) { final ViewGroup launcher = (ViewGroup) v.getParent(); final Workspace workspace = (Workspace) launcher.findViewById(R.id.workspace); final ViewGroup hotseat = (ViewGroup) launcher.findViewById(R.id.all_apps_button_cluster); final View previousIndicator = launcher.findViewById(R.id.previous_screen); final View nextIndicator = launcher.findViewById(R.id.next_screen); final int pageIndex = workspace.getCurrentPage(); final int pageCount = workspace.getChildCount(); final int action = e.getAction(); final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); boolean wasHandled = false; switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: if (handleKeyEvent) { if (v == previousIndicator) { if (pageIndex > 0) { // Snap to previous page and clear focus workspace.snapToPage(pageIndex - 1); } } else if (v == nextIndicator) { // Select the last button in the hot seat hotseat.getChildAt(hotseat.getChildCount() - 1).requestFocus(); } } wasHandled = true; break; case KeyEvent.KEYCODE_DPAD_RIGHT: if (handleKeyEvent) { if (v == previousIndicator) { // Select the first button in the hot seat hotseat.getChildAt(0).requestFocus(); } else if (v == nextIndicator) { if (pageIndex < (pageCount - 1)) { // Snap to next page and clear focus workspace.snapToPage(pageIndex + 1); } } } wasHandled = true; break; case KeyEvent.KEYCODE_DPAD_UP: if (handleKeyEvent) { // Select the first bubble text view in the current page of the workspace final CellLayout layout = (CellLayout) workspace.getChildAt(pageIndex); final CellLayoutChildren children = layout.getChildrenLayout(); final View newIcon = getBubbleTextViewInDirection(layout, children, -1, 1); if (newIcon != null) { newIcon.requestFocus(); } else { workspace.requestFocus(); } } wasHandled = true; break; case KeyEvent.KEYCODE_DPAD_DOWN: // Do nothing wasHandled = true; break; default: break; } return wasHandled; } /** * Handles key events in the workspace dock (bottom of the screen). */ static boolean handleDockButtonKeyEvent(View v, int keyCode, KeyEvent e, int orientation) { final ViewGroup parent = (ViewGroup) v.getParent(); final ViewGroup launcher = (ViewGroup) parent.getParent(); final Workspace workspace = (Workspace) launcher.findViewById(R.id.workspace); final int buttonIndex = parent.indexOfChild(v); final int buttonCount = parent.getChildCount(); final int pageIndex = workspace.getCurrentPage(); final int pageCount = workspace.getChildCount(); final View previousIndicator = launcher.findViewById(R.id.previous_screen); final View nextIndicator = launcher.findViewById(R.id.next_screen); // NOTE: currently we don't special case for the phone UI in different // orientations, even though the dock is on the side in landscape mode. This // is to ensure that accessibility consistency is maintained across rotations. final int action = e.getAction(); final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); boolean wasHandled = false; switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: if (handleKeyEvent) { // Select the previous button, otherwise select the previous page indicator if (buttonIndex > 0) { parent.getChildAt(buttonIndex - 1).requestFocus(); } else { previousIndicator.requestFocus(); } } wasHandled = true; break; case KeyEvent.KEYCODE_DPAD_RIGHT: if (handleKeyEvent) { // Select the next button, otherwise select the next page indicator if (buttonIndex < (buttonCount - 1)) { parent.getChildAt(buttonIndex + 1).requestFocus(); } else { nextIndicator.requestFocus(); } } wasHandled = true; break; case KeyEvent.KEYCODE_DPAD_UP: if (handleKeyEvent) { // Select the first bubble text view in the current page of the workspace final CellLayout layout = (CellLayout) workspace.getChildAt(pageIndex); final CellLayoutChildren children = layout.getChildrenLayout(); final View newIcon = getBubbleTextViewInDirection(layout, children, -1, 1); if (newIcon != null) { newIcon.requestFocus(); } else { workspace.requestFocus(); } } wasHandled = true; break; case KeyEvent.KEYCODE_DPAD_DOWN: // Do nothing wasHandled = true; break; default: break; } return wasHandled; } /** * Private helper method to get the CellLayoutChildren given a CellLayout index. */ private static CellLayoutChildren getCellLayoutChildrenForIndex(ViewGroup container, int i) { ViewGroup parent = (ViewGroup) container.getChildAt(i); return (CellLayoutChildren) parent.getChildAt(0); } /** * Private helper method to sort all the CellLayout children in order of their (x,y) spatially * from top left to bottom right. */ private static ArrayList getCellLayoutChildrenSortedSpatially(CellLayout layout, ViewGroup parent) { // First we order each the CellLayout children by their x,y coordinates final int cellCountX = layout.getCountX(); final int count = parent.getChildCount(); ArrayList views = new ArrayList(); for (int j = 0; j < count; ++j) { views.add(parent.getChildAt(j)); } Collections.sort(views, new Comparator() { @Override public int compare(View lhs, View rhs) { CellLayout.LayoutParams llp = (CellLayout.LayoutParams) lhs.getLayoutParams(); CellLayout.LayoutParams rlp = (CellLayout.LayoutParams) rhs.getLayoutParams(); int lvIndex = (llp.cellY * cellCountX) + llp.cellX; int rvIndex = (rlp.cellY * cellCountX) + rlp.cellX; return lvIndex - rvIndex; } }); return views; } /** * Private helper method to find the index of the next BubbleTextView in the delta direction. * @param delta either -1 or 1 depending on the direction we want to search */ private static View findIndexOfBubbleTextView(ArrayList views, int i, int delta) { // Then we find the next BubbleTextView offset by delta from i final int count = views.size(); int newI = i + delta; while (0 <= newI && newI < count) { View newV = views.get(newI); if (newV instanceof BubbleTextView) { return newV; } newI += delta; } return null; } private static View getBubbleTextViewInDirection(CellLayout layout, ViewGroup parent, int i, int delta) { final ArrayList views = getCellLayoutChildrenSortedSpatially(layout, parent); return findIndexOfBubbleTextView(views, i, delta); } private static View getBubbleTextViewInDirection(CellLayout layout, ViewGroup parent, View v, int delta) { final ArrayList views = getCellLayoutChildrenSortedSpatially(layout, parent); return findIndexOfBubbleTextView(views, views.indexOf(v), delta); } /** * Private helper method to find the next closest BubbleTextView in the delta direction on the * next line. * @param delta either -1 or 1 depending on the line and direction we want to search */ private static View getClosestBubbleTextViewOnLine(CellLayout layout, ViewGroup parent, View v, int lineDelta) { final ArrayList views = getCellLayoutChildrenSortedSpatially(layout, parent); final CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams(); final int cellCountX = layout.getCountX(); final int cellCountY = layout.getCountY(); final int row = lp.cellY; final int newRow = row + lineDelta; if (0 <= newRow && newRow < cellCountY) { float closestDistance = Float.MAX_VALUE; int closestIndex = -1; int index = views.indexOf(v); int endIndex = (lineDelta < 0) ? -1 : views.size(); while (index != endIndex) { View newV = views.get(index); CellLayout.LayoutParams tmpLp = (CellLayout.LayoutParams) newV.getLayoutParams(); boolean satisfiesRow = (lineDelta < 0) ? (tmpLp.cellY < row) : (tmpLp.cellY > row); if (satisfiesRow && newV instanceof BubbleTextView) { float tmpDistance = (float) Math.sqrt(Math.pow(tmpLp.cellX - lp.cellX, 2) + Math.pow(tmpLp.cellY - lp.cellY, 2)); if (tmpDistance < closestDistance) { closestIndex = index; closestDistance = tmpDistance; } } if (index <= endIndex) { ++index; } else { --index; } } if (closestIndex > -1) { return views.get(closestIndex); } } return null; } /** * Handles key events in a Workspace containing BubbleTextView. */ static boolean handleBubbleTextViewKeyEvent(BubbleTextView v, int keyCode, KeyEvent e) { CellLayoutChildren parent = (CellLayoutChildren) v.getParent(); final CellLayout layout = (CellLayout) parent.getParent(); final Workspace workspace = (Workspace) layout.getParent(); final ViewGroup launcher = (ViewGroup) workspace.getParent(); final ViewGroup tabs = (ViewGroup) launcher.findViewById(R.id.all_apps_button_cluster); int iconIndex = parent.indexOfChild(v); int iconCount = parent.getChildCount(); int pageIndex = workspace.indexOfChild(layout); int pageCount = workspace.getChildCount(); final int action = e.getAction(); final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); boolean wasHandled = false; switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: if (handleKeyEvent) { // Select the previous icon or the last icon on the previous page if possible View newIcon = getBubbleTextViewInDirection(layout, parent, v, -1); if (newIcon != null) { newIcon.requestFocus(); } else { if (pageIndex > 0) { parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); newIcon = getBubbleTextViewInDirection(layout, parent, parent.getChildCount(), -1); if (newIcon != null) { newIcon.requestFocus(); } else { // Snap to the previous page workspace.snapToPage(pageIndex - 1); } } } } wasHandled = true; break; case KeyEvent.KEYCODE_DPAD_RIGHT: if (handleKeyEvent) { // Select the next icon or the first icon on the next page if possible View newIcon = getBubbleTextViewInDirection(layout, parent, v, 1); if (newIcon != null) { newIcon.requestFocus(); } else { if (pageIndex < (pageCount - 1)) { parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); newIcon = getBubbleTextViewInDirection(layout, parent, -1, 1); if (newIcon != null) { newIcon.requestFocus(); } else { // Snap to the next page workspace.snapToPage(pageIndex + 1); } } } } wasHandled = true; break; case KeyEvent.KEYCODE_DPAD_UP: if (handleKeyEvent) { // Select the closest icon in the previous line, otherwise select the tab bar View newIcon = getClosestBubbleTextViewOnLine(layout, parent, v, -1); if (newIcon != null) { newIcon.requestFocus(); wasHandled = true; } else { tabs.requestFocus(); } } break; case KeyEvent.KEYCODE_DPAD_DOWN: if (handleKeyEvent) { // Select the closest icon in the next line, otherwise select the tab bar View newIcon = getClosestBubbleTextViewOnLine(layout, parent, v, 1); if (newIcon != null) { newIcon.requestFocus(); wasHandled = true; } } break; case KeyEvent.KEYCODE_PAGE_UP: if (handleKeyEvent) { // Select the first icon on the previous page or the first icon on this page // if there is no previous page if (pageIndex > 0) { parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); View newIcon = getBubbleTextViewInDirection(layout, parent, -1, 1); if (newIcon != null) { newIcon.requestFocus(); } else { // Snap to the previous page workspace.snapToPage(pageIndex - 1); } } else { View newIcon = getBubbleTextViewInDirection(layout, parent, -1, 1); if (newIcon != null) { newIcon.requestFocus(); } } } wasHandled = true; break; case KeyEvent.KEYCODE_PAGE_DOWN: if (handleKeyEvent) { // Select the first icon on the next page or the last icon on this page // if there is no previous page if (pageIndex < (pageCount - 1)) { parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); View newIcon = getBubbleTextViewInDirection(layout, parent, -1, 1); if (newIcon != null) { newIcon.requestFocus(); } else { // Snap to the next page workspace.snapToPage(pageIndex + 1); } } else { View newIcon = getBubbleTextViewInDirection(layout, parent, parent.getChildCount(), -1); if (newIcon != null) { newIcon.requestFocus(); } } } wasHandled = true; break; case KeyEvent.KEYCODE_MOVE_HOME: if (handleKeyEvent) { // Select the first icon on this page View newIcon = getBubbleTextViewInDirection(layout, parent, -1, 1); if (newIcon != null) { newIcon.requestFocus(); } } wasHandled = true; break; case KeyEvent.KEYCODE_MOVE_END: if (handleKeyEvent) { // Select the last icon on this page View newIcon = getBubbleTextViewInDirection(layout, parent, parent.getChildCount(), -1); if (newIcon != null) { newIcon.requestFocus(); } } wasHandled = true; break; default: break; } return wasHandled; } }