From 8c4e97db879eef3b943325a971a145e5223f49f8 Mon Sep 17 00:00:00 2001 From: Svetoslav Date: Wed, 22 Oct 2014 18:53:36 -0700 Subject: [PATCH] Enhance computation of click point for accessibility. In explore by touch mode the user performs a double tap to click on an item. In this case the system sends down and up events at the location of accessibility focus. The accessibility focused view may be partially covered. In order to click in this view we compute a point where to send the down and up events. This clicking strategy is a bridge-gap and we will switch to accessibility actions in the future. When computing the point to click we were taking into account whether the view was covered by a clickable sibling or a clickable sibling of a predecessor. Despite our expectation cases in which this is not enough happen in practice. In particular, the focused view may be covered by a clickable descendant of a non-clickable sibling of a predecessor that covers the focused view. This change takes care of handling this case. Note that computing the click point is a fair amount of work but this happens very rarely and on demand. Also the code is short circuiting where possible. Change-Id: I4d3cd8b67a7baf0bcc12f370ea7ba1b04c42c355 --- core/java/android/view/View.java | 15 ++ core/java/android/view/ViewGroup.java | 210 +++++++++++++++++++++----- 2 files changed, 189 insertions(+), 36 deletions(-) diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 1ecc8d9067bca..850b24f286859 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -5877,6 +5877,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return true; } + /** + * Adds the clickable rectangles withing the bounds of this view. They + * may overlap. This method is intended for use only by the accessibility + * layer. + * + * @param outRects List to which to add clickable areas. + */ + void addClickableRectsForAccessibility(List outRects) { + if (isClickable() || isLongClickable()) { + RectF bounds = new RectF(); + bounds.set(0, 0, getWidth(), getHeight()); + outRects.add(bounds); + } + } + static void offsetRects(List rects, float offsetX, float offsetY) { final int rectCount = rects.size(); for (int i = 0; i < rectCount; i++) { diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 7538dffa59f64..9229de623b2c8 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -51,8 +51,10 @@ import com.android.internal.util.Predicate; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; @@ -468,6 +470,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager @ViewDebug.ExportedProperty(category = "layout") private int mChildCountWithTransientState = 0; + // Iterator over the children in decreasing Z order (top children first). + private OrderedChildIterator mOrderedChildIterator; + /** * Currently registered axes for nested scrolling. Flag set consisting of * {@link #SCROLL_AXIS_HORIZONTAL} {@link #SCROLL_AXIS_VERTICAL} or {@link #SCROLL_AXIS_NONE} @@ -817,19 +822,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager return false; } - // Check whether any clickable siblings cover the child - // view and if so keep track of the intersections. Also - // respect Z ordering when iterating over children. - ArrayList orderedList = buildOrderedChildList(); - final boolean useCustomOrder = orderedList == null - && isChildrenDrawingOrderEnabled(); - - final int childCount = mChildrenCount; - for (int i = childCount - 1; i >= 0; i--) { - final int childIndex = useCustomOrder - ? getChildDrawingOrder(childCount, i) : i; - final View sibling = (orderedList == null) - ? mChildren[childIndex] : orderedList.get(childIndex); + Iterator iterator = obtainOrderedChildIterator(); + while (iterator.hasNext()) { + View sibling = iterator.next(); // We care only about siblings over the child. if (sibling == child) { @@ -837,12 +832,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } // Ignore invisible views as they are not interactive. - if (sibling.getVisibility() != View.VISIBLE) { - continue; - } - - // If sibling is not interactive we do not care. - if (!sibling.isClickable() && !sibling.isLongClickable()) { + if (!isVisible(sibling)) { continue; } @@ -850,29 +840,36 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager RectF siblingBounds = mAttachInfo.mTmpTransformRect1; siblingBounds.set(0, 0, sibling.getWidth(), sibling.getHeight()); - // Take into account the sibling transformation matrix. - if (!sibling.hasIdentityMatrix()) { - sibling.getMatrix().mapRect(siblingBounds); - } - - // Offset the sibling to our coordinates. - final int siblingDx = sibling.mLeft - mScrollX; - final int siblingDy = sibling.mTop - mScrollY; - siblingBounds.offset(siblingDx, siblingDy); + // Translate the sibling bounds to our coordinates. + offsetChildRectToMyCoords(siblingBounds, sibling); // Compute the intersection between the child and the sibling. if (siblingBounds.intersect(bounds)) { - // If an interactive sibling completely covers the child, done. - if (siblingBounds.equals(bounds)) { - if (orderedList != null) orderedList.clear(); - return false; + List clickableRects = new ArrayList<>(); + sibling.addClickableRectsForAccessibility(clickableRects); + + final int clickableRectCount = clickableRects.size(); + for (int j = 0; j < clickableRectCount; j++) { + RectF clickableRect = clickableRects.get(j); + + // Translate the clickable rect to our coordinates. + offsetChildRectToMyCoords(clickableRect, sibling); + + // Compute the intersection between the child and the clickable rects. + if (clickableRect.intersect(bounds)) { + // If a clickable rect completely covers the child, done. + if (clickableRect.equals(bounds)) { + releaseOrderedChildIterator(); + return false; + } + // Keep track of the intersection rectangle. + intersections.add(clickableRect); + } } - // Keep track of the intersection rectangle. - RectF intersection = new RectF(siblingBounds); - intersections.add(intersection); } } - if (orderedList != null) orderedList.clear(); + + releaseOrderedChildIterator(); if (mParent instanceof ViewGroup) { ViewGroup parentGroup = (ViewGroup) mParent; @@ -883,6 +880,94 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager return true; } + @Override + void addClickableRectsForAccessibility(List outRects) { + int sizeBefore = outRects.size(); + + super.addClickableRectsForAccessibility(outRects); + + // If we added ourselves, then no need to visit children. + if (outRects.size() > sizeBefore) { + return; + } + + Iterator iterator = obtainOrderedChildIterator(); + while (iterator.hasNext()) { + View child = iterator.next(); + + // Cannot click on an invisible view. + if (!isVisible(child)) { + continue; + } + + sizeBefore = outRects.size(); + + // Add clickable rects in the child bounds. + child.addClickableRectsForAccessibility(outRects); + + // Offset the clickable rects for out children to our coordinates. + final int sizeAfter = outRects.size(); + for (int j = sizeBefore; j < sizeAfter; j++) { + RectF rect = outRects.get(j); + + // Translate the clickable rect to our coordinates. + offsetChildRectToMyCoords(rect, child); + + // If a clickable rect fills the parent, done. + if ((int) rect.left == 0 && (int) rect.top == 0 + && (int) rect.right == mRight && (int) rect.bottom == mBottom) { + releaseOrderedChildIterator(); + return; + } + } + } + + releaseOrderedChildIterator(); + } + + private void offsetChildRectToMyCoords(RectF rect, View child) { + if (!child.hasIdentityMatrix()) { + child.getMatrix().mapRect(rect); + } + final int childDx = child.mLeft - mScrollX; + final int childDy = child.mTop - mScrollY; + rect.offset(childDx, childDy); + } + + private static boolean isVisible(View view) { + return (view.getAlpha() > 0 && view.getTransitionAlpha() > 0 && + view.getVisibility() == VISIBLE); + } + + /** + * Obtains the iterator to traverse the children in a descending Z order. + * Only one party can use the iterator at any given time and you cannot + * modify the children while using this iterator. Acquisition if already + * obtained is an error. + * + * @return The child iterator. + */ + OrderedChildIterator obtainOrderedChildIterator() { + if (mOrderedChildIterator == null) { + mOrderedChildIterator = new OrderedChildIterator(); + } else if (mOrderedChildIterator.isInitialized()) { + throw new IllegalStateException("Already obtained"); + } + mOrderedChildIterator.initialize(); + return mOrderedChildIterator; + } + + /** + * Releases the iterator to traverse the children in a descending Z order. + * Release if not obtained is an error. + */ + void releaseOrderedChildIterator() { + if (mOrderedChildIterator == null || !mOrderedChildIterator.isInitialized()) { + throw new IllegalStateException("Not obtained"); + } + mOrderedChildIterator.release(); + } + /** * Called when a child view has changed whether or not it is tracking transient state. */ @@ -7295,4 +7380,57 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager canvas.drawLines(sDebugLines, paint); } + + private final class OrderedChildIterator implements Iterator { + private List mOrderedChildList; + private boolean mUseCustomOrder; + private int mCurrentIndex; + private boolean mInitialized; + + public void initialize() { + mOrderedChildList = buildOrderedChildList(); + mUseCustomOrder = (mOrderedChildList == null) + && isChildrenDrawingOrderEnabled(); + mCurrentIndex = mChildrenCount - 1; + mInitialized = true; + } + + public void release() { + if (mOrderedChildList != null) { + mOrderedChildList.clear(); + } + mUseCustomOrder = false; + mCurrentIndex = 0; + mInitialized = false; + } + + public boolean isInitialized() { + return mInitialized; + } + + @Override + public boolean hasNext() { + return (mCurrentIndex >= 0); + } + + @Override + public View next() { + if (!hasNext()) { + throw new NoSuchElementException("No such element"); + } + return getChild(mCurrentIndex--); + } + + private View getChild(int index) { + final int childIndex = mUseCustomOrder + ? getChildDrawingOrder(mChildrenCount, index) : index; + return (mOrderedChildList == null) + ? mChildren[childIndex] : mOrderedChildList.get(childIndex); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } }