Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8c4e97db authored by Svetoslav's avatar Svetoslav Committed by Svetoslav Ganov
Browse files

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
parent bec0c53a
Loading
Loading
Loading
Loading
+15 −0
Original line number Diff line number Diff line
@@ -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<RectF> outRects) {
        if (isClickable() || isLongClickable()) {
            RectF bounds = new RectF();
            bounds.set(0, 0, getWidth(), getHeight());
            outRects.add(bounds);
        }
    }
    static void offsetRects(List<RectF> rects, float offsetX, float offsetY) {
        final int rectCount = rects.size();
        for (int i = 0; i < rectCount; i++) {
+174 −36
Original line number Diff line number Diff line
@@ -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<View> 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<View> 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();
                List<RectF> 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.
                RectF intersection = new RectF(siblingBounds);
                intersections.add(intersection);
                        intersections.add(clickableRect);
                    }
                }
            }
        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<RectF> outRects) {
        int sizeBefore = outRects.size();

        super.addClickableRectsForAccessibility(outRects);

        // If we added ourselves, then no need to visit children.
        if (outRects.size() > sizeBefore) {
            return;
        }

        Iterator<View> 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<View> {
        private List<View> 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();
        }
    }
}