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
This commit is contained in:
@@ -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<View> 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.
|
||||
* <p>
|
||||
* 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
|
||||
|
||||
@@ -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<View> 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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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">
|
||||
<com.android.internal.widget.ActionBarView
|
||||
@@ -54,6 +55,7 @@ This is an optimized layout for a screen with the Action Bar enabled.
|
||||
android:layout_height="wrap_content"
|
||||
style="?attr/actionBarSplitStyle"
|
||||
android:visibility="gone"
|
||||
android:touchscreenBlocksFocus="true"
|
||||
android:keyboardNavigationCluster="true"
|
||||
android:gravity="center"/>
|
||||
</com.android.internal.widget.ActionBarOverlayLayout>
|
||||
|
||||
@@ -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">
|
||||
<Toolbar
|
||||
|
||||
@@ -1249,6 +1249,7 @@ please see styles_device_defaults.xml.
|
||||
<item name="collapseContentDescription">@string/toolbar_collapse_description</item>
|
||||
<item name="contentInsetStart">16dp</item>
|
||||
<item name="contentInsetStartWithNavigation">@dimen/action_bar_content_inset_with_nav</item>
|
||||
<item name="touchscreenBlocksFocus">true</item>
|
||||
<item name="keyboardNavigationCluster">true</item>
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user