diff --git a/core/java/android/view/FocusFinder.java b/core/java/android/view/FocusFinder.java index 61c92018a4062..ae1ee42c87200 100644 --- a/core/java/android/view/FocusFinder.java +++ b/core/java/android/view/FocusFinder.java @@ -18,6 +18,7 @@ package android.view; import android.annotation.NonNull; import android.annotation.Nullable; +import android.content.pm.PackageManager; import android.graphics.Rect; import android.util.ArrayMap; import android.util.SparseArray; @@ -88,8 +89,9 @@ public class FocusFinder { private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) { View next = null; + ViewGroup effectiveRoot = getEffectiveRoot(root, focused); if (focused != null) { - next = findNextUserSpecifiedFocus(root, focused, direction); + next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction); } if (next != null) { return next; @@ -97,9 +99,9 @@ public class FocusFinder { ArrayList focusables = mTempList; try { focusables.clear(); - root.addFocusables(focusables, direction); + effectiveRoot.addFocusables(focusables, direction); if (!focusables.isEmpty()) { - next = findNextFocus(root, focused, focusedRect, direction, focusables); + next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables); } } finally { focusables.clear(); @@ -107,6 +109,35 @@ public class FocusFinder { return next; } + /** + * Returns the "effective" root of a view. The "effective" root is the closest ancestor + * within-which focus should cycle. + *

+ * For example: normal focus navigation would stay within a ViewGroup marked as + * touchscreenBlocksFocus and keyboardNavigationCluster until a cluster-jump out. + * @return the "effective" root of {@param focused} + */ + private ViewGroup getEffectiveRoot(ViewGroup root, View focused) { + if (focused == null) { + return root; + } + ViewParent effective = focused.getParent(); + do { + if (effective == root) { + return root; + } + ViewGroup vg = (ViewGroup) effective; + if (vg.getTouchscreenBlocksFocus() + && focused.getContext().getPackageManager().hasSystemFeature( + PackageManager.FEATURE_TOUCHSCREEN) + && vg.isKeyboardNavigationCluster()) { + return vg; + } + effective = effective.getParent(); + } while (effective != null); + return root; + } + /** * Find the root of the next keyboard navigation cluster after the current one. * @param root The view tree to look inside. Cannot be null diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index de0ec40ac97b3..1edc8d74daafc 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -1190,7 +1190,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final int focusableCount = views.size(); final int descendantFocusability = getDescendantFocusability(); - final boolean focusSelf = (isFocusableInTouchMode() || !shouldBlockFocusForTouchscreen()); + final boolean blockFocusForTouchscreen = shouldBlockFocusForTouchscreen(); + final boolean focusSelf = (isFocusableInTouchMode() || !blockFocusForTouchscreen); if (descendantFocusability == FOCUS_BLOCK_DESCENDANTS) { if (focusSelf) { @@ -1199,7 +1200,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager return; } - if (shouldBlockFocusForTouchscreen()) { + if (blockFocusForTouchscreen) { focusableMode |= FOCUSABLES_TOUCH_MODE; } @@ -1234,7 +1235,19 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager public void addKeyboardNavigationClusters(Collection views, int direction) { final int focusableCount = views.size(); - super.addKeyboardNavigationClusters(views, direction); + if (isKeyboardNavigationCluster()) { + // Cluster-navigation can enter a touchscreenBlocksFocus cluster, so temporarily + // disable touchscreenBlocksFocus to evaluate whether it contains focusables. + final boolean blockedFocus = getTouchscreenBlocksFocus(); + try { + setTouchscreenBlocksFocusNoRefocus(false); + super.addKeyboardNavigationClusters(views, direction); + } finally { + setTouchscreenBlocksFocusNoRefocus(blockedFocus); + } + } else { + super.addKeyboardNavigationClusters(views, direction); + } if (focusableCount != views.size()) { // No need to look for groups inside a group. @@ -1280,6 +1293,14 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } } + private void setTouchscreenBlocksFocusNoRefocus(boolean touchscreenBlocksFocus) { + if (touchscreenBlocksFocus) { + mGroupFlags |= FLAG_TOUCHSCREEN_BLOCKS_FOCUS; + } else { + mGroupFlags &= ~FLAG_TOUCHSCREEN_BLOCKS_FOCUS; + } + } + /** * Check whether this ViewGroup should ignore focus requests for itself and its children. */ @@ -1288,8 +1309,12 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } boolean shouldBlockFocusForTouchscreen() { + // There is a special case for keyboard-navigation clusters. We allow cluster navigation + // to jump into blockFocusForTouchscreen ViewGroups which are clusters. Once in the + // cluster, focus is free to move around within it. return getTouchscreenBlocksFocus() && - mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN); + mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN) + && (!hasFocus() || !isKeyboardNavigationCluster()); } @Override @@ -3175,6 +3200,21 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager @TestApi @Override public boolean restoreFocusInCluster(@FocusRealDirection int direction) { + // Allow cluster-navigation to enter touchscreenBlocksFocus ViewGroups. + if (isKeyboardNavigationCluster()) { + final boolean blockedFocus = getTouchscreenBlocksFocus(); + try { + setTouchscreenBlocksFocusNoRefocus(false); + return restoreFocusInClusterInternal(direction); + } finally { + setTouchscreenBlocksFocusNoRefocus(blockedFocus); + } + } else { + return restoreFocusInClusterInternal(direction); + } + } + + private boolean restoreFocusInClusterInternal(@FocusRealDirection int direction) { if (mFocusedInCluster != null && !mFocusedInCluster.isKeyboardNavigationCluster() && getDescendantFocusability() != FOCUS_BLOCK_DESCENDANTS && (mFocusedInCluster.mViewFlags & VISIBILITY_MASK) == VISIBLE diff --git a/core/java/android/widget/Toolbar.java b/core/java/android/widget/Toolbar.java index 2eb50e0820411..bf3085d6f1d19 100644 --- a/core/java/android/widget/Toolbar.java +++ b/core/java/android/widget/Toolbar.java @@ -343,6 +343,9 @@ public class Toolbar extends ViewGroup { final ViewGroup vgParent = (ViewGroup) parent; if (vgParent.isKeyboardNavigationCluster()) { setKeyboardNavigationCluster(false); + if (vgParent.getTouchscreenBlocksFocus()) { + setTouchscreenBlocksFocus(false); + } break; } parent = vgParent.getParent(); diff --git a/core/res/res/layout/screen_action_bar.xml b/core/res/res/layout/screen_action_bar.xml index edcbb2b6b4989..4c1d6cb535398 100644 --- a/core/res/res/layout/screen_action_bar.xml +++ b/core/res/res/layout/screen_action_bar.xml @@ -35,6 +35,7 @@ This is an optimized layout for a screen with the Action Bar enabled. android:layout_alignParentTop="true" style="?attr/actionBarStyle" android:transitionName="android:action_bar" + android:touchscreenBlocksFocus="true" android:keyboardNavigationCluster="true" android:gravity="top"> diff --git a/core/res/res/layout/screen_toolbar.xml b/core/res/res/layout/screen_toolbar.xml index 0bec8c40c1a81..ded2527984410 100644 --- a/core/res/res/layout/screen_toolbar.xml +++ b/core/res/res/layout/screen_toolbar.xml @@ -35,6 +35,7 @@ This is an optimized layout for a screen with a toolbar enabled. android:layout_alignParentTop="true" style="?attr/actionBarStyle" android:transitionName="android:action_bar" + android:touchscreenBlocksFocus="true" android:keyboardNavigationCluster="true" android:gravity="top"> @string/toolbar_collapse_description 16dp @dimen/action_bar_content_inset_with_nav + true true