Improving accessibility focus traversal.
1. Now the views considered during the accessibility focus search
are the ones that would get accessibility focus when thovered
over. This way the user will get the same items i.e. feedback
if he touch explores the screen and uses focus traversal. This
is imperative for a good user experience.
2. Updated which focusables are considered when searching for access
focus in ViewGroup. Generally accessibility focus ignores focus
before/after descendants.
3. Implemented focus search strategy in AbsListView that will traverse
the items of the current list (and the stuff withing one item
before moving to the next) before continuing the search if
forward and backward accessibility focus direction.
4. View focus search stops at root namespace. This is not the right
way to prevent some stuff that is not supposed to get a focus in
a container for a specific state. Actually the addFocusables
for that container has to be overriden. Further this approach
leads to focus getting stuck. The accessibility focus ignores
root names space since we want to traverse the entire screen.
5. Fixed an bug in AccessibilityInteractionController which was not
starting to search from the root of a virtual node tree.
6. Fixed a couple of bugs in FocusFinder where it was possible to
get index out of bounds exception if the focusables list is empty.
bug:5932640
Change-Id: Ic3bdd11767a7d40fbb21f35dcd79a4746af784d4
diff --git a/core/java/android/view/AccessibilityInteractionController.java b/core/java/android/view/AccessibilityInteractionController.java
index d42757d..e1f1db2 100644
--- a/core/java/android/view/AccessibilityInteractionController.java
+++ b/core/java/android/view/AccessibilityInteractionController.java
@@ -330,7 +330,7 @@
if (provider != null) {
List<AccessibilityNodeInfo> infosFromProvider =
provider.findAccessibilityNodeInfosByText(text,
- virtualDescendantId);
+ AccessibilityNodeInfo.UNDEFINED);
if (infosFromProvider != null) {
infos.addAll(infosFromProvider);
}
diff --git a/core/java/android/view/FocusFinder.java b/core/java/android/view/FocusFinder.java
index 6bf1888..9063cea 100644
--- a/core/java/android/view/FocusFinder.java
+++ b/core/java/android/view/FocusFinder.java
@@ -276,7 +276,10 @@
return focusables.get(position + 1);
}
}
- return focusables.get(0);
+ if (!focusables.isEmpty()) {
+ return focusables.get(0);
+ }
+ return null;
}
private static View getBackwardFocusable(ViewGroup root, View focused,
@@ -293,7 +296,10 @@
return focusables.get(position - 1);
}
}
- return focusables.get(count - 1);
+ if (!focusables.isEmpty()) {
+ return focusables.get(count - 1);
+ }
+ return null;
}
/**
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 35a6879..42fd63f 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -6029,8 +6029,7 @@
return;
}
if ((focusableMode & FOCUSABLES_ACCESSIBILITY) == FOCUSABLES_ACCESSIBILITY) {
- if (AccessibilityManager.getInstance(mContext).isEnabled()
- && includeForAccessibility()) {
+ if (canTakeAccessibilityFocusFromHover()) {
views.add(this);
return;
}
@@ -6183,57 +6182,28 @@
}
}
- /**
- * Find the best view to take accessibility focus from a hover.
- * This function finds the deepest actionable view and if that
- * fails ask the parent to take accessibility focus from hover.
- *
- * @param x The X hovered location in this view coorditantes.
- * @param y The Y hovered location in this view coorditantes.
- * @return Whether the request was handled.
- *
- * @hide
- */
- public boolean requestAccessibilityFocusFromHover(float x, float y) {
- if (onRequestAccessibilityFocusFromHover(x, y)) {
- return true;
- }
- ViewParent parent = mParent;
- if (parent instanceof View) {
- View parentView = (View) parent;
-
- float[] position = mAttachInfo.mTmpTransformLocation;
- position[0] = x;
- position[1] = y;
-
- // Compensate for the transformation of the current matrix.
- if (!hasIdentityMatrix()) {
- getMatrix().mapPoints(position);
+ private void requestAccessibilityFocusFromHover() {
+ if (includeForAccessibility() && isActionableForAccessibility()) {
+ requestAccessibilityFocus();
+ } else {
+ if (mParent != null) {
+ View nextFocus = mParent.findViewToTakeAccessibilityFocusFromHover(this, this);
+ if (nextFocus != null) {
+ nextFocus.requestAccessibilityFocus();
+ }
}
-
- // Compensate for the parent scroll and the offset
- // of this view stop from the parent top.
- position[0] += mLeft - parentView.mScrollX;
- position[1] += mTop - parentView.mScrollY;
-
- return parentView.requestAccessibilityFocusFromHover(position[0], position[1]);
}
- return false;
}
/**
- * Requests to give this View focus from hover.
- *
- * @param x The X hovered location in this view coorditantes.
- * @param y The Y hovered location in this view coorditantes.
- * @return Whether the request was handled.
- *
* @hide
*/
- public boolean onRequestAccessibilityFocusFromHover(float x, float y) {
- if (includeForAccessibility()
- && (isActionableForAccessibility() || hasListenersForAccessibility())) {
- return requestAccessibilityFocus();
+ public boolean canTakeAccessibilityFocusFromHover() {
+ if (includeForAccessibility() && isActionableForAccessibility()) {
+ return true;
+ }
+ if (mParent != null) {
+ return (mParent.findViewToTakeAccessibilityFocusFromHover(this, this) == this);
}
return false;
}
@@ -6495,14 +6465,15 @@
* important for accessibility are regarded.
*
* @return Whether to regard the view for accessibility.
+ *
+ * @hide
*/
- boolean includeForAccessibility() {
+ public boolean includeForAccessibility() {
if (mAttachInfo != null) {
if (!mAttachInfo.mIncludeNotImportantViews) {
return isImportantForAccessibility();
- } else {
- return true;
}
+ return true;
}
return false;
}
@@ -6513,8 +6484,10 @@
* accessiiblity.
*
* @return True if the view is actionable for accessibility.
+ *
+ * @hide
*/
- private boolean isActionableForAccessibility() {
+ public boolean isActionableForAccessibility() {
return (isClickable() || isLongClickable() || isFocusable());
}
@@ -7687,7 +7660,7 @@
&& pointInView(event.getX(), event.getY())) {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
mSendingHoverAccessibilityEvents = true;
- requestAccessibilityFocusFromHover((int) event.getX(), (int) event.getY());
+ requestAccessibilityFocusFromHover();
}
} else {
if (action == MotionEvent.ACTION_HOVER_EXIT
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index b3c8895..a4c0258 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -628,7 +628,11 @@
* FOCUS_RIGHT, or 0 for not applicable.
*/
public View focusSearch(View focused, int direction) {
- if (isRootNamespace()) {
+ // If we are moving accessibility focus we want to consider all
+ // views no matter if they are on the screen. It is responsibility
+ // of the accessibility service to check whether the result is in
+ // the screen.
+ if (isRootNamespace() && (direction & FOCUS_ACCESSIBILITY) == 0) {
// root namespace means we should consider ourselves the top of the
// tree for focus searching; otherwise we could be focus searching
// into other tabs. see LocalActivityManager and TabHost for more info
@@ -857,20 +861,13 @@
* {@inheritDoc}
*/
@Override
- public void addFocusables(ArrayList<View> views, int direction) {
- addFocusables(views, direction, FOCUSABLES_TOUCH_MODE);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
final int focusableCount = views.size();
final int descendantFocusability = getDescendantFocusability();
- if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) {
+ if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS
+ || (focusableMode & FOCUSABLES_ACCESSIBILITY) == FOCUSABLES_ACCESSIBILITY) {
final int count = mChildrenCount;
final View[] children = mChildren;
@@ -886,10 +883,11 @@
// FOCUS_AFTER_DESCENDANTS and there are some descendants focusable. this is
// to avoid the focus search finding layouts when a more precise search
// among the focusable children would be more interesting.
- if (
- descendantFocusability != FOCUS_AFTER_DESCENDANTS ||
+ if (descendantFocusability != FOCUS_AFTER_DESCENDANTS
// No focusable descendants
- (focusableCount == views.size())) {
+ || (focusableCount == views.size())
+ // We are collecting accessibility focusables.
+ || (focusableMode & FOCUSABLES_ACCESSIBILITY) == FOCUSABLES_ACCESSIBILITY) {
super.addFocusables(views, direction, focusableMode);
}
}
@@ -1660,6 +1658,20 @@
}
/**
+ * @hide
+ */
+ @Override
+ public View findViewToTakeAccessibilityFocusFromHover(View child, View descendant) {
+ if (includeForAccessibility() && isActionableForAccessibility()) {
+ return this;
+ }
+ if (mParent != null) {
+ return mParent.findViewToTakeAccessibilityFocusFromHover(this, descendant);
+ }
+ return null;
+ }
+
+ /**
* Implement this method to intercept hover events before they are handled
* by child views.
* <p>
diff --git a/core/java/android/view/ViewParent.java b/core/java/android/view/ViewParent.java
index ddff91d..d93b996 100644
--- a/core/java/android/view/ViewParent.java
+++ b/core/java/android/view/ViewParent.java
@@ -295,4 +295,16 @@
* @hide
*/
public void childAccessibilityStateChanged(View child);
+
+ /**
+ * A descendant requests this view to find a candidate to take accessibility
+ * focus from hover.
+ *
+ * @param child The child making the call.
+ * @param descendant The descendant that made the initial request.
+ * @return A view to take accessibility focus.
+ *
+ * @hide
+ */
+ public View findViewToTakeAccessibilityFocusFromHover(View child, View descendant);
}
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 1310719..0eee17d 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -2340,6 +2340,14 @@
return true;
}
+ @Override
+ public View findViewToTakeAccessibilityFocusFromHover(View child, View descendant) {
+ if (descendant.includeForAccessibility()) {
+ return descendant;
+ }
+ return null;
+ }
+
/**
* We want to draw a highlight around the current accessibility focused.
* Since adding a style for all possible view is not a viable option we
@@ -2535,6 +2543,20 @@
return handled;
}
+ /**
+ * @hide
+ */
+ public View getAccessibilityFocusedHost() {
+ return mAccessibilityFocusedHost;
+ }
+
+ /**
+ * @hide
+ */
+ public AccessibilityNodeInfo getAccessibilityFocusedVirtualView() {
+ return mAccessibilityFocusedVirtualView;
+ }
+
void setAccessibilityFocusedHost(View host) {
if (mAccessibilityFocusedHost != null && mAccessibilityFocusedVirtualView == null) {
mAccessibilityFocusedHost.clearAccessibilityFocusNoCallbacks();
@@ -2687,7 +2709,7 @@
/**
* Return true if child is an ancestor of parent, (or equal to the parent).
*/
- static boolean isViewDescendantOf(View child, View parent) {
+ public static boolean isViewDescendantOf(View child, View parent) {
if (child == parent) {
return true;
}
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index 3b4ec7d..ab9d370 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -42,6 +42,7 @@
import android.util.StateSet;
import android.view.ActionMode;
import android.view.ContextMenu.ContextMenuInfo;
+import android.view.FocusFinder;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
import android.view.InputDevice;
@@ -56,6 +57,7 @@
import android.view.ViewDebug;
import android.view.ViewGroup;
import android.view.ViewParent;
+import android.view.ViewRootImpl;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
@@ -1327,6 +1329,119 @@
}
@Override
+ public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
+ if ((focusableMode & FOCUSABLES_ACCESSIBILITY) == FOCUSABLES_ACCESSIBILITY
+ && (direction == ACCESSIBILITY_FOCUS_FORWARD
+ || direction == ACCESSIBILITY_FOCUS_BACKWARD)) {
+ if (canTakeAccessibilityFocusFromHover()) {
+ views.add(this);
+ }
+ } else {
+ super.addFocusables(views, direction, focusableMode);
+ }
+ }
+
+ @Override
+ public View focusSearch(int direction) {
+ return focusSearch(null, direction);
+ }
+
+ @Override
+ public View focusSearch(View focused, int direction) {
+ switch (direction) {
+ case ACCESSIBILITY_FOCUS_FORWARD: {
+ ViewRootImpl viewRootImpl = getViewRootImpl();
+ if (viewRootImpl == null) {
+ break;
+ }
+ View currentFocus = viewRootImpl.getAccessibilityFocusedHost();
+ if (currentFocus == null) {
+ break;
+ }
+ // If we have the focus try giving it to the first child.
+ if (currentFocus == this) {
+ final int firstVisiblePosition = getFirstVisiblePosition();
+ if (firstVisiblePosition >= 0) {
+ return getChildAt(0);
+ }
+ return null;
+ }
+ // Find the item that has accessibility focus.
+ final int currentPosition = getPositionForView(currentFocus);
+ if (currentPosition < 0 || currentPosition >= getCount()) {
+ break;
+ }
+ // Try to advance focus in the current item.
+ View currentItem = getChildAt(currentPosition - getFirstVisiblePosition());
+ if (currentItem instanceof ViewGroup) {
+ ViewGroup currentItemGroup = (ViewGroup) currentItem;
+ View nextFocus = FocusFinder.getInstance().findNextFocus(currentItemGroup,
+ currentFocus, direction);
+ if (nextFocus != null && nextFocus != currentItemGroup
+ && nextFocus != currentFocus) {
+ return nextFocus;
+ }
+ }
+ // Try to move focus to the next item.
+ final int nextPosition = currentPosition - getFirstVisiblePosition() + 1;
+ if (nextPosition < getChildCount()) {
+ return getChildAt(nextPosition);
+ }
+ } break;
+ case ACCESSIBILITY_FOCUS_BACKWARD: {
+ ViewRootImpl viewRootImpl = getViewRootImpl();
+ if (viewRootImpl == null) {
+ break;
+ }
+ View currentFocus = viewRootImpl.getAccessibilityFocusedHost();
+ if (currentFocus == null) {
+ break;
+ }
+ // If we have the focus do a generic search.
+ if (currentFocus == this) {
+ return super.focusSearch(this, direction);
+ }
+ // Find the item that has accessibility focus.
+ final int currentPosition = getPositionForView(currentFocus);
+ if (currentPosition < 0 || currentPosition >= getCount()) {
+ break;
+ }
+ // Try to advance focus in the current item.
+ View currentItem = getChildAt(currentPosition - getFirstVisiblePosition());
+ if (currentItem instanceof ViewGroup) {
+ ViewGroup currentItemGroup = (ViewGroup) currentItem;
+ View nextFocus = FocusFinder.getInstance().findNextFocus(currentItemGroup,
+ currentFocus, direction);
+ if (nextFocus != null && nextFocus != currentItemGroup
+ && nextFocus != currentFocus) {
+ return nextFocus;
+ }
+ }
+ // Try to move focus to the previous item.
+ final int nextPosition = currentPosition - getFirstVisiblePosition() - 1;
+ if (nextPosition >= 0) {
+ return getChildAt(nextPosition);
+ } else {
+ return this;
+ }
+ }
+ }
+ return super.focusSearch(focused, direction);
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public View findViewToTakeAccessibilityFocusFromHover(View child, View descendant) {
+ final int position = getPositionForView(child);
+ if (position != INVALID_POSITION) {
+ return getChildAt(position - mFirstPosition);
+ }
+ return super.findViewToTakeAccessibilityFocusFromHover(child, descendant);
+ }
+
+ @Override
public void sendAccessibilityEvent(int eventType) {
// Since this class calls onScrollChanged even if the mFirstPosition and the
// child count have not changed we will avoid sending duplicate accessibility
diff --git a/core/java/android/widget/AdapterView.java b/core/java/android/widget/AdapterView.java
index abfc577..502de31 100644
--- a/core/java/android/widget/AdapterView.java
+++ b/core/java/android/widget/AdapterView.java
@@ -24,7 +24,6 @@
import android.util.SparseArray;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
-import android.view.MotionEvent;
import android.view.SoundEffectConstants;
import android.view.View;
import android.view.ViewDebug;
@@ -32,6 +31,7 @@
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeProvider;
/**
* An AdapterView is a view whose children are determined by an {@link Adapter}.
@@ -957,24 +957,6 @@
event.setItemCount(getCount());
}
- /**
- * @hide
- */
- @Override
- public boolean onRequestAccessibilityFocusFromHover(float x, float y) {
- // We prefer to five focus to the child instead of this view.
- // Usually the children are not actionable for accessibility,
- // and they will not take accessibility focus, so we give it.
- final int childCount = getChildCount();
- for (int i = 0; i < childCount; i++) {
- View child = getChildAt(i);
- if (isTransformedTouchPointInView(x, y, child, null)) {
- return child.requestAccessibilityFocus();
- }
- }
- return super.onRequestAccessibilityFocusFromHover(x, y);
- }
-
private boolean isScrollableForAccessibility() {
T adapter = getAdapter();
if (adapter != null) {