Add initial support for TAB navigation.
Bug: 3286652
Change-Id: I813a0318b3b8d9c9bc791ea6a2427be11c08de00
diff --git a/core/java/android/view/FocusFinder.java b/core/java/android/view/FocusFinder.java
index 8ad9a62..d9bf918 100644
--- a/core/java/android/view/FocusFinder.java
+++ b/core/java/android/view/FocusFinder.java
@@ -19,6 +19,8 @@
import android.graphics.Rect;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
/**
* The algorithm used for finding the next focusable view in a given direction
@@ -44,6 +46,7 @@
Rect mFocusedRect = new Rect();
Rect mOtherRect = new Rect();
Rect mBestCandidateRect = new Rect();
+ SequentialFocusComparator mSequentialFocusComparator = new SequentialFocusComparator();
// enforce thread local access
private FocusFinder() {}
@@ -76,6 +79,7 @@
switch (direction) {
case View.FOCUS_RIGHT:
case View.FOCUS_DOWN:
+ case View.FOCUS_FORWARD:
final int rootTop = root.getScrollY();
final int rootLeft = root.getScrollX();
mFocusedRect.set(rootLeft, rootTop, rootLeft, rootTop);
@@ -83,6 +87,7 @@
case View.FOCUS_LEFT:
case View.FOCUS_UP:
+ case View.FOCUS_BACKWARD:
final int rootBottom = root.getScrollY() + root.getHeight();
final int rootRight = root.getScrollX() + root.getWidth();
mFocusedRect.set(rootRight, rootBottom,
@@ -107,6 +112,48 @@
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
ArrayList<View> focusables = root.getFocusables(direction);
+ if (focusables.isEmpty()) {
+ // The focus cannot change.
+ return null;
+ }
+
+ if (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD) {
+ if (focused != null && !focusables.contains(focused)) {
+ // Add the currently focused view to the list to have it sorted
+ // along with the other views.
+ focusables.add(focused);
+ }
+
+ try {
+ // Note: This sort is stable.
+ mSequentialFocusComparator.setRoot(root);
+ Collections.sort(focusables, mSequentialFocusComparator);
+ } finally {
+ mSequentialFocusComparator.recycle();
+ }
+
+ final int count = focusables.size();
+ switch (direction) {
+ case View.FOCUS_FORWARD:
+ if (focused != null) {
+ int position = focusables.lastIndexOf(focused);
+ if (position >= 0 && position + 1 < count) {
+ return focusables.get(position + 1);
+ }
+ }
+ return focusables.get(0);
+
+ case View.FOCUS_BACKWARD:
+ if (focused != null) {
+ int position = focusables.indexOf(focused);
+ if (position > 0) {
+ return focusables.get(position - 1);
+ }
+ }
+ return focusables.get(count - 1);
+ }
+ return null;
+ }
// initialize the best candidate to something impossible
// (so the first plausible view will become the best choice)
@@ -477,4 +524,59 @@
throw new IllegalArgumentException("direction must be one of "
+ "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
}
+
+ /**
+ * Sorts views according to their visual layout and geometry for default tab order.
+ * This is used for sequential focus traversal.
+ */
+ private static final class SequentialFocusComparator implements Comparator<View> {
+ private final Rect mFirstRect = new Rect();
+ private final Rect mSecondRect = new Rect();
+ private ViewGroup mRoot;
+
+ public void recycle() {
+ mRoot = null;
+ }
+
+ public void setRoot(ViewGroup root) {
+ mRoot = root;
+ }
+
+ public int compare(View first, View second) {
+ if (first == second) {
+ return 0;
+ }
+
+ getRect(first, mFirstRect);
+ getRect(second, mSecondRect);
+
+ if (mFirstRect.top < mSecondRect.top) {
+ return -1;
+ } else if (mFirstRect.top > mSecondRect.top) {
+ return 1;
+ } else if (mFirstRect.left < mSecondRect.left) {
+ return -1;
+ } else if (mFirstRect.left > mSecondRect.left) {
+ return 1;
+ } else if (mFirstRect.bottom < mSecondRect.bottom) {
+ return -1;
+ } else if (mFirstRect.bottom > mSecondRect.bottom) {
+ return 1;
+ } else if (mFirstRect.right < mSecondRect.right) {
+ return -1;
+ } else if (mFirstRect.right > mSecondRect.right) {
+ return 1;
+ } else {
+ // The view are distinct but completely coincident so we consider
+ // them equal for our purposes. Since the sort is stable, this
+ // means that the views will retain their layout order relative to one another.
+ return 0;
+ }
+ }
+
+ private void getRect(View view, Rect rect) {
+ view.getDrawingRect(rect);
+ mRoot.offsetDescendantRectToMyCoords(view, rect);
+ }
+ }
}
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 75aebc9..71d07c6 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -64,6 +64,7 @@
import android.view.inputmethod.InputMethodManager;
import android.widget.ScrollBarDrawable;
import com.android.internal.R;
+import com.android.internal.util.Predicate;
import com.android.internal.view.menu.MenuBuilder;
import java.lang.ref.WeakReference;
@@ -2066,6 +2067,12 @@
*/
private int mNextFocusDownId = View.NO_ID;
+ /**
+ * When this view has focus and the next focus is {@link #FOCUS_FORWARD},
+ * the user may specify which view to go to next.
+ */
+ int mNextFocusForwardId = View.NO_ID;
+
private CheckForLongPress mPendingCheckForLongPress;
private CheckForTap mPendingCheckForTap = null;
private PerformClick mPerformClick;
@@ -2424,6 +2431,9 @@
case R.styleable.View_nextFocusDown:
mNextFocusDownId = a.getResourceId(attr, View.NO_ID);
break;
+ case R.styleable.View_nextFocusForward:
+ mNextFocusForwardId = a.getResourceId(attr, View.NO_ID);
+ break;
case R.styleable.View_minWidth:
mMinWidth = a.getDimensionPixelSize(attr, 0);
break;
@@ -3112,9 +3122,9 @@
* @param gainFocus True if the View has focus; false otherwise.
* @param direction The direction focus has moved when requestFocus()
* is called to give this view focus. Values are
- * {@link #FOCUS_UP}, {@link #FOCUS_DOWN}, {@link #FOCUS_LEFT} or
- * {@link #FOCUS_RIGHT}. It may not always apply, in which
- * case use the default.
+ * {@link #FOCUS_UP}, {@link #FOCUS_DOWN}, {@link #FOCUS_LEFT},
+ * {@link #FOCUS_RIGHT}, {@link #FOCUS_FORWARD}, or {@link #FOCUS_BACKWARD}.
+ * It may not always apply, in which case use the default.
* @param previouslyFocusedRect The rectangle, in this view's coordinate
* system, of the previously focused view. If applicable, this will be
* passed in as finer grained information about where the focus is coming
@@ -3359,7 +3369,8 @@
}
/**
- * @return The user specified next focus ID.
+ * Gets the id of the view to use when the next focus is {@link #FOCUS_LEFT}.
+ * @return The next focus ID, or {@link #NO_ID} if the framework should decide automatically.
*
* @attr ref android.R.styleable#View_nextFocusLeft
*/
@@ -3368,9 +3379,9 @@
}
/**
- * Set the id of the view to use for the next focus
- *
- * @param nextFocusLeftId
+ * Sets the id of the view to use when the next focus is {@link #FOCUS_LEFT}.
+ * @param nextFocusLeftId The next focus ID, or {@link #NO_ID} if the framework should
+ * decide automatically.
*
* @attr ref android.R.styleable#View_nextFocusLeft
*/
@@ -3379,7 +3390,8 @@
}
/**
- * @return The user specified next focus ID.
+ * Gets the id of the view to use when the next focus is {@link #FOCUS_RIGHT}.
+ * @return The next focus ID, or {@link #NO_ID} if the framework should decide automatically.
*
* @attr ref android.R.styleable#View_nextFocusRight
*/
@@ -3388,9 +3400,9 @@
}
/**
- * Set the id of the view to use for the next focus
- *
- * @param nextFocusRightId
+ * Sets the id of the view to use when the next focus is {@link #FOCUS_RIGHT}.
+ * @param nextFocusRightId The next focus ID, or {@link #NO_ID} if the framework should
+ * decide automatically.
*
* @attr ref android.R.styleable#View_nextFocusRight
*/
@@ -3399,7 +3411,8 @@
}
/**
- * @return The user specified next focus ID.
+ * Gets the id of the view to use when the next focus is {@link #FOCUS_UP}.
+ * @return The next focus ID, or {@link #NO_ID} if the framework should decide automatically.
*
* @attr ref android.R.styleable#View_nextFocusUp
*/
@@ -3408,9 +3421,9 @@
}
/**
- * Set the id of the view to use for the next focus
- *
- * @param nextFocusUpId
+ * Sets the id of the view to use when the next focus is {@link #FOCUS_UP}.
+ * @param nextFocusUpId The next focus ID, or {@link #NO_ID} if the framework should
+ * decide automatically.
*
* @attr ref android.R.styleable#View_nextFocusUp
*/
@@ -3419,7 +3432,8 @@
}
/**
- * @return The user specified next focus ID.
+ * Gets the id of the view to use when the next focus is {@link #FOCUS_DOWN}.
+ * @return The next focus ID, or {@link #NO_ID} if the framework should decide automatically.
*
* @attr ref android.R.styleable#View_nextFocusDown
*/
@@ -3428,9 +3442,9 @@
}
/**
- * Set the id of the view to use for the next focus
- *
- * @param nextFocusDownId
+ * Sets the id of the view to use when the next focus is {@link #FOCUS_DOWN}.
+ * @param nextFocusDownId The next focus ID, or {@link #NO_ID} if the framework should
+ * decide automatically.
*
* @attr ref android.R.styleable#View_nextFocusDown
*/
@@ -3439,6 +3453,27 @@
}
/**
+ * Gets the id of the view to use when the next focus is {@link #FOCUS_FORWARD}.
+ * @return The next focus ID, or {@link #NO_ID} if the framework should decide automatically.
+ *
+ * @attr ref android.R.styleable#View_nextFocusForward
+ */
+ public int getNextFocusForwardId() {
+ return mNextFocusForwardId;
+ }
+
+ /**
+ * Sets the id of the view to use when the next focus is {@link #FOCUS_FORWARD}.
+ * @param nextFocusForwardId The next focus ID, or {@link #NO_ID} if the framework should
+ * decide automatically.
+ *
+ * @attr ref android.R.styleable#View_nextFocusForward
+ */
+ public void setNextFocusForwardId(int nextFocusForwardId) {
+ mNextFocusForwardId = nextFocusForwardId;
+ }
+
+ /**
* Returns the visibility of this view and all of its ancestors
*
* @return True if this view and all of its ancestors are {@link #VISIBLE}
@@ -3949,10 +3984,10 @@
/**
* If a user manually specified the next view id for a particular direction,
- * use the root to look up the view. Once a view is found, it is cached
- * for future lookups.
+ * use the root to look up the view.
* @param root The root view of the hierarchy containing this view.
- * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT
+ * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, FOCUS_FORWARD,
+ * or FOCUS_BACKWARD.
* @return The user specified next view, or null if there is none.
*/
View findUserSetNextFocus(View root, int direction) {
@@ -3969,6 +4004,18 @@
case FOCUS_DOWN:
if (mNextFocusDownId == View.NO_ID) return null;
return findViewShouldExist(root, mNextFocusDownId);
+ case FOCUS_FORWARD:
+ if (mNextFocusForwardId == View.NO_ID) return null;
+ return findViewShouldExist(root, mNextFocusForwardId);
+ case FOCUS_BACKWARD: {
+ final int id = mID;
+ return root.findViewByPredicate(new Predicate<View>() {
+ @Override
+ public boolean apply(View t) {
+ return t.mNextFocusForwardId == id;
+ }
+ });
+ }
}
return null;
}
@@ -9359,6 +9406,18 @@
}
/**
+ * {@hide}
+ * @param predicate The predicate to evaluate.
+ * @return The first view that matches the predicate or null.
+ */
+ protected View findViewByPredicateTraversal(Predicate<View> predicate) {
+ if (predicate.apply(this)) {
+ return this;
+ }
+ return null;
+ }
+
+ /**
* Look for a child view with the given id. If this view has the given
* id, return this view.
*
@@ -9387,6 +9446,18 @@
}
/**
+ * {@hide}
+ * Look for a child view that matches the specified predicate.
+ * If this view matches the predicate, return this view.
+ *
+ * @param predicate The predicate to evaluate.
+ * @return The first view that matches the predicate or null.
+ */
+ public final View findViewByPredicate(Predicate<View> predicate) {
+ return findViewByPredicateTraversal(predicate);
+ }
+
+ /**
* Sets the identifier for this view. The identifier does not have to be
* unique in this view's hierarchy. The identifier should be a positive
* number.
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index a9aa8f9..7b3cc6c 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -18,6 +18,7 @@
import android.animation.LayoutTransition;
import com.android.internal.R;
+import com.android.internal.util.Predicate;
import android.content.Context;
import android.content.res.Configuration;
@@ -2516,6 +2517,33 @@
}
/**
+ * {@hide}
+ */
+ @Override
+ protected View findViewByPredicateTraversal(Predicate<View> predicate) {
+ if (predicate.apply(this)) {
+ return this;
+ }
+
+ final View[] where = mChildren;
+ final int len = mChildrenCount;
+
+ for (int i = 0; i < len; i++) {
+ View v = where[i];
+
+ if ((v.mPrivateFlags & IS_ROOT_NAMESPACE) == 0) {
+ v = v.findViewByPredicate(predicate);
+
+ if (v != null) {
+ return v;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
* Adds a child view. If no layout parameters are already set on the child, the
* default parameters for this ViewGroup are set on the child.
*
diff --git a/core/java/android/view/ViewRoot.java b/core/java/android/view/ViewRoot.java
index 95949b9..281dd27 100644
--- a/core/java/android/view/ViewRoot.java
+++ b/core/java/android/view/ViewRoot.java
@@ -2415,81 +2415,78 @@
}
/**
- * @param keyCode The key code
- * @return True if the key is directional.
+ * Returns true if the key is used for keyboard navigation.
+ * @param keyEvent The key event.
+ * @return True if the key is used for keyboard navigation.
*/
- static boolean isDirectional(int keyCode) {
- switch (keyCode) {
+ private static boolean isNavigationKey(KeyEvent keyEvent) {
+ switch (keyEvent.getKeyCode()) {
case KeyEvent.KEYCODE_DPAD_LEFT:
case KeyEvent.KEYCODE_DPAD_RIGHT:
case KeyEvent.KEYCODE_DPAD_UP:
case KeyEvent.KEYCODE_DPAD_DOWN:
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_PAGE_UP:
+ case KeyEvent.KEYCODE_PAGE_DOWN:
+ case KeyEvent.KEYCODE_MOVE_HOME:
+ case KeyEvent.KEYCODE_MOVE_END:
+ case KeyEvent.KEYCODE_TAB:
+ case KeyEvent.KEYCODE_SPACE:
+ case KeyEvent.KEYCODE_ENTER:
return true;
}
return false;
}
/**
- * Returns true if this key is a keyboard key.
+ * Returns true if the key is used for typing.
* @param keyEvent The key event.
- * @return whether this key is a keyboard key.
+ * @return True if the key is used for typing.
*/
- private static boolean isKeyboardKey(KeyEvent keyEvent) {
- final int convertedKey = keyEvent.getUnicodeChar();
- return convertedKey > 0;
+ private static boolean isTypingKey(KeyEvent keyEvent) {
+ return keyEvent.getUnicodeChar() > 0;
}
-
-
/**
- * See if the key event means we should leave touch mode (and leave touch
- * mode if so).
+ * See if the key event means we should leave touch mode (and leave touch mode if so).
* @param event The key event.
* @return Whether this key event should be consumed (meaning the act of
* leaving touch mode alone is considered the event).
*/
private boolean checkForLeavingTouchModeAndConsume(KeyEvent event) {
- final int action = event.getAction();
- if (action != KeyEvent.ACTION_DOWN && action != KeyEvent.ACTION_MULTIPLE) {
- return false;
- }
- if ((event.getFlags()&KeyEvent.FLAG_KEEP_TOUCH_MODE) != 0) {
- return false;
- }
-
- // only relevant if we are in touch mode
+ // Only relevant in touch mode.
if (!mAttachInfo.mInTouchMode) {
return false;
}
- // if something like an edit text has focus and the user is typing,
- // leave touch mode
- //
- // note: the condition of not being a keyboard key is kind of a hacky
- // approximation of whether we think the focused view will want the
- // key; if we knew for sure whether the focused view would consume
- // the event, that would be better.
- if (isKeyboardKey(event) && mView != null && mView.hasFocus()) {
- mFocusedView = mView.findFocus();
- if ((mFocusedView instanceof ViewGroup)
- && ((ViewGroup) mFocusedView).getDescendantFocusability() ==
- ViewGroup.FOCUS_AFTER_DESCENDANTS) {
- // something has focus, but is holding it weakly as a container
- return false;
- }
- if (ensureTouchMode(false)) {
- throw new IllegalStateException("should not have changed focus "
- + "when leaving touch mode while a view has focus.");
- }
+ // Only consider leaving touch mode on DOWN or MULTIPLE actions, never on UP.
+ final int action = event.getAction();
+ if (action != KeyEvent.ACTION_DOWN && action != KeyEvent.ACTION_MULTIPLE) {
return false;
}
- if (isDirectional(event.getKeyCode())) {
- // no view has focus, so we leave touch mode (and find something
- // to give focus to). the event is consumed if we were able to
- // find something to give focus to.
+ // Don't leave touch mode if the IME told us not to.
+ if ((event.getFlags() & KeyEvent.FLAG_KEEP_TOUCH_MODE) != 0) {
+ return false;
+ }
+
+ // If the key can be used for keyboard navigation then leave touch mode
+ // and select a focused view if needed (in ensureTouchMode).
+ // When a new focused view is selected, we consume the navigation key because
+ // navigation doesn't make much sense unless a view already has focus so
+ // the key's purpose is to set focus.
+ if (isNavigationKey(event)) {
return ensureTouchMode(false);
}
+
+ // If the key can be used for typing then leave touch mode
+ // and select a focused view if needed (in ensureTouchMode).
+ // Always allow the view to process the typing key.
+ if (isTypingKey(event)) {
+ ensureTouchMode(false);
+ return false;
+ }
+
return false;
}
@@ -2640,16 +2637,31 @@
int direction = 0;
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_DPAD_LEFT:
- direction = View.FOCUS_LEFT;
+ if (event.hasNoModifiers()) {
+ direction = View.FOCUS_LEFT;
+ }
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
- direction = View.FOCUS_RIGHT;
+ if (event.hasNoModifiers()) {
+ direction = View.FOCUS_RIGHT;
+ }
break;
case KeyEvent.KEYCODE_DPAD_UP:
- direction = View.FOCUS_UP;
+ if (event.hasNoModifiers()) {
+ direction = View.FOCUS_UP;
+ }
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
- direction = View.FOCUS_DOWN;
+ if (event.hasNoModifiers()) {
+ direction = View.FOCUS_DOWN;
+ }
+ break;
+ case KeyEvent.KEYCODE_TAB:
+ if (event.hasNoModifiers()) {
+ direction = View.FOCUS_FORWARD;
+ } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
+ direction = View.FOCUS_BACKWARD;
+ }
break;
}
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index 0deb0a0..d31acec 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -4382,6 +4382,18 @@
requestLayout();
invalidate();
}
+
+ /**
+ * If there is a selection returns true.
+ * Otherwise resurrects the selection and returns false.
+ */
+ boolean ensureSelectionOnMovementKey() {
+ if (mSelectedPosition < 0) {
+ resurrectSelection();
+ return false;
+ }
+ return true;
+ }
/**
* Makes the item at the supplied position selected.
@@ -4718,9 +4730,17 @@
dX = dest.left + dest.width() / 2;
dY = dest.bottom;
break;
+ case View.FOCUS_FORWARD:
+ case View.FOCUS_BACKWARD:
+ sX = source.right + source.width() / 2;
+ sY = source.top + source.height() / 2;
+ dX = dest.left + dest.width() / 2;
+ dY = dest.top + dest.height() / 2;
+ break;
default:
throw new IllegalArgumentException("direction must be one of "
- + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
+ + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, "
+ + "FOCUS_FORWARD, FOCUS_BACKWARD}.");
}
int deltaX = dX - sX;
int deltaY = dY - sY;
diff --git a/core/java/android/widget/AutoCompleteTextView.java b/core/java/android/widget/AutoCompleteTextView.java
index e07befa..ee037cd 100644
--- a/core/java/android/widget/AutoCompleteTextView.java
+++ b/core/java/android/widget/AutoCompleteTextView.java
@@ -630,7 +630,10 @@
// from the drop down as its content
case KeyEvent.KEYCODE_ENTER:
case KeyEvent.KEYCODE_DPAD_CENTER:
- performCompletion();
+ case KeyEvent.KEYCODE_TAB:
+ if (event.hasNoModifiers()) {
+ performCompletion();
+ }
return true;
}
}
@@ -646,7 +649,9 @@
if (!isPopupShowing()) {
switch(keyCode) {
case KeyEvent.KEYCODE_DPAD_DOWN:
- performValidation();
+ if (event.hasNoModifiers()) {
+ performValidation();
+ }
}
}
diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java
index df01da7..c7addcc 100644
--- a/core/java/android/widget/GridView.java
+++ b/core/java/android/widget/GridView.java
@@ -1483,61 +1483,95 @@
int action = event.getAction();
if (action != KeyEvent.ACTION_UP) {
- if (mSelectedPosition < 0) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_DPAD_UP:
- case KeyEvent.KEYCODE_DPAD_DOWN:
- case KeyEvent.KEYCODE_DPAD_LEFT:
- case KeyEvent.KEYCODE_DPAD_RIGHT:
- case KeyEvent.KEYCODE_DPAD_CENTER:
- case KeyEvent.KEYCODE_SPACE:
- case KeyEvent.KEYCODE_ENTER:
- resurrectSelection();
- return true;
- }
- }
-
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_LEFT:
- handled = arrowScroll(FOCUS_LEFT);
+ if (event.hasNoModifiers()) {
+ handled = ensureSelectionOnMovementKey() && arrowScroll(FOCUS_LEFT);
+ }
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
- handled = arrowScroll(FOCUS_RIGHT);
+ if (event.hasNoModifiers()) {
+ handled = ensureSelectionOnMovementKey() && arrowScroll(FOCUS_RIGHT);
+ }
break;
case KeyEvent.KEYCODE_DPAD_UP:
- if (!event.isAltPressed()) {
- handled = arrowScroll(FOCUS_UP);
-
- } else {
- handled = fullScroll(FOCUS_UP);
+ if (event.hasNoModifiers()) {
+ handled = ensureSelectionOnMovementKey() && arrowScroll(FOCUS_UP);
+ } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+ handled = ensureSelectionOnMovementKey() && fullScroll(FOCUS_UP);
}
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
- if (!event.isAltPressed()) {
- handled = arrowScroll(FOCUS_DOWN);
- } else {
- handled = fullScroll(FOCUS_DOWN);
+ if (event.hasNoModifiers()) {
+ handled = ensureSelectionOnMovementKey() && arrowScroll(FOCUS_DOWN);
+ } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+ handled = ensureSelectionOnMovementKey() && fullScroll(FOCUS_DOWN);
}
break;
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_ENTER: {
- if (getChildCount() > 0 && event.getRepeatCount() == 0) {
+ if (event.hasNoModifiers()
+ && event.getRepeatCount() == 0 && getChildCount() > 0) {
+ ensureSelectionOnMovementKey();
keyPressed();
}
-
return true;
}
case KeyEvent.KEYCODE_SPACE:
if (mPopup == null || !mPopup.isShowing()) {
- if (!event.isShiftPressed()) {
- handled = pageScroll(FOCUS_DOWN);
- } else {
- handled = pageScroll(FOCUS_UP);
+ if (event.hasNoModifiers()) {
+ handled = ensureSelectionOnMovementKey() && pageScroll(FOCUS_DOWN);
+ } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
+ handled = ensureSelectionOnMovementKey() && pageScroll(FOCUS_UP);
+ }
+ }
+ break;
+
+ case KeyEvent.KEYCODE_PAGE_UP:
+ if (event.hasNoModifiers()) {
+ handled = ensureSelectionOnMovementKey() && pageScroll(FOCUS_UP);
+ } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+ handled = ensureSelectionOnMovementKey() && fullScroll(FOCUS_UP);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_PAGE_DOWN:
+ if (event.hasNoModifiers()) {
+ handled = ensureSelectionOnMovementKey() && pageScroll(FOCUS_DOWN);
+ } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+ handled = ensureSelectionOnMovementKey() && fullScroll(FOCUS_DOWN);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_MOVE_HOME:
+ if (event.hasNoModifiers()) {
+ handled = ensureSelectionOnMovementKey() && fullScroll(FOCUS_UP);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_MOVE_END:
+ if (event.hasNoModifiers()) {
+ handled = ensureSelectionOnMovementKey() && fullScroll(FOCUS_DOWN);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_TAB:
+ // XXX Sometimes it is useful to be able to TAB through the items in
+ // a GridView sequentially. Unfortunately this can create an
+ // asymmetry in TAB navigation order unless the list selection
+ // always reverts to the top or bottom when receiving TAB focus from
+ // another widget. Leaving this behavior disabled for now but
+ // perhaps it should be configurable (and more comprehensive).
+ if (false) {
+ if (event.hasNoModifiers()) {
+ handled = ensureSelectionOnMovementKey() && sequenceScroll(FOCUS_FORWARD);
+ } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
+ handled = ensureSelectionOnMovementKey() && sequenceScroll(FOCUS_BACKWARD);
}
}
break;
@@ -1686,6 +1720,64 @@
return moved;
}
+ /**
+ * Goes to the next or previous item according to the order set by the
+ * adapter.
+ */
+ boolean sequenceScroll(int direction) {
+ int selectedPosition = mSelectedPosition;
+ int numColumns = mNumColumns;
+ int count = mItemCount;
+
+ int startOfRow;
+ int endOfRow;
+ if (!mStackFromBottom) {
+ startOfRow = (selectedPosition / numColumns) * numColumns;
+ endOfRow = Math.min(startOfRow + numColumns - 1, count - 1);
+ } else {
+ int invertedSelection = count - 1 - selectedPosition;
+ endOfRow = count - 1 - (invertedSelection / numColumns) * numColumns;
+ startOfRow = Math.max(0, endOfRow - numColumns + 1);
+ }
+
+ boolean moved = false;
+ boolean showScroll = false;
+ switch (direction) {
+ case FOCUS_FORWARD:
+ if (selectedPosition < count - 1) {
+ // Move to the next item.
+ mLayoutMode = LAYOUT_MOVE_SELECTION;
+ setSelectionInt(selectedPosition + 1);
+ moved = true;
+ // Show the scrollbar only if changing rows.
+ showScroll = selectedPosition == endOfRow;
+ }
+ break;
+
+ case FOCUS_BACKWARD:
+ if (selectedPosition > 0) {
+ // Move to the previous item.
+ mLayoutMode = LAYOUT_MOVE_SELECTION;
+ setSelectionInt(selectedPosition - 1);
+ moved = true;
+ // Show the scrollbar only if changing rows.
+ showScroll = selectedPosition == startOfRow;
+ }
+ break;
+ }
+
+ if (moved) {
+ playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
+ invokeOnItemScrollListener();
+ }
+
+ if (showScroll) {
+ awakenScrollBars();
+ }
+
+ return moved;
+ }
+
@Override
protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
@@ -1729,7 +1821,7 @@
* change is coming from?
* @param childIndex The index to check.
* @param direction The direction, one of
- * {FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}
+ * {FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, FOCUS_FORWARD, FOCUS_BACKWARD}
* @return Whether childIndex is a candidate.
*/
private boolean isCandidateSelection(int childIndex, int direction) {
@@ -1761,9 +1853,16 @@
case View.FOCUS_UP:
// coming from bottom, need to be in last row
return rowEnd == count - 1;
+ case View.FOCUS_FORWARD:
+ // coming from top-left, need to be first in top row
+ return childIndex == rowStart && rowStart == 0;
+ case View.FOCUS_BACKWARD:
+ // coming from bottom-right, need to be last in bottom row
+ return childIndex == rowEnd && rowEnd == count - 1;
default:
throw new IllegalArgumentException("direction must be one of "
- + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
+ + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, "
+ + "FOCUS_FORWARD, FOCUS_BACKWARD}.");
}
}
diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java
index 6894c99..1f95159 100644
--- a/core/java/android/widget/ListView.java
+++ b/core/java/android/widget/ListView.java
@@ -17,6 +17,7 @@
package android.widget;
import com.android.internal.R;
+import com.android.internal.util.Predicate;
import com.google.android.collect.Lists;
import android.content.Context;
@@ -2077,51 +2078,47 @@
int action = event.getAction();
if (action != KeyEvent.ACTION_UP) {
- if (mSelectedPosition < 0) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_DPAD_UP:
- case KeyEvent.KEYCODE_DPAD_DOWN:
- case KeyEvent.KEYCODE_DPAD_CENTER:
- case KeyEvent.KEYCODE_ENTER:
- case KeyEvent.KEYCODE_SPACE:
- if (resurrectSelection()) {
- return true;
- }
- }
- }
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_UP:
- if (!event.isAltPressed()) {
- while (count > 0) {
- handled = arrowScroll(FOCUS_UP);
- count--;
+ if (event.hasNoModifiers()) {
+ if (ensureSelectionOnMovementKey()) {
+ while (count-- > 0) {
+ handled |= arrowScroll(FOCUS_UP);
+ }
}
- } else {
- handled = fullScroll(FOCUS_UP);
+ } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+ handled = ensureSelectionOnMovementKey() && fullScroll(FOCUS_UP);
}
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
- if (!event.isAltPressed()) {
- while (count > 0) {
- handled = arrowScroll(FOCUS_DOWN);
- count--;
+ if (event.hasNoModifiers()) {
+ if (ensureSelectionOnMovementKey()) {
+ while (count-- > 0) {
+ handled |= arrowScroll(FOCUS_DOWN);
+ }
}
- } else {
- handled = fullScroll(FOCUS_DOWN);
+ } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+ handled = ensureSelectionOnMovementKey() && fullScroll(FOCUS_DOWN);
}
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
- handled = handleHorizontalFocusWithinListItem(View.FOCUS_LEFT);
+ if (event.hasNoModifiers()) {
+ handled = handleHorizontalFocusWithinListItem(View.FOCUS_LEFT);
+ }
break;
+
case KeyEvent.KEYCODE_DPAD_RIGHT:
- handled = handleHorizontalFocusWithinListItem(View.FOCUS_RIGHT);
+ if (event.hasNoModifiers()) {
+ handled = handleHorizontalFocusWithinListItem(View.FOCUS_RIGHT);
+ }
break;
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_ENTER:
if (mItemCount > 0 && event.getRepeatCount() == 0) {
+ ensureSelectionOnMovementKey();
keyPressed();
}
handled = true;
@@ -2129,14 +2126,58 @@
case KeyEvent.KEYCODE_SPACE:
if (mPopup == null || !mPopup.isShowing()) {
- if (!event.isShiftPressed()) {
- pageScroll(FOCUS_DOWN);
- } else {
- pageScroll(FOCUS_UP);
+ if (event.hasNoModifiers()) {
+ handled = ensureSelectionOnMovementKey() && pageScroll(FOCUS_DOWN);
+ } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
+ handled = ensureSelectionOnMovementKey() && pageScroll(FOCUS_UP);
}
handled = true;
}
break;
+
+ case KeyEvent.KEYCODE_PAGE_UP:
+ if (event.hasNoModifiers()) {
+ handled = ensureSelectionOnMovementKey() && pageScroll(FOCUS_UP);
+ } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+ handled = ensureSelectionOnMovementKey() && fullScroll(FOCUS_UP);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_PAGE_DOWN:
+ if (event.hasNoModifiers()) {
+ handled = ensureSelectionOnMovementKey() && pageScroll(FOCUS_DOWN);
+ } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+ handled = ensureSelectionOnMovementKey() && fullScroll(FOCUS_DOWN);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_MOVE_HOME:
+ if (event.hasNoModifiers()) {
+ handled = ensureSelectionOnMovementKey() && fullScroll(FOCUS_UP);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_MOVE_END:
+ if (event.hasNoModifiers()) {
+ handled = ensureSelectionOnMovementKey() && fullScroll(FOCUS_DOWN);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_TAB:
+ // XXX Sometimes it is useful to be able to TAB through the items in
+ // a ListView sequentially. Unfortunately this can create an
+ // asymmetry in TAB navigation order unless the list selection
+ // always reverts to the top or bottom when receiving TAB focus from
+ // another widget. Leaving this behavior disabled for now but
+ // perhaps it should be configurable (and more comprehensive).
+ if (false) {
+ if (event.hasNoModifiers()) {
+ handled = ensureSelectionOnMovementKey() && arrowScroll(FOCUS_DOWN);
+ } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
+ handled = ensureSelectionOnMovementKey() && arrowScroll(FOCUS_UP);
+ }
+ }
+ break;
}
}
@@ -3401,7 +3442,7 @@
}
/* (non-Javadoc)
- * @see android.view.View#findViewWithTag(String)
+ * @see android.view.View#findViewWithTag(Object)
* First look in our children, then in any header and footer views that may be scrolled off.
*/
@Override
@@ -3409,12 +3450,12 @@
View v;
v = super.findViewWithTagTraversal(tag);
if (v == null) {
- v = findViewTagInHeadersOrFooters(mHeaderViewInfos, tag);
+ v = findViewWithTagInHeadersOrFooters(mHeaderViewInfos, tag);
if (v != null) {
return v;
}
- v = findViewTagInHeadersOrFooters(mFooterViewInfos, tag);
+ v = findViewWithTagInHeadersOrFooters(mFooterViewInfos, tag);
if (v != null) {
return v;
}
@@ -3426,7 +3467,7 @@
*
* Look in the passed in list of headers or footers for the view with the tag.
*/
- View findViewTagInHeadersOrFooters(ArrayList<FixedViewInfo> where, Object tag) {
+ View findViewWithTagInHeadersOrFooters(ArrayList<FixedViewInfo> where, Object tag) {
if (where != null) {
int len = where.size();
View v;
@@ -3446,6 +3487,55 @@
return null;
}
+ /**
+ * @hide
+ * @see android.view.View#findViewByPredicate(Predicate)
+ * First look in our children, then in any header and footer views that may be scrolled off.
+ */
+ @Override
+ protected View findViewByPredicateTraversal(Predicate<View> predicate) {
+ View v;
+ v = super.findViewByPredicateTraversal(predicate);
+ if (v == null) {
+ v = findViewByPredicateInHeadersOrFooters(mHeaderViewInfos, predicate);
+ if (v != null) {
+ return v;
+ }
+
+ v = findViewByPredicateInHeadersOrFooters(mFooterViewInfos, predicate);
+ if (v != null) {
+ return v;
+ }
+ }
+ return v;
+ }
+
+ /* (non-Javadoc)
+ *
+ * Look in the passed in list of headers or footers for the first view that matches
+ * the predicate.
+ */
+ View findViewByPredicateInHeadersOrFooters(ArrayList<FixedViewInfo> where,
+ Predicate<View> predicate) {
+ if (where != null) {
+ int len = where.size();
+ View v;
+
+ for (int i = 0; i < len; i++) {
+ v = where.get(i).view;
+
+ if (!v.isRootNamespace()) {
+ v = v.findViewByPredicate(predicate);
+
+ if (v != null) {
+ return v;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (mItemsCanFocus && ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
diff --git a/core/java/android/widget/SearchView.java b/core/java/android/widget/SearchView.java
index dcd58b0..3bf5af3 100644
--- a/core/java/android/widget/SearchView.java
+++ b/core/java/android/widget/SearchView.java
@@ -623,14 +623,16 @@
// If there is text in the query box, handle enter, and action keys
// The search key is handled by the dialog's onKeyDown().
- if (!mQueryTextView.isEmpty()) {
- if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) {
- v.cancelLongPress();
+ if (!mQueryTextView.isEmpty() && event.hasNoModifiers()) {
+ if (event.getAction() == KeyEvent.ACTION_UP) {
+ if (keyCode == KeyEvent.KEYCODE_ENTER) {
+ v.cancelLongPress();
- // Launch as a regular search.
- launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, mQueryTextView.getText()
- .toString());
- return true;
+ // Launch as a regular search.
+ launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, mQueryTextView.getText()
+ .toString());
+ return true;
+ }
}
if (event.getAction() == KeyEvent.ACTION_DOWN) {
SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
@@ -658,11 +660,11 @@
if (mSuggestionsAdapter == null) {
return false;
}
- if (event.getAction() == KeyEvent.ACTION_DOWN) {
-
+ if (event.getAction() == KeyEvent.ACTION_DOWN && event.hasNoModifiers()) {
// First, check for enter or search (both of which we'll treat as a
// "click")
- if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) {
+ if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH
+ || keyCode == KeyEvent.KEYCODE_TAB) {
int position = mQueryTextView.getListSelection();
return onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null);
}
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index d03b0ea..052760f 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -4586,9 +4586,8 @@
if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
int variation = mInputType & EditorInfo.TYPE_MASK_VARIATION;
-
- if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS ||
- variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT) {
+ if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
+ || variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT) {
return true;
}
}
@@ -4596,6 +4595,23 @@
return false;
}
+ /**
+ * Returns true if pressing TAB in this field advances focus instead
+ * of inserting the character. Insert tabs only in multi-line editors.
+ */
+ private boolean shouldAdvanceFocusOnTab() {
+ if (mInput != null && !mSingleLine) {
+ if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
+ int variation = mInputType & EditorInfo.TYPE_MASK_VARIATION;
+ if (variation == EditorInfo.TYPE_TEXT_FLAG_IME_MULTI_LINE
+ || variation == EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
private int doKeyDown(int keyCode, KeyEvent event, KeyEvent otherEvent) {
if (!isEnabled()) {
return 0;
@@ -4604,16 +4620,12 @@
switch (keyCode) {
case KeyEvent.KEYCODE_ENTER:
mEnterKeyIsDown = true;
- // If ALT modifier is held, then we always insert a
- // newline character.
- if ((event.getMetaState()&KeyEvent.META_ALT_ON) == 0) {
-
+ if (event.hasNoModifiers()) {
// When mInputContentType is set, we know that we are
// running in a "modern" cupcake environment, so don't need
// to worry about the application trying to capture
// enter key events.
if (mInputContentType != null) {
-
// If there is an action listener, given them a
// chance to consume the event.
if (mInputContentType.onEditorActionListener != null &&
@@ -4624,11 +4636,11 @@
return -1;
}
}
-
+
// If our editor should move focus when enter is pressed, or
// this is a generated event from an IME action button, then
// don't let it be inserted into the text.
- if ((event.getFlags()&KeyEvent.FLAG_EDITOR_ACTION) != 0
+ if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0
|| shouldAdvanceFocusOnEnter()) {
return -1;
}
@@ -4637,8 +4649,18 @@
case KeyEvent.KEYCODE_DPAD_CENTER:
mDPadCenterIsDown = true;
- if (shouldAdvanceFocusOnEnter()) {
- return 0;
+ if (event.hasNoModifiers()) {
+ if (shouldAdvanceFocusOnEnter()) {
+ return 0;
+ }
+ }
+ break;
+
+ case KeyEvent.KEYCODE_TAB:
+ if (event.hasNoModifiers() || event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
+ if (shouldAdvanceFocusOnTab()) {
+ return 0;
+ }
}
break;
@@ -4733,76 +4755,80 @@
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_CENTER:
mDPadCenterIsDown = false;
- /*
- * If there is a click listener, just call through to
- * super, which will invoke it.
- *
- * If there isn't a click listener, try to show the soft
- * input method. (It will also
- * call performClick(), but that won't do anything in
- * this case.)
- */
- if (mOnClickListener == null) {
- if (mMovement != null && mText instanceof Editable
- && mLayout != null && onCheckIsTextEditor()) {
- InputMethodManager imm = (InputMethodManager)
- getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
- imm.showSoftInput(this, 0);
- }
- }
- return super.onKeyUp(keyCode, event);
-
- case KeyEvent.KEYCODE_ENTER:
- mEnterKeyIsDown = false;
- if (mInputContentType != null
- && mInputContentType.onEditorActionListener != null
- && mInputContentType.enterDown) {
- mInputContentType.enterDown = false;
- if (mInputContentType.onEditorActionListener.onEditorAction(
- this, EditorInfo.IME_NULL, event)) {
- return true;
- }
- }
-
- if ((event.getFlags()&KeyEvent.FLAG_EDITOR_ACTION) != 0
- || shouldAdvanceFocusOnEnter()) {
+ if (event.hasNoModifiers()) {
/*
* If there is a click listener, just call through to
* super, which will invoke it.
*
- * If there isn't a click listener, try to advance focus,
- * but still call through to super, which will reset the
- * pressed state and longpress state. (It will also
+ * If there isn't a click listener, try to show the soft
+ * input method. (It will also
* call performClick(), but that won't do anything in
* this case.)
*/
if (mOnClickListener == null) {
- View v = focusSearch(FOCUS_DOWN);
+ if (mMovement != null && mText instanceof Editable
+ && mLayout != null && onCheckIsTextEditor()) {
+ InputMethodManager imm = (InputMethodManager)
+ getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.showSoftInput(this, 0);
+ }
+ }
+ }
+ return super.onKeyUp(keyCode, event);
- if (v != null) {
- if (!v.requestFocus(FOCUS_DOWN)) {
- throw new IllegalStateException("focus search returned a view " +
- "that wasn't able to take focus!");
- }
-
- /*
- * Return true because we handled the key; super
- * will return false because there was no click
- * listener.
- */
- super.onKeyUp(keyCode, event);
+ case KeyEvent.KEYCODE_ENTER:
+ mEnterKeyIsDown = false;
+ if (event.hasNoModifiers()) {
+ if (mInputContentType != null
+ && mInputContentType.onEditorActionListener != null
+ && mInputContentType.enterDown) {
+ mInputContentType.enterDown = false;
+ if (mInputContentType.onEditorActionListener.onEditorAction(
+ this, EditorInfo.IME_NULL, event)) {
return true;
- } else if ((event.getFlags()
- & KeyEvent.FLAG_EDITOR_ACTION) != 0) {
- // No target for next focus, but make sure the IME
- // if this came from it.
- InputMethodManager imm = InputMethodManager.peekInstance();
- if (imm != null) {
- imm.hideSoftInputFromWindow(getWindowToken(), 0);
- }
}
}
+ if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0
+ || shouldAdvanceFocusOnEnter()) {
+ /*
+ * If there is a click listener, just call through to
+ * super, which will invoke it.
+ *
+ * If there isn't a click listener, try to advance focus,
+ * but still call through to super, which will reset the
+ * pressed state and longpress state. (It will also
+ * call performClick(), but that won't do anything in
+ * this case.)
+ */
+ if (mOnClickListener == null) {
+ View v = focusSearch(FOCUS_DOWN);
+
+ if (v != null) {
+ if (!v.requestFocus(FOCUS_DOWN)) {
+ throw new IllegalStateException(
+ "focus search returned a view " +
+ "that wasn't able to take focus!");
+ }
+
+ /*
+ * Return true because we handled the key; super
+ * will return false because there was no click
+ * listener.
+ */
+ super.onKeyUp(keyCode, event);
+ return true;
+ } else if ((event.getFlags()
+ & KeyEvent.FLAG_EDITOR_ACTION) != 0) {
+ // No target for next focus, but make sure the IME
+ // if this came from it.
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null) {
+ imm.hideSoftInputFromWindow(getWindowToken(), 0);
+ }
+ }
+ }
+ }
return super.onKeyUp(keyCode, event);
}
break;
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 873f539..9598f96 100755
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -1635,6 +1635,14 @@
will result when the reference is accessed.-->
<attr name="nextFocusDown" format="reference"/>
+ <!-- Defines the next view to give focus to when the next focus is
+ {@link android.view.View#FOCUS_FORWARD}
+
+ If the reference refers to a view that does not exist or is part
+ of a hierarchy that is invisible, a {@link java.lang.RuntimeException}
+ will result when the reference is accessed.-->
+ <attr name="nextFocusForward" format="reference"/>
+
<!-- Defines whether this view reacts to click events. -->
<attr name="clickable" format="boolean" />
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index 7e06c86..5aed668 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -1408,6 +1408,7 @@
<public type="attr" name="fastScrollTrackDrawable" />
<public type="attr" name="fastScrollOverlayPosition" />
<public type="attr" name="customTokens" />
+ <public type="attr" name="nextFocusForward" />
<public type="anim" name="animator_fade_in" />
<public type="anim" name="animator_fade_out" />