From aee802f3bc3da699315a7040728e5483483099ff Mon Sep 17 00:00:00 2001 From: Evan Rosky Date: Wed, 15 Feb 2017 13:26:51 -0800 Subject: [PATCH] Allow cluster navigation to jump into touchscreenBlocksFocus Adds an exception to touchscreenBlocksFocus ViewGroups which are also keyboardNavigationClusters. The behavior we want is that cluster navigation can jump into touchscreenBlocksFocus clusters but normal keyboard navigation can't. Once focus is in a touchscreenBlocksFocus cluster; however, we allow focus navigation to move freely within that cluster. It remains in that cluster until a subsequent cluster navigation brings it back out. Adds back the touchscreenBlocksFocus attributes to Toolbar and actionbar so that they behave like they did before. Bug: 34363323 Test: Added CTS test. Verified desired behavior in a test app Change-Id: I555bf5570b16a57f0d4c8a020ae509a1e1b33910 --- core/java/android/view/FocusFinder.java | 37 +++++++++++++++-- core/java/android/view/ViewGroup.java | 48 +++++++++++++++++++++-- core/java/android/widget/Toolbar.java | 3 ++ core/res/res/layout/screen_action_bar.xml | 2 + core/res/res/layout/screen_toolbar.xml | 1 + core/res/res/values/styles.xml | 1 + 6 files changed, 85 insertions(+), 7 deletions(-) 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