From 4e6a976c2d85f7261ae4318a9ccffd2440f73124 Mon Sep 17 00:00:00 2001 From: Winson Chung Date: Mon, 9 May 2011 11:56:34 -0700 Subject: Adding keyboard focus support for phones. Change-Id: I2daab961d0727bc5d892db6b50ad0f51fe23873c --- .../android/launcher2/AppsCustomizePagedView.java | 17 +- src/com/android/launcher2/FocusHelper.java | 294 ++++++++++++++++++++- src/com/android/launcher2/Launcher.java | 13 +- .../android/launcher2/PagedViewExtendedLayout.java | 3 +- src/com/android/launcher2/PagedViewGridLayout.java | 120 +++++++++ src/com/android/launcher2/PagedViewWidget.java | 18 +- 6 files changed, 444 insertions(+), 21 deletions(-) create mode 100644 src/com/android/launcher2/PagedViewGridLayout.java (limited to 'src/com/android/launcher2') diff --git a/src/com/android/launcher2/AppsCustomizePagedView.java b/src/com/android/launcher2/AppsCustomizePagedView.java index a1e61714a..0c4867cf1 100644 --- a/src/com/android/launcher2/AppsCustomizePagedView.java +++ b/src/com/android/launcher2/AppsCustomizePagedView.java @@ -533,8 +533,7 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen /* * Widgets PagedView implementation */ - private void setupPage(PagedViewExtendedLayout layout) { - layout.setGravity(Gravity.LEFT); + private void setupPage(PagedViewGridLayout layout) { layout.setPadding(mPageLayoutPaddingLeft, mPageLayoutPaddingTop, mPageLayoutPaddingRight, mPageLayoutPaddingBottom); @@ -679,18 +678,17 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen int numWidgetsPerPage = mWidgetCountX * mWidgetCountY; int numPages = (int) Math.ceil(mWidgets.size() / (float) numWidgetsPerPage); for (int i = 0; i < numPages; ++i) { - PagedViewExtendedLayout layout = new PagedViewExtendedLayout(context); + PagedViewGridLayout layout = new PagedViewGridLayout(context, mWidgetCountX, + mWidgetCountY); setupPage(layout); addView(layout); } } public void syncWidgetPageItems(int page) { - Context context = getContext(); - PagedViewExtendedLayout layout = (PagedViewExtendedLayout) getChildAt(page); + PagedViewGridLayout layout = (PagedViewGridLayout) getChildAt(page); layout.removeAllViews(); // Calculate the dimensions of each cell we are giving to each widget - FrameLayout container = new FrameLayout(context); int numWidgetsPerPage = mWidgetCountX * mWidgetCountY; int offset = page * numWidgetsPerPage; int cellWidth = ((mWidgetSpacingLayout.getContentWidth() - mPageLayoutWidthGap @@ -728,15 +726,14 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen widget.setOnTouchListener(this); // Layout each widget - FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(cellWidth, cellHeight); int ix = i % mWidgetCountX; int iy = i / mWidgetCountX; + PagedViewGridLayout.LayoutParams lp = new PagedViewGridLayout.LayoutParams(cellWidth, + cellHeight); lp.leftMargin = (ix * cellWidth) + (ix * mWidgetCellWidthGap); lp.topMargin = (iy * cellHeight) + (iy * mWidgetCellHeightGap); - container.addView(widget, lp); + layout.addView(widget, lp); } - layout.addView(container, new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); } @Override public void syncPages() { diff --git a/src/com/android/launcher2/FocusHelper.java b/src/com/android/launcher2/FocusHelper.java index c9bd58cd0..f14ebaeb9 100644 --- a/src/com/android/launcher2/FocusHelper.java +++ b/src/com/android/launcher2/FocusHelper.java @@ -20,6 +20,7 @@ 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; @@ -39,6 +40,27 @@ class ButtonBarKeyEventListener implements View.OnKeyListener { } } +/** + * 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. @@ -107,6 +129,7 @@ public class FocusHelper { /** * Handles key events in a PageViewExtendedLayout containing PagedViewWidgets. + * To be deprecated. */ static boolean handlePagedViewWidgetKeyEvent(PagedViewWidget w, int keyCode, KeyEvent e) { if (!LauncherApplication.isScreenXLarge()) return false; @@ -222,6 +245,136 @@ public class FocusHelper { 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. @@ -236,8 +389,6 @@ public class FocusHelper { * Handles key events in a PageViewCellLayout containing PagedViewIcons. */ static boolean handlePagedViewIconKeyEvent(PagedViewIcon v, int keyCode, KeyEvent e) { - if (!LauncherApplication.isScreenXLarge()) return false; - final PagedViewCellLayoutChildren parent = (PagedViewCellLayoutChildren) v.getParent(); final PagedViewCellLayout parentLayout = (PagedViewCellLayout) parent.getParent(); // Note we have an extra parent because of the @@ -423,7 +574,7 @@ public class FocusHelper { } /** - * Handles key events in a the workspace button bar. + * Handles key events in the workspace button bar. */ static boolean handleButtonBarButtonKeyEvent(View v, int keyCode, KeyEvent e) { if (!LauncherApplication.isScreenXLarge()) return false; @@ -511,6 +662,141 @@ public class FocusHelper { 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. */ @@ -618,8 +904,6 @@ public class FocusHelper { * Handles key events in a Workspace containing BubbleTextView. */ static boolean handleBubbleTextViewKeyEvent(BubbleTextView v, int keyCode, KeyEvent e) { - if (!LauncherApplication.isScreenXLarge()) return false; - CellLayoutChildren parent = (CellLayoutChildren) v.getParent(); final CellLayout layout = (CellLayout) parent.getParent(); final Workspace workspace = (Workspace) layout.getParent(); diff --git a/src/com/android/launcher2/Launcher.java b/src/com/android/launcher2/Launcher.java index ceb36be4a..9b136c45d 100644 --- a/src/com/android/launcher2/Launcher.java +++ b/src/com/android/launcher2/Launcher.java @@ -935,8 +935,11 @@ public final class Launcher extends Activity hotseatRight.setContentDescription(mHotseatLabels[1]); hotseatRight.setImageDrawable(mHotseatIcons[1]); + View.OnKeyListener listener = new IndicatorKeyEventListener(); mPreviousView = (ImageView) dragLayer.findViewById(R.id.previous_screen); + mPreviousView.setOnKeyListener(listener); mNextView = (ImageView) dragLayer.findViewById(R.id.next_screen); + mNextView.setOnKeyListener(listener); Drawable previous = mPreviousView.getDrawable(); Drawable next = mNextView.getDrawable(); @@ -1009,7 +1012,14 @@ public final class Launcher extends Activity dragController.addDropTarget(allAppsDeleteZone); } mButtonCluster = (ViewGroup) findViewById(R.id.all_apps_button_cluster); - View.OnKeyListener listener = new ButtonBarKeyEventListener(); + View.OnKeyListener listener = null; + if (LauncherApplication.isScreenXLarge()) { + // For tablets, AllApps lives in the button bar at the top + listener = new ButtonBarKeyEventListener(); + } else { + // For phones, AppsCustomize lives in the "dock" at the bottom + listener = new DockKeyEventListener(); + } int buttonCount = mButtonCluster.getChildCount(); for (int i = 0; i < buttonCount; ++i) { mButtonCluster.getChildAt(i).setOnKeyListener(listener); @@ -2678,6 +2688,7 @@ public final class Launcher extends Activity }); if (toAllApps) { + toView.setVisibility(View.VISIBLE); toView.setFastAlpha(0f); ValueAnimator alphaAnim = ValueAnimator.ofFloat(0f, 1f).setDuration(fadeDuration); alphaAnim.setInterpolator(new DecelerateInterpolator(1.5f)); diff --git a/src/com/android/launcher2/PagedViewExtendedLayout.java b/src/com/android/launcher2/PagedViewExtendedLayout.java index 776b99fab..f471b76c7 100644 --- a/src/com/android/launcher2/PagedViewExtendedLayout.java +++ b/src/com/android/launcher2/PagedViewExtendedLayout.java @@ -23,7 +23,8 @@ import android.view.View; import android.widget.LinearLayout; /** - * The linear layout used strictly for the widget/wallpaper tab of the customization tray + * The linear layout used strictly for the widget/wallpaper tab of the customization tray. + * To be deprecated. */ public class PagedViewExtendedLayout extends LinearLayout implements Page { static final String TAG = "PagedViewExtendedLayout"; diff --git a/src/com/android/launcher2/PagedViewGridLayout.java b/src/com/android/launcher2/PagedViewGridLayout.java new file mode 100644 index 000000000..cedf71c5b --- /dev/null +++ b/src/com/android/launcher2/PagedViewGridLayout.java @@ -0,0 +1,120 @@ +/* + * 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 android.content.Context; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.MeasureSpec; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +/** + * The grid based layout used strictly for the widget/wallpaper tab of the AppsCustomize pane + */ +public class PagedViewGridLayout extends FrameLayout implements Page { + static final String TAG = "PagedViewGridLayout"; + + private int mCellCountX; + private int mCellCountY; + + public PagedViewGridLayout(Context context, int cellCountX, int cellCountY) { + super(context, null, 0); + mCellCountX = cellCountX; + mCellCountY = cellCountY; + } + + int getCellCountX() { + return mCellCountX; + } + int getCellCountY() { + return mCellCountY; + } + + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // PagedView currently has issues with different-sized pages since it calculates the + // offset of each page to scroll to before it updates the actual size of each page + // (which can change depending on the content if the contents aren't a fixed size). + // We work around this by having a minimum size on each widget page). + int widthSpecSize = Math.max(getSuggestedMinimumWidth(), + MeasureSpec.getSize(widthMeasureSpec)); + int widthSpecMode = MeasureSpec.AT_MOST; + super.onMeasure(MeasureSpec.makeMeasureSpec(widthSpecSize, widthSpecMode), + heightMeasureSpec); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + // We eat up the touch events here, since the PagedView (which uses the same swiping + // touch code as Workspace previously) uses onInterceptTouchEvent() to determine when + // the user is scrolling between pages. This means that if the pages themselves don't + // handle touch events, it gets forwarded up to PagedView itself, and it's own + // onTouchEvent() handling will prevent further intercept touch events from being called + // (it's the same view in that case). This is not ideal, but to prevent more changes, + // we just always mark the touch event as handled. + return super.onTouchEvent(event) || true; + } + + @Override + protected boolean onSetAlpha(int alpha) { + return true; + } + + @Override + public void setAlpha(float alpha) { + setChildrenAlpha(alpha); + super.setAlpha(alpha); + } + + private void setChildrenAlpha(float alpha) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + getChildAt(i).setAlpha(alpha); + } + } + + @Override + public void removeAllViewsOnPage() { + removeAllViews(); + } + + @Override + public void removeViewOnPageAt(int index) { + removeViewAt(index); + } + + @Override + public int getPageChildCount() { + return getChildCount(); + } + + @Override + public View getChildOnPageAt(int i) { + return getChildAt(i); + } + + @Override + public int indexOfChildOnPage(View v) { + return indexOfChild(v); + } + + public static class LayoutParams extends FrameLayout.LayoutParams { + public LayoutParams(int width, int height) { + super(width, height); + } + } +} diff --git a/src/com/android/launcher2/PagedViewWidget.java b/src/com/android/launcher2/PagedViewWidget.java index 46bf532b4..41473c336 100644 --- a/src/com/android/launcher2/PagedViewWidget.java +++ b/src/com/android/launcher2/PagedViewWidget.java @@ -255,14 +255,24 @@ public class PagedViewWidget extends LinearLayout implements Checkable { @Override public boolean onKeyDown(int keyCode, KeyEvent event) { - return FocusHelper.handlePagedViewWidgetKeyEvent(this, keyCode, event) - || super.onKeyDown(keyCode, event); + if (LauncherApplication.isScreenXLarge()) { + return FocusHelper.handlePagedViewWidgetKeyEvent(this, keyCode, event) + || super.onKeyDown(keyCode, event); + } else { + return FocusHelper.handlePagedViewGridLayoutWidgetKeyEvent(this, keyCode, event) + || super.onKeyDown(keyCode, event); + } } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { - return FocusHelper.handlePagedViewWidgetKeyEvent(this, keyCode, event) - || super.onKeyUp(keyCode, event); + if (LauncherApplication.isScreenXLarge()) { + return FocusHelper.handlePagedViewWidgetKeyEvent(this, keyCode, event) + || super.onKeyUp(keyCode, event); + } else { + return FocusHelper.handlePagedViewGridLayoutWidgetKeyEvent(this, keyCode, event) + || super.onKeyUp(keyCode, event); + } } @Override -- cgit v1.2.3