From 8957f2ddda08e891475e64552ffa225ca2fccedb Mon Sep 17 00:00:00 2001 From: Vadim Tryshev Date: Wed, 21 Dec 2016 19:22:26 -0800 Subject: [PATCH] Introducing teleportation between sections. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The key combos differ from the ones in the spec because key combos including Meta key don’t get delivered to apps. To fix this, I’ll implement necessary plumbing after the feature freeze. Meanwhile, temporary combos are used. Given that the section and cluster teleportation have a lot in common, I’m not introducing new methods, but adding a param to the cluster teleportation ones. I should have also changed the names to something like findNextKeyboardNavigationCluster => findNextFocusGroup, where “FocusGroup” is a generalized name for clusters and sections. However, that name depends on b/33708251, so I’m not doing it now. I don’t rename existing identifiers, and using “focusGroup” for new ones. Admittedly, this creates mess that will be resolved based on the outcome of the mentioned bug. Bug: 32151632 Test: Manual checks; CTS are coming after feature freeze Change-Id: I01b5d6e5a9689b8f643fa4af695d2ce61265f374 --- api/current.txt | 11 ++-- api/system-current.txt | 11 ++-- api/test-current.txt | 11 ++-- core/java/android/view/FocusFinder.java | 74 ++++++++++++++++++------ core/java/android/view/View.java | 65 ++++++++++++++++++--- core/java/android/view/ViewGroup.java | 29 +++------- core/java/android/view/ViewParent.java | 5 +- core/java/android/view/ViewRootImpl.java | 39 +++++++++---- 8 files changed, 174 insertions(+), 71 deletions(-) diff --git a/api/current.txt b/api/current.txt index ec16f5a5f56b8..443ac9dbaac9f 100644 --- a/api/current.txt +++ b/api/current.txt @@ -41816,7 +41816,7 @@ package android.view { method public android.view.View findNearestTouchable(android.view.ViewGroup, int, int, int, int[]); method public final android.view.View findNextFocus(android.view.ViewGroup, android.view.View, int); method public android.view.View findNextFocusFromRect(android.view.ViewGroup, android.graphics.Rect, int); - method public android.view.View findNextKeyboardNavigationCluster(android.view.ViewGroup, android.view.View, int); + method public android.view.View findNextKeyboardNavigationCluster(int, android.view.View, android.view.View, int); method public static android.view.FocusFinder getInstance(); } @@ -43109,7 +43109,7 @@ package android.view { method public void addChildrenForAccessibility(java.util.ArrayList); method public void addFocusables(java.util.ArrayList, int); method public void addFocusables(java.util.ArrayList, int, int); - method public void addKeyboardNavigationClusters(java.util.Collection, int); + method public void addKeyboardNavigationClusters(int, java.util.Collection, int); method public void addOnAttachStateChangeListener(android.view.View.OnAttachStateChangeListener); method public void addOnLayoutChangeListener(android.view.View.OnLayoutChangeListener); method public void addTouchables(java.util.ArrayList); @@ -43397,7 +43397,7 @@ package android.view { method public boolean isVerticalFadingEdgeEnabled(); method public boolean isVerticalScrollBarEnabled(); method public void jumpDrawablesToCurrentState(); - method public android.view.View keyboardNavigationClusterSearch(int); + method public android.view.View keyboardNavigationClusterSearch(int, android.view.View, int); method public void layout(int, int, int, int); method public final void measure(int, int); method protected static int[] mergeDrawableStates(int[], int[]); @@ -43679,6 +43679,8 @@ package android.view { field public static final int FOCUS_BACKWARD = 1; // 0x1 field public static final int FOCUS_DOWN = 130; // 0x82 field public static final int FOCUS_FORWARD = 2; // 0x2 + field public static final int FOCUS_GROUP_CLUSTER = 1; // 0x1 + field public static final int FOCUS_GROUP_SECTION = 2; // 0x2 field public static final int FOCUS_LEFT = 17; // 0x11 field public static final int FOCUS_RIGHT = 66; // 0x42 field public static final int FOCUS_UP = 33; // 0x21 @@ -44046,7 +44048,6 @@ package android.view { method protected deprecated boolean isChildrenDrawnWithCacheEnabled(); method public boolean isMotionEventSplittingEnabled(); method public boolean isTransitionGroup(); - method public android.view.View keyboardNavigationClusterSearch(android.view.View, int); method public final void layout(int, int, int, int); method protected void measureChild(android.view.View, int, int); method protected void measureChildWithMargins(android.view.View, int, int, int, int); @@ -44209,7 +44210,7 @@ package android.view { method public abstract boolean isLayoutRequested(); method public abstract boolean isTextAlignmentResolved(); method public abstract boolean isTextDirectionResolved(); - method public abstract android.view.View keyboardNavigationClusterSearch(android.view.View, int); + method public abstract android.view.View keyboardNavigationClusterSearch(int, android.view.View, int); method public abstract void notifySubtreeAccessibilityStateChanged(android.view.View, android.view.View, int); method public abstract boolean onNestedFling(android.view.View, float, float, boolean); method public abstract boolean onNestedPreFling(android.view.View, float, float); diff --git a/api/system-current.txt b/api/system-current.txt index 4bba111f93921..bc0e3ef5ac274 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -45008,7 +45008,7 @@ package android.view { method public android.view.View findNearestTouchable(android.view.ViewGroup, int, int, int, int[]); method public final android.view.View findNextFocus(android.view.ViewGroup, android.view.View, int); method public android.view.View findNextFocusFromRect(android.view.ViewGroup, android.graphics.Rect, int); - method public android.view.View findNextKeyboardNavigationCluster(android.view.ViewGroup, android.view.View, int); + method public android.view.View findNextKeyboardNavigationCluster(int, android.view.View, android.view.View, int); method public static android.view.FocusFinder getInstance(); } @@ -46301,7 +46301,7 @@ package android.view { method public void addChildrenForAccessibility(java.util.ArrayList); method public void addFocusables(java.util.ArrayList, int); method public void addFocusables(java.util.ArrayList, int, int); - method public void addKeyboardNavigationClusters(java.util.Collection, int); + method public void addKeyboardNavigationClusters(int, java.util.Collection, int); method public void addOnAttachStateChangeListener(android.view.View.OnAttachStateChangeListener); method public void addOnLayoutChangeListener(android.view.View.OnLayoutChangeListener); method public void addTouchables(java.util.ArrayList); @@ -46589,7 +46589,7 @@ package android.view { method public boolean isVerticalFadingEdgeEnabled(); method public boolean isVerticalScrollBarEnabled(); method public void jumpDrawablesToCurrentState(); - method public android.view.View keyboardNavigationClusterSearch(int); + method public android.view.View keyboardNavigationClusterSearch(int, android.view.View, int); method public void layout(int, int, int, int); method public final void measure(int, int); method protected static int[] mergeDrawableStates(int[], int[]); @@ -46871,6 +46871,8 @@ package android.view { field public static final int FOCUS_BACKWARD = 1; // 0x1 field public static final int FOCUS_DOWN = 130; // 0x82 field public static final int FOCUS_FORWARD = 2; // 0x2 + field public static final int FOCUS_GROUP_CLUSTER = 1; // 0x1 + field public static final int FOCUS_GROUP_SECTION = 2; // 0x2 field public static final int FOCUS_LEFT = 17; // 0x11 field public static final int FOCUS_RIGHT = 66; // 0x42 field public static final int FOCUS_UP = 33; // 0x21 @@ -47238,7 +47240,6 @@ package android.view { method protected deprecated boolean isChildrenDrawnWithCacheEnabled(); method public boolean isMotionEventSplittingEnabled(); method public boolean isTransitionGroup(); - method public android.view.View keyboardNavigationClusterSearch(android.view.View, int); method public final void layout(int, int, int, int); method protected void measureChild(android.view.View, int, int); method protected void measureChildWithMargins(android.view.View, int, int, int, int); @@ -47401,7 +47402,7 @@ package android.view { method public abstract boolean isLayoutRequested(); method public abstract boolean isTextAlignmentResolved(); method public abstract boolean isTextDirectionResolved(); - method public abstract android.view.View keyboardNavigationClusterSearch(android.view.View, int); + method public abstract android.view.View keyboardNavigationClusterSearch(int, android.view.View, int); method public abstract void notifySubtreeAccessibilityStateChanged(android.view.View, android.view.View, int); method public abstract boolean onNestedFling(android.view.View, float, float, boolean); method public abstract boolean onNestedPreFling(android.view.View, float, float); diff --git a/api/test-current.txt b/api/test-current.txt index d4fb027bcb4de..c545584380d17 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -42103,7 +42103,7 @@ package android.view { method public android.view.View findNearestTouchable(android.view.ViewGroup, int, int, int, int[]); method public final android.view.View findNextFocus(android.view.ViewGroup, android.view.View, int); method public android.view.View findNextFocusFromRect(android.view.ViewGroup, android.graphics.Rect, int); - method public android.view.View findNextKeyboardNavigationCluster(android.view.ViewGroup, android.view.View, int); + method public android.view.View findNextKeyboardNavigationCluster(int, android.view.View, android.view.View, int); method public static android.view.FocusFinder getInstance(); } @@ -43398,7 +43398,7 @@ package android.view { method public void addChildrenForAccessibility(java.util.ArrayList); method public void addFocusables(java.util.ArrayList, int); method public void addFocusables(java.util.ArrayList, int, int); - method public void addKeyboardNavigationClusters(java.util.Collection, int); + method public void addKeyboardNavigationClusters(int, java.util.Collection, int); method public void addOnAttachStateChangeListener(android.view.View.OnAttachStateChangeListener); method public void addOnLayoutChangeListener(android.view.View.OnLayoutChangeListener); method public void addTouchables(java.util.ArrayList); @@ -43687,7 +43687,7 @@ package android.view { method public boolean isVerticalFadingEdgeEnabled(); method public boolean isVerticalScrollBarEnabled(); method public void jumpDrawablesToCurrentState(); - method public android.view.View keyboardNavigationClusterSearch(int); + method public android.view.View keyboardNavigationClusterSearch(int, android.view.View, int); method public void layout(int, int, int, int); method public final void measure(int, int); method protected static int[] mergeDrawableStates(int[], int[]); @@ -43969,6 +43969,8 @@ package android.view { field public static final int FOCUS_BACKWARD = 1; // 0x1 field public static final int FOCUS_DOWN = 130; // 0x82 field public static final int FOCUS_FORWARD = 2; // 0x2 + field public static final int FOCUS_GROUP_CLUSTER = 1; // 0x1 + field public static final int FOCUS_GROUP_SECTION = 2; // 0x2 field public static final int FOCUS_LEFT = 17; // 0x11 field public static final int FOCUS_RIGHT = 66; // 0x42 field public static final int FOCUS_UP = 33; // 0x21 @@ -44340,7 +44342,6 @@ package android.view { method protected deprecated boolean isChildrenDrawnWithCacheEnabled(); method public boolean isMotionEventSplittingEnabled(); method public boolean isTransitionGroup(); - method public android.view.View keyboardNavigationClusterSearch(android.view.View, int); method public final void layout(int, int, int, int); method protected void measureChild(android.view.View, int, int); method protected void measureChildWithMargins(android.view.View, int, int, int, int); @@ -44503,7 +44504,7 @@ package android.view { method public abstract boolean isLayoutRequested(); method public abstract boolean isTextAlignmentResolved(); method public abstract boolean isTextDirectionResolved(); - method public abstract android.view.View keyboardNavigationClusterSearch(android.view.View, int); + method public abstract android.view.View keyboardNavigationClusterSearch(int, android.view.View, int); method public abstract void notifySubtreeAccessibilityStateChanged(android.view.View, android.view.View, int); method public abstract boolean onNestedFling(android.view.View, float, float, boolean); method public abstract boolean onNestedPreFling(android.view.View, float, float); diff --git a/core/java/android/view/FocusFinder.java b/core/java/android/view/FocusFinder.java index 3f3d5190fac70..1a4f0d1ae803d 100644 --- a/core/java/android/view/FocusFinder.java +++ b/core/java/android/view/FocusFinder.java @@ -16,12 +16,16 @@ package android.view; +import static android.view.View.FOCUS_GROUP_CLUSTER; +import static android.view.View.FOCUS_GROUP_SECTION; + import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Rect; import android.util.ArrayMap; import android.util.SparseArray; import android.util.SparseBooleanArray; +import android.view.View.FocusGroupType; import java.util.ArrayList; import java.util.Collections; @@ -107,21 +111,26 @@ public class FocusFinder { /** * Find the root of the next keyboard navigation cluster after the current one. - * @param root Thew view tree to look inside. Cannot be null + * @param focusGroupType Type of the focus group + * @param root The view tree to look inside. Cannot be null * @param currentCluster The starting point of the search. Null means the default cluster * @param direction Direction to look * @return The next cluster, or null if none exists */ public View findNextKeyboardNavigationCluster( - @NonNull ViewGroup root, @Nullable View currentCluster, int direction) { + @FocusGroupType int focusGroupType, + @NonNull View root, + @Nullable View currentCluster, + int direction) { View next = null; final ArrayList clusters = mTempList; try { clusters.clear(); - root.addKeyboardNavigationClusters(clusters, direction); + root.addKeyboardNavigationClusters(focusGroupType, clusters, direction); if (!clusters.isEmpty()) { - next = findNextKeyboardNavigationCluster(root, currentCluster, clusters, direction); + next = findNextKeyboardNavigationCluster( + focusGroupType, root, currentCluster, clusters, direction); } } finally { clusters.clear(); @@ -197,19 +206,25 @@ public class FocusFinder { } } - private View findNextKeyboardNavigationCluster(ViewGroup root, View currentCluster, - List clusters, int direction) { + private View findNextKeyboardNavigationCluster( + @FocusGroupType int focusGroupType, + View root, + View currentCluster, + List clusters, + int direction) { final int count = clusters.size(); switch (direction) { case View.FOCUS_FORWARD: case View.FOCUS_DOWN: case View.FOCUS_RIGHT: - return getNextKeyboardNavigationCluster(root, currentCluster, clusters, count); + return getNextKeyboardNavigationCluster( + focusGroupType, root, currentCluster, clusters, count); case View.FOCUS_BACKWARD: case View.FOCUS_UP: case View.FOCUS_LEFT: - return getPreviousKeyboardNavigationCluster(root, currentCluster, clusters, count); + return getPreviousKeyboardNavigationCluster( + focusGroupType, root, currentCluster, clusters, count); default: throw new IllegalArgumentException("Unknown direction: " + direction); } @@ -315,8 +330,12 @@ public class FocusFinder { return null; } - private static View getNextKeyboardNavigationCluster(ViewGroup root, View currentCluster, - List clusters, int count) { + private static View getNextKeyboardNavigationCluster( + @FocusGroupType int focusGroupType, + View root, + View currentCluster, + List clusters, + int count) { if (currentCluster == null) { // The current cluster is the default one. // The next cluster after the default one is the first one. @@ -330,12 +349,25 @@ public class FocusFinder { return clusters.get(position + 1); } - // The current cluster is the last one. The next one is the default one, i.e. the root. - return root; + switch (focusGroupType) { + case FOCUS_GROUP_CLUSTER: + // The current cluster is the last one. The next one is the default one, i.e. the + // root. + return root; + case FOCUS_GROUP_SECTION: + // There is no "default section", hence returning the first one. + return clusters.get(0); + default: + throw new IllegalArgumentException("Unknown focus group type: " + focusGroupType); + } } - private static View getPreviousKeyboardNavigationCluster(ViewGroup root, View currentCluster, - List clusters, int count) { + private static View getPreviousKeyboardNavigationCluster( + @FocusGroupType int focusGroupType, + View root, + View currentCluster, + List clusters, + int count) { if (currentCluster == null) { // The current cluster is the default one. // The previous cluster before the default one is the last one. @@ -349,9 +381,17 @@ public class FocusFinder { return clusters.get(position - 1); } - // The current cluster is the first one. The previous one is the default one, i.e. the - // root. - return root; + switch (focusGroupType) { + case FOCUS_GROUP_CLUSTER: + // The current cluster is the first one. The previous one is the default one, i.e. + // the root. + return root; + case FOCUS_GROUP_SECTION: + // There is no "default section", hence returning the last one. + return clusters.get(count - 1); + default: + throw new IllegalArgumentException("Unknown focus group type: " + focusGroupType); + } } /** diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index f51e029321471..aa941b8fcfb4a 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -1248,6 +1248,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @Retention(RetentionPolicy.SOURCE) public @interface FocusRealDirection {} // Like @FocusDirection, but without forward/backward + /** @hide */ + @IntDef({ + FOCUS_GROUP_CLUSTER, + FOCUS_GROUP_SECTION + }) + @Retention(RetentionPolicy.SOURCE) + public @interface FocusGroupType {} + /** * Use with {@link #focusSearch(int)}. Move focus to the previous selectable * item. @@ -1280,6 +1288,18 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public static final int FOCUS_DOWN = 0x00000082; + /** + * Use with {@link #keyboardNavigationClusterSearch(int, View, int)}. Search for a keyboard + * navigation cluster. + */ + public static final int FOCUS_GROUP_CLUSTER = 1; + + /** + * Use with {@link #keyboardNavigationClusterSearch(int, View, int)}. Search for a keyboard + * navigation section. + */ + public static final int FOCUS_GROUP_SECTION = 2; + /** * Bits of {@link #getMeasuredWidthAndState()} and * {@link #getMeasuredWidthAndState()} that provide the actual measured size. @@ -9096,22 +9116,47 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } } + final boolean isFocusGroupOfType(@FocusGroupType int focusGroupType) { + switch (focusGroupType) { + case FOCUS_GROUP_CLUSTER: + return isKeyboardNavigationCluster(); + case FOCUS_GROUP_SECTION: + return isKeyboardNavigationSection(); + default: + throw new IllegalArgumentException("Unknown focus group type: " + focusGroupType); + } + } + /** * Find the nearest keyboard navigation cluster in the specified direction. * This does not actually give focus to that cluster. * + * @param focusGroupType Type of the focus group + * @param currentCluster The starting point of the search. Null means the current cluster is not + * found yet * @param direction Direction to look * * @return The nearest keyboard navigation cluster in the specified direction, or null if none * can be found */ - public View keyboardNavigationClusterSearch(int direction) { - if (mParent != null) { - final View currentCluster = isKeyboardNavigationCluster() ? this : null; - return mParent.keyboardNavigationClusterSearch(currentCluster, direction); - } else { - return null; + public View keyboardNavigationClusterSearch( + @FocusGroupType int focusGroupType, View currentCluster, int direction) { + if (isFocusGroupOfType(focusGroupType)) { + currentCluster = this; } + if (isRootNamespace() + || focusGroupType == FOCUS_GROUP_SECTION && isKeyboardNavigationCluster()) { + // Root namespace means we should consider ourselves the top of the + // tree for cluster searching; otherwise we could be focus searching + // into other tabs. see LocalActivityManager and TabHost for more info. + // In addition, a cluster node works as a root for section searches. + return FocusFinder.getInstance().findNextKeyboardNavigationCluster( + focusGroupType, this, currentCluster, direction); + } else if (mParent != null) { + return mParent.keyboardNavigationClusterSearch( + focusGroupType, currentCluster, direction); + } + return null; } /** @@ -9240,11 +9285,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * Adds any keyboard navigation cluster roots that are descendants of this view (possibly * including this view if it is a cluster root itself) to views. * + * @param focusGroupType Type of the focus group * @param views Cluster roots found so far * @param direction Direction to look */ - public void addKeyboardNavigationClusters(@NonNull Collection views, int direction) { - if (!isKeyboardNavigationCluster()) { + public void addKeyboardNavigationClusters( + @FocusGroupType int focusGroupType, + @NonNull Collection views, + int direction) { + if (!(isFocusGroupOfType(focusGroupType))) { return; } views.add(this); diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 7c133ac5b7a9f..7835899d14196 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -901,23 +901,6 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager return null; } - @Override - public View keyboardNavigationClusterSearch(View currentCluster, int direction) { - if (isKeyboardNavigationCluster()) { - currentCluster = this; - } - if (isRootNamespace()) { - // root namespace means we should consider ourselves the top of the - // tree for cluster searching; otherwise we could be focus searching - // into other tabs. see LocalActivityManager and TabHost for more info - return FocusFinder.getInstance().findNextKeyboardNavigationCluster( - this, currentCluster, direction); - } else if (mParent != null) { - return mParent.keyboardNavigationClusterSearch(currentCluster, direction); - } - return null; - } - @Override public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) { return false; @@ -1164,10 +1147,11 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } @Override - public void addKeyboardNavigationClusters(Collection views, int direction) { + public void addKeyboardNavigationClusters( + @FocusGroupType int focusGroupType, Collection views, int direction) { final int focusableCount = views.size(); - super.addKeyboardNavigationClusters(views, direction); + super.addKeyboardNavigationClusters(focusGroupType, views, direction); if (focusableCount != views.size()) { // No need to look for clusters inside a cluster. @@ -1183,8 +1167,13 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager for (int i = 0; i < count; i++) { final View child = children[i]; + if (focusGroupType == FOCUS_GROUP_SECTION && child.isKeyboardNavigationCluster()) { + // When the current cluster is the default cluster, and we are searching for + // sections, ignore sections inside non-default clusters. + continue; + } if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) { - child.addKeyboardNavigationClusters(views, direction); + child.addKeyboardNavigationClusters(focusGroupType, views, direction); } } } diff --git a/core/java/android/view/ViewParent.java b/core/java/android/view/ViewParent.java index 79b05cdb6e503..c5414e925e0e6 100644 --- a/core/java/android/view/ViewParent.java +++ b/core/java/android/view/ViewParent.java @@ -18,6 +18,7 @@ package android.view; import android.graphics.Rect; import android.os.Bundle; +import android.view.View.FocusGroupType; import android.view.accessibility.AccessibilityEvent; /** @@ -150,6 +151,7 @@ public interface ViewParent { * Find the nearest keyboard navigation cluster in the specified direction. * This does not actually give focus to that cluster. * + * @param focusGroupType Type of the focus group * @param currentCluster The starting point of the search. Null means the current cluster is not * found yet * @param direction Direction to look @@ -157,7 +159,8 @@ public interface ViewParent { * @return The nearest keyboard navigation cluster in the specified direction, or null if none * can be found */ - View keyboardNavigationClusterSearch(View currentCluster, int direction); + View keyboardNavigationClusterSearch( + @FocusGroupType int focusGroupType, View currentCluster, int direction); /** * Change the z order of the child so it's on top of all other children. diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 4f2020311ff46..18d59aa12d08e 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -16,6 +16,8 @@ package android.view; +import static android.view.View.FOCUS_GROUP_CLUSTER; +import static android.view.View.FOCUS_GROUP_SECTION; import static android.view.WindowCallbacks.RESIZE_MODE_DOCKED_DIVIDER; import static android.view.WindowCallbacks.RESIZE_MODE_FREEFORM; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY; @@ -71,6 +73,7 @@ import android.util.TimeUtils; import android.util.TypedValue; import android.view.Surface.OutOfResourcesException; import android.view.View.AttachInfo; +import android.view.View.FocusGroupType; import android.view.View.MeasureSpec; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; @@ -4392,11 +4395,12 @@ public final class ViewRootImpl implements ViewParent, return false; } - private boolean performClusterNavigation(int direction) { + private boolean performClusterNavigation( + @FocusGroupType int focusGroupType, int direction) { final View focused = mView.findFocus(); final View cluster = focused != null - ? focused.keyboardNavigationClusterSearch(direction) - : keyboardNavigationClusterSearch(null, direction); + ? focused.keyboardNavigationClusterSearch(focusGroupType, null, direction) + : keyboardNavigationClusterSearch(focusGroupType, null, direction); if (cluster != null && cluster.restoreLastFocus()) { return true; @@ -4418,15 +4422,32 @@ public final class ViewRootImpl implements ViewParent, } int clusterNavigationDirection = 0; + @FocusGroupType int focusGroupType = 0; if (event.getAction() == KeyEvent.ACTION_DOWN && event.isCtrlPressed()) { final int character = event.getUnicodeChar(event.getMetaState() & ~KeyEvent.META_CTRL_MASK); if (character == '+') { + focusGroupType = FOCUS_GROUP_CLUSTER; clusterNavigationDirection = View.FOCUS_FORWARD; } if (character == '_') { + focusGroupType = FOCUS_GROUP_CLUSTER; + clusterNavigationDirection = View.FOCUS_BACKWARD; + } + } + + if (event.getAction() == KeyEvent.ACTION_DOWN && event.isAltPressed()) { + final int character = + event.getUnicodeChar(event.getMetaState() & ~KeyEvent.META_ALT_MASK); + if (character == '+') { + focusGroupType = FOCUS_GROUP_SECTION; + clusterNavigationDirection = View.FOCUS_FORWARD; + } + + if (character == '_') { + focusGroupType = FOCUS_GROUP_SECTION; clusterNavigationDirection = View.FOCUS_BACKWARD; } } @@ -4456,7 +4477,7 @@ public final class ViewRootImpl implements ViewParent, // Handle automatic focus changes. if (event.getAction() == KeyEvent.ACTION_DOWN) { if (clusterNavigationDirection != 0) { - if (performClusterNavigation(clusterNavigationDirection)) { + if (performClusterNavigation(focusGroupType, clusterNavigationDirection)) { return FINISH_HANDLED; } } else { @@ -5887,13 +5908,11 @@ public final class ViewRootImpl implements ViewParent, * {@inheritDoc} */ @Override - public View keyboardNavigationClusterSearch(View currentCluster, int direction) { + public View keyboardNavigationClusterSearch( + @FocusGroupType int focusGroupType, View currentCluster, int direction) { checkThread(); - if (!(mView instanceof ViewGroup)) { - return null; - } - return FocusFinder.getInstance().findNextKeyboardNavigationCluster( - (ViewGroup) mView, currentCluster, direction); + return FocusFinder.getInstance().findNextKeyboardNavigationCluster(focusGroupType, + mView, currentCluster, direction); } public void debug() {