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

Commit 87b7f805 authored by Jeff Brown's avatar Jeff Brown
Browse files

Send hover to all children under pointer.

Previously we only sent hover to the topmost child, but this doesn't
handle cases where multiple children are overlapped to achieve
certain special effects.  Now we send hover to all children until
one of them handles it.

Also moved the call to send the accessibility event into the
main dispatch function so that we can send the accessibility event
for all innermost hovered views even when setHovered() might
not be called.

Change-Id: I6fb8b974db44b594c441deafc012b8415afdfac7
parent 194f4a7a
Loading
Loading
Loading
Loading
+48 −10
Original line number Diff line number Diff line
@@ -2485,6 +2485,12 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit
    Paint mLayerPaint;
    Rect mLocalDirtyRect;

    /**
     * Set to true when the view is sending hover accessibility events because it
     * is the innermost hovered view.
     */
    private boolean mSendingHoverAccessibilityEvents;

    /**
     * Consistency verifier for debugging purposes.
     * @hide
@@ -5200,6 +5206,21 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit
     * @return True if the event was handled by the view, false otherwise.
     */
    protected boolean dispatchHoverEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_HOVER_ENTER:
                if (!hasHoveredChild() && !mSendingHoverAccessibilityEvents) {
                    mSendingHoverAccessibilityEvents = true;
                    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
                }
                break;
            case MotionEvent.ACTION_HOVER_EXIT:
                if (mSendingHoverAccessibilityEvents) {
                    mSendingHoverAccessibilityEvents = false;
                    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
                }
                break;
        }

        if (mOnHoverListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                && mOnHoverListener.onHover(this, event)) {
            return true;
@@ -5208,6 +5229,16 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit
        return onHoverEvent(event);
    }

    /**
     * Returns true if the view has a child to which it has recently sent
     * {@link MotionEvent#ACTION_HOVER_ENTER}.  If this view is hovered and
     * it does not have a hovered child, then it must be the innermost hovered view.
     * @hide
     */
    protected boolean hasHoveredChild() {
        return false;
    }

    /**
     * Dispatch a generic motion event to the view under the first pointer.
     * <p>
@@ -5840,13 +5871,7 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit
     * @see #onHoverChanged
     */
    public boolean onHoverEvent(MotionEvent event) {
        final int viewFlags = mViewFlags;
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            return false;
        }

        if ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
        if (isHoverable()) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_HOVER_ENTER:
                    setHovered(true);
@@ -5857,10 +5882,25 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit
            }
            return true;
        }
        return false;
    }

    /**
     * Returns true if the view should handle {@link #onHoverEvent}
     * by calling {@link #setHovered} to change its hovered state.
     *
     * @return True if the view is hoverable.
     */
    private boolean isHoverable() {
        final int viewFlags = mViewFlags;
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            return false;
        }

        return (viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE;
    }

    /**
     * Returns true if the view is currently hovered.
     *
@@ -5918,8 +5958,6 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit
     * @see #setHovered
     */
    public void onHoverChanged(boolean hovered) {
        sendAccessibilityEvent(hovered ? AccessibilityEvent.TYPE_VIEW_HOVER_ENTER
                : AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
    }

    /**
+151 −72
Original line number Diff line number Diff line
@@ -143,11 +143,12 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
    @ViewDebug.ExportedProperty(category = "events")
    private float mLastTouchDownY;

    // The child which last received ACTION_HOVER_ENTER and ACTION_HOVER_MOVE.
    // The child might not have actually handled the hover event, but we will
    // continue sending hover events to it as long as the pointer remains over
    // it and the view group does not intercept hover.
    private View mHoveredChild;
    // First hover target in the linked list of hover targets.
    // The hover targets are children which have received ACTION_HOVER_ENTER.
    // They might not have actually handled the hover event, but we will
    // continue sending hover events to them as long as the pointer remains over
    // their bounds and the view group does not intercept hover.
    private HoverTarget mFirstHoverTarget;

    // True if the view group itself received a hover event.
    // It might not have actually handled the hover event.
@@ -1240,80 +1241,120 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
        final boolean interceptHover = onInterceptHoverEvent(event);
        event.setAction(action); // restore action in case it was changed

        // Figure out which child should receive the next hover event.
        View newHoveredChild = null;
        MotionEvent eventNoHistory = event;
        boolean handled = false;

        // Send events to the hovered children and build a new list of hover targets until
        // one is found that handles the event.
        HoverTarget firstOldHoverTarget = mFirstHoverTarget;
        mFirstHoverTarget = null;
        if (!interceptHover && action != MotionEvent.ACTION_HOVER_EXIT) {
            final float x = event.getX();
            final float y = event.getY();
            final int childrenCount = mChildrenCount;
            if (childrenCount != 0) {
                final View[] children = mChildren;
                HoverTarget lastHoverTarget = null;
                for (int i = childrenCount - 1; i >= 0; i--) {
                    final View child = children[i];
                    if (canViewReceivePointerEvents(child)
                            && isTransformedTouchPointInView(x, y, child, null)) {
                        newHoveredChild = child;
                    if (!canViewReceivePointerEvents(child)
                            || !isTransformedTouchPointInView(x, y, child, null)) {
                        continue;
                    }

                    // Obtain a hover target for this child.  Dequeue it from the
                    // old hover target list if the child was previously hovered.
                    HoverTarget hoverTarget = firstOldHoverTarget;
                    final boolean wasHovered;
                    for (HoverTarget predecessor = null; ;) {
                        if (hoverTarget == null) {
                            hoverTarget = HoverTarget.obtain(child);
                            wasHovered = false;
                            break;
                        }

                        if (hoverTarget.child == child) {
                            if (predecessor != null) {
                                predecessor.next = hoverTarget.next;
                            } else {
                                firstOldHoverTarget = hoverTarget.next;
                            }
                            hoverTarget.next = null;
                            wasHovered = true;
                            break;
                        }

                        predecessor = hoverTarget;
                        hoverTarget = hoverTarget.next;
                    }

        MotionEvent eventNoHistory = event;
        boolean handled = false;
                    // Enqueue the hover target onto the new hover target list.
                    if (lastHoverTarget != null) {
                        lastHoverTarget.next = hoverTarget;
                    } else {
                        lastHoverTarget = hoverTarget;
                        mFirstHoverTarget = hoverTarget;
                    }

        // Send events to the hovered child.
        if (mHoveredChild == newHoveredChild) {
            if (newHoveredChild != null) {
                // Send event to the same child as before.
                handled |= dispatchTransformedGenericPointerEvent(event, newHoveredChild);
                    // Dispatch the event to the child.
                    if (action == MotionEvent.ACTION_HOVER_ENTER) {
                        if (!wasHovered) {
                            // Send the enter as is.
                            handled |= dispatchTransformedGenericPointerEvent(
                                    event, child); // enter
                        }
                    } else if (action == MotionEvent.ACTION_HOVER_MOVE) {
                        if (!wasHovered) {
                            // Synthesize an enter from a move.
                            eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory);
                            eventNoHistory.setAction(MotionEvent.ACTION_HOVER_ENTER);
                            handled |= dispatchTransformedGenericPointerEvent(
                                    eventNoHistory, child); // enter
                            eventNoHistory.setAction(action);

                            handled |= dispatchTransformedGenericPointerEvent(
                                    eventNoHistory, child); // move
                        } else {
            if (mHoveredChild != null) {
                            // Send the move as is.
                            handled |= dispatchTransformedGenericPointerEvent(event, child);
                        }
                    }
                    if (handled) {
                        break;
                    }
                }
            }
        }

        // Send exit events to all previously hovered children that are no longer hovered.
        while (firstOldHoverTarget != null) {
            final View child = firstOldHoverTarget.child;

            // Exit the old hovered child.
            if (action == MotionEvent.ACTION_HOVER_EXIT) {
                // Send the exit as is.
                handled |= dispatchTransformedGenericPointerEvent(
                            event, mHoveredChild); // exit
                        event, child); // exit
            } else {
                // Synthesize an exit from a move or enter.
                    // Ignore the result because hover focus is moving to a different view.
                // Ignore the result because hover focus has moved to a different view.
                if (action == MotionEvent.ACTION_HOVER_MOVE) {
                    dispatchTransformedGenericPointerEvent(
                                event, mHoveredChild); // move
                            event, child); // move
                }
                eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory);
                eventNoHistory.setAction(MotionEvent.ACTION_HOVER_EXIT);
                dispatchTransformedGenericPointerEvent(
                            eventNoHistory, mHoveredChild); // exit
                        eventNoHistory, child); // exit
                eventNoHistory.setAction(action);
            }
                mHoveredChild = null;
            }

            if (newHoveredChild != null) {
                // Enter the new hovered child.
                if (action == MotionEvent.ACTION_HOVER_ENTER) {
                    // Send the enter as is.
                    handled |= dispatchTransformedGenericPointerEvent(
                            event, newHoveredChild); // enter
                    mHoveredChild = newHoveredChild;
                } else if (action == MotionEvent.ACTION_HOVER_MOVE) {
                    // Synthesize an enter from a move.
                    eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory);
                    eventNoHistory.setAction(MotionEvent.ACTION_HOVER_ENTER);
                    handled |= dispatchTransformedGenericPointerEvent(
                            eventNoHistory, newHoveredChild); // enter
                    eventNoHistory.setAction(action);

                    handled |= dispatchTransformedGenericPointerEvent(
                            eventNoHistory, newHoveredChild); // move
                    mHoveredChild = newHoveredChild;
                }
            }
            final HoverTarget nextOldHoverTarget = firstOldHoverTarget.next;
            firstOldHoverTarget.recycle();
            firstOldHoverTarget = nextOldHoverTarget;
        }

        // Send events to the view group itself if it is hovered.
        // Send events to the view group itself if no children have handled it.
        boolean newHoveredSelf = !handled;
        if (newHoveredSelf == mHoveredSelf) {
            if (newHoveredSelf) {
@@ -1368,6 +1409,12 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
        return handled;
    }

    /** @hide */
    @Override
    protected boolean hasHoveredChild() {
        return mFirstHoverTarget != null;
    }

    /**
     * Implement this method to intercept hover events before they are handled
     * by child views.
@@ -3423,10 +3470,6 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
            mTransition.removeChild(this, view);
        }

        if (view == mHoveredChild) {
            mHoveredChild = null;
        }

        boolean clearChildFocus = false;
        if (view == mFocused) {
            view.clearFocusForRemoval();
@@ -3490,7 +3533,6 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
        final OnHierarchyChangeListener onHierarchyChangeListener = mOnHierarchyChangeListener;
        final boolean notifyListener = onHierarchyChangeListener != null;
        final View focused = mFocused;
        final View hoveredChild = mHoveredChild;
        final boolean detach = mAttachInfo != null;
        View clearChildFocus = null;

@@ -3504,10 +3546,6 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
                mTransition.removeChild(this, view);
            }

            if (view == hoveredChild) {
                mHoveredChild = null;
            }

            if (view == focused) {
                view.clearFocusForRemoval();
                clearChildFocus = view;
@@ -3565,7 +3603,6 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
        final OnHierarchyChangeListener listener = mOnHierarchyChangeListener;
        final boolean notify = listener != null;
        final View focused = mFocused;
        final View hoveredChild = mHoveredChild;
        final boolean detach = mAttachInfo != null;
        View clearChildFocus = null;

@@ -3578,10 +3615,6 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
                mTransition.removeChild(this, view);
            }

            if (view == hoveredChild) {
                mHoveredChild = null;
            }

            if (view == focused) {
                view.clearFocusForRemoval();
                clearChildFocus = view;
@@ -5328,4 +5361,50 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
            }
        }
    }

    /* Describes a hovered view. */
    private static final class HoverTarget {
        private static final int MAX_RECYCLED = 32;
        private static final Object sRecycleLock = new Object();
        private static HoverTarget sRecycleBin;
        private static int sRecycledCount;

        // The hovered child view.
        public View child;

        // The next target in the target list.
        public HoverTarget next;

        private HoverTarget() {
        }

        public static HoverTarget obtain(View child) {
            final HoverTarget target;
            synchronized (sRecycleLock) {
                if (sRecycleBin == null) {
                    target = new HoverTarget();
                } else {
                    target = sRecycleBin;
                    sRecycleBin = target.next;
                     sRecycledCount--;
                    target.next = null;
                }
            }
            target.child = child;
            return target;
        }

        public void recycle() {
            synchronized (sRecycleLock) {
                if (sRecycledCount < MAX_RECYCLED) {
                    next = sRecycleBin;
                    sRecycleBin = this;
                    sRecycledCount += 1;
                } else {
                    next = null;
                }
                child = null;
            }
        }
    }
}