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

Commit 194f4a7a authored by Jeff Brown's avatar Jeff Brown Committed by Android (Google) Code Review
Browse files

Merge "Only handle onHoverEvent in actionable views."

parents 32b6328b 10b6290c
Loading
Loading
Loading
Loading
+9 −0
Original line number Diff line number Diff line
@@ -21704,7 +21704,10 @@ package android.view {
    method public void dispatchDisplayHint(int);
    method public boolean dispatchDragEvent(android.view.DragEvent);
    method protected void dispatchDraw(android.graphics.Canvas);
    method protected boolean dispatchGenericFocusedEvent(android.view.MotionEvent);
    method public boolean dispatchGenericMotionEvent(android.view.MotionEvent);
    method protected boolean dispatchGenericPointerEvent(android.view.MotionEvent);
    method protected boolean dispatchHoverEvent(android.view.MotionEvent);
    method public boolean dispatchKeyEvent(android.view.KeyEvent);
    method public boolean dispatchKeyEventPreIme(android.view.KeyEvent);
    method public boolean dispatchKeyShortcutEvent(android.view.KeyEvent);
@@ -21892,6 +21895,7 @@ package android.view {
    method public void onFinishTemporaryDetach();
    method protected void onFocusChanged(boolean, int, android.graphics.Rect);
    method public boolean onGenericMotionEvent(android.view.MotionEvent);
    method public void onHoverChanged(boolean);
    method public boolean onHoverEvent(android.view.MotionEvent);
    method public void onInitializeAccessibilityEvent(android.view.accessibility.AccessibilityEvent);
    method public void onInitializeAccessibilityNodeInfo(android.view.accessibility.AccessibilityNodeInfo);
@@ -22168,6 +22172,10 @@ package android.view {
    method public abstract boolean onGenericMotion(android.view.View, android.view.MotionEvent);
  }
  public static abstract interface View.OnHoverListener {
    method public abstract boolean onHover(android.view.View, android.view.MotionEvent);
  }
  public static abstract interface View.OnKeyListener {
    method public abstract boolean onKey(android.view.View, int, android.view.KeyEvent);
  }
@@ -22338,6 +22346,7 @@ package android.view {
    method protected void measureChildren(int, int);
    method public final void offsetDescendantRectToMyCoords(android.view.View, android.graphics.Rect);
    method public final void offsetRectIntoDescendantCoords(android.view.View, android.graphics.Rect);
    method public boolean onInterceptHoverEvent(android.view.MotionEvent);
    method public boolean onInterceptTouchEvent(android.view.MotionEvent);
    method protected abstract void onLayout(boolean, int, int, int, int);
    method protected boolean onRequestFocusInDescendants(int, android.graphics.Rect);
+117 −30
Original line number Diff line number Diff line
@@ -2262,6 +2262,8 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit

    private OnTouchListener mOnTouchListener;

    private OnHoverListener mOnHoverListener;

    private OnGenericMotionListener mOnGenericMotionListener;

    private OnDragListener mOnDragListener;
@@ -5118,9 +5120,6 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit
            return true;
        }

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }
        return false;
    }

@@ -5148,6 +5147,12 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit
                    || action == MotionEvent.ACTION_HOVER_MOVE
                    || action == MotionEvent.ACTION_HOVER_EXIT) {
                if (dispatchHoverEvent(event)) {
                    // For compatibility with existing applications that handled HOVER_MOVE
                    // events in onGenericMotionEvent, dispatch the event there.  The
                    // onHoverEvent method did not exist at the time.
                    if (action == MotionEvent.ACTION_HOVER_MOVE) {
                        dispatchGenericMotionEventInternal(event);
                    }
                    return true;
                }
            } else if (dispatchGenericPointerEvent(event)) {
@@ -5157,6 +5162,17 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit
            return true;
        }

        if (dispatchGenericMotionEventInternal(event)) {
            return true;
        }

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }
        return false;
    }

    private boolean dispatchGenericMotionEventInternal(MotionEvent event) {
        //noinspection SimplifiableIfStatement
        if (mOnGenericMotionListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                && mOnGenericMotionListener.onGenericMotion(this, event)) {
@@ -5182,9 +5198,13 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     * @hide
     */
    protected boolean dispatchHoverEvent(MotionEvent event) {
        if (mOnHoverListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                && mOnHoverListener.onHover(this, event)) {
            return true;
        }

        return onHoverEvent(event);
    }

@@ -5197,7 +5217,6 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     * @hide
     */
    protected boolean dispatchGenericPointerEvent(MotionEvent event) {
        return false;
@@ -5212,7 +5231,6 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     * @hide
     */
    protected boolean dispatchGenericFocusedEvent(MotionEvent event) {
        return false;
@@ -5789,36 +5807,56 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit
    /**
     * Implement this method to handle hover events.
     * <p>
     * Hover events are pointer events with action {@link MotionEvent#ACTION_HOVER_ENTER},
     * {@link MotionEvent#ACTION_HOVER_MOVE}, or {@link MotionEvent#ACTION_HOVER_EXIT}.
     * </p><p>
     * The view receives hover enter as the pointer enters the bounds of the view and hover
     * exit as the pointer exits the bound of the view or just before the pointer goes down
     * (which implies that {@link #onTouchEvent(MotionEvent)} will be called soon).
     * </p><p>
     * If the view would like to handle the hover event itself and prevent its children
     * from receiving hover, it should return true from this method.  If this method returns
     * true and a child has already received a hover enter event, the child will
     * automatically receive a hover exit event.
     * This method is called whenever a pointer is hovering into, over, or out of the
     * bounds of a view and the view is not currently being touched.
     * Hover events are represented as pointer events with action
     * {@link MotionEvent#ACTION_HOVER_ENTER}, {@link MotionEvent#ACTION_HOVER_MOVE},
     * or {@link MotionEvent#ACTION_HOVER_EXIT}.
     * </p>
     * <ul>
     * <li>The view receives a hover event with action {@link MotionEvent#ACTION_HOVER_ENTER}
     * when the pointer enters the bounds of the view.</li>
     * <li>The view receives a hover event with action {@link MotionEvent#ACTION_HOVER_MOVE}
     * when the pointer has already entered the bounds of the view and has moved.</li>
     * <li>The view receives a hover event with action {@link MotionEvent#ACTION_HOVER_EXIT}
     * when the pointer has exited the bounds of the view or when the pointer is
     * about to go down due to a button click, tap, or similar user action that
     * causes the view to be touched.</li>
     * </ul>
     * <p>
     * The view should implement this method to return true to indicate that it is
     * handling the hover event, such as by changing its drawable state.
     * </p><p>
     * The default implementation sets the hovered state of the view if the view is
     * clickable.
     * The default implementation calls {@link #setHovered} to update the hovered state
     * of the view when a hover enter or hover exit event is received, if the view
     * is enabled and is clickable.
     * </p>
     *
     * @param event The motion event that describes the hover.
     * @return True if this view handled the hover event and does not want its children
     * to receive the hover event.
     * @return True if the view handled the hover event.
     *
     * @see #isHovered
     * @see #setHovered
     * @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) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_HOVER_ENTER:
                    setHovered(true);
                    break;

                case MotionEvent.ACTION_HOVER_EXIT:
                    setHovered(false);
                    break;
            }
            return true;
        }

        return false;
    }
@@ -5827,32 +5865,63 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit
     * Returns true if the view is currently hovered.
     *
     * @return True if the view is currently hovered.
     *
     * @see #setHovered
     * @see #onHoverChanged
     */
    @ViewDebug.ExportedProperty
    public boolean isHovered() {
        return (mPrivateFlags & HOVERED) != 0;
    }

    /**
     * Sets whether the view is currently hovered.
     * <p>
     * Calling this method also changes the drawable state of the view.  This
     * enables the view to react to hover by using different drawable resources
     * to change its appearance.
     * </p><p>
     * The {@link #onHoverChanged} method is called when the hovered state changes.
     * </p>
     *
     * @param hovered True if the view is hovered.
     *
     * @see #isHovered
     * @see #onHoverChanged
     */
    public void setHovered(boolean hovered) {
        if (hovered) {
            if ((mPrivateFlags & HOVERED) == 0) {
                mPrivateFlags |= HOVERED;
                refreshDrawableState();
                sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
                onHoverChanged(true);
            }
        } else {
            if ((mPrivateFlags & HOVERED) != 0) {
                mPrivateFlags &= ~HOVERED;
                refreshDrawableState();
                sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
                onHoverChanged(false);
            }
        }
    }

    /**
     * Implement this method to handle hover state changes.
     * <p>
     * This method is called whenever the hover state changes as a result of a
     * call to {@link #setHovered}.
     * </p>
     *
     * @param hovered The current hover state, as returned by {@link #isHovered}.
     *
     * @see #isHovered
     * @see #setHovered
     */
    public void onHoverChanged(boolean hovered) {
        sendAccessibilityEvent(hovered ? AccessibilityEvent.TYPE_VIEW_HOVER_ENTER
                : AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
    }

    /**
     * Implement this method to handle touch screen motion events.
     *
@@ -13101,6 +13170,24 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit
        boolean onTouch(View v, MotionEvent event);
    }

    /**
     * Interface definition for a callback to be invoked when a hover event is
     * dispatched to this view. The callback will be invoked before the hover
     * event is given to the view.
     */
    public interface OnHoverListener {
        /**
         * Called when a hover event is dispatched to a view. This allows listeners to
         * get a chance to respond before the target view.
         *
         * @param v The view the hover event has been dispatched to.
         * @param event The MotionEvent object containing full information about
         *        the event.
         * @return True if the listener has consumed the event, false otherwise.
         */
        boolean onHover(View v, MotionEvent event);
    }

    /**
     * Interface definition for a callback to be invoked when a generic motion event is
     * dispatched to this view. The callback will be invoked before the generic motion
+164 −78
Original line number Diff line number Diff line
@@ -143,9 +143,16 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
    @ViewDebug.ExportedProperty(category = "events")
    private float mLastTouchDownY;

    // Child which last received ACTION_HOVER_ENTER and ACTION_HOVER_MOVE.
    // 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;

    // True if the view group itself received a hover event.
    // It might not have actually handled the hover event.
    private boolean mHoveredSelf;

    /**
     * Internal flags.
     *
@@ -1222,81 +1229,132 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
        return false;
    }

    /** @hide */
    /**
     * {@inheritDoc}
     */
    @Override
    protected boolean dispatchHoverEvent(MotionEvent event) {
        // Send the hover enter or hover move event to the view group first.
        // If it handles the event then a hovered child should receive hover exit.
        boolean handled = false;
        final boolean interceptHover;
        final int action = event.getAction();
        if (action == MotionEvent.ACTION_HOVER_EXIT) {
            interceptHover = true;
        } else {
            handled = super.dispatchHoverEvent(event);
            interceptHover = handled;
        }

        // Send successive hover events to the hovered child as long as the pointer
        // remains within the child's bounds.
        MotionEvent eventNoHistory = event;
        if (mHoveredChild != null) {
        // First check whether the view group wants to intercept the hover event.
        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;
        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;
                for (int i = childrenCount - 1; i >= 0; i--) {
                    final View child = children[i];
                    if (canViewReceivePointerEvents(child)
                            && isTransformedTouchPointInView(x, y, child, null)) {
                        newHoveredChild = child;
                        break;
                    }
                }
            }
        }

            if (interceptHover
                    || !isTransformedTouchPointInView(x, y, mHoveredChild, null)) {
                // Pointer exited the child.
                // Send it a hover exit with only the most recent coordinates.  We could
                // try to find the exact point in history when the pointer left the view
                // but it is not worth the effort.
        MotionEvent eventNoHistory = event;
        boolean handled = false;

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

        // Find a new hovered child if needed.
        if (!interceptHover && mHoveredChild == null
                && (action == MotionEvent.ACTION_HOVER_ENTER
                        || action == MotionEvent.ACTION_HOVER_MOVE)) {
            final int childrenCount = mChildrenCount;
            if (childrenCount != 0) {
                final View[] children = mChildren;
                final float x = event.getX();
                final float y = event.getY();
            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);

                for (int i = childrenCount - 1; i >= 0; i--) {
                    final View child = children[i];
                    if (!canViewReceivePointerEvents(child)
                            || !isTransformedTouchPointInView(x, y, child, null)) {
                        continue;
                    handled |= dispatchTransformedGenericPointerEvent(
                            eventNoHistory, newHoveredChild); // move
                    mHoveredChild = newHoveredChild;
                }
            }
        }

                    // Found the hovered child.
                    mHoveredChild = child;
        // Send events to the view group itself if it is hovered.
        boolean newHoveredSelf = !handled;
        if (newHoveredSelf == mHoveredSelf) {
            if (newHoveredSelf) {
                // Send event to the view group as before.
                handled |= super.dispatchHoverEvent(event);
            }
        } else {
            if (mHoveredSelf) {
                // Exit the view group.
                if (action == MotionEvent.ACTION_HOVER_EXIT) {
                    // Send the exit as is.
                    handled |= super.dispatchHoverEvent(event); // exit
                } else {
                    // Synthesize an exit from a move or enter.
                    // Ignore the result because hover focus is moving to a different view.
                    if (action == MotionEvent.ACTION_HOVER_MOVE) {
                        // Pointer was moving within the view group and entered the child.
                        // Send it a hover enter and hover move with only the most recent
                        // coordinates.  We could try to find the exact point in history when
                        // the pointer entered the view but it is not worth the effort.
                        super.dispatchHoverEvent(event); // move
                    }
                    eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory);
                    eventNoHistory.setAction(MotionEvent.ACTION_HOVER_EXIT);
                    super.dispatchHoverEvent(eventNoHistory); // exit
                    eventNoHistory.setAction(action);
                }
                mHoveredSelf = false;
            }

            if (newHoveredSelf) {
                // Enter the view group.
                if (action == MotionEvent.ACTION_HOVER_ENTER) {
                    // Send the enter as is.
                    handled |= super.dispatchHoverEvent(event); // enter
                    mHoveredSelf = true;
                } 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, child);
                    handled |= super.dispatchHoverEvent(eventNoHistory); // enter
                    eventNoHistory.setAction(action);

                        handled |= dispatchTransformedGenericPointerEvent(eventNoHistory, child);
                    } else { /* must be ACTION_HOVER_ENTER */
                        // Pointer entered the child.
                        handled |= dispatchTransformedGenericPointerEvent(event, child);
                    }
                    break;
                    handled |= super.dispatchHoverEvent(eventNoHistory); // move
                    mHoveredSelf = true;
                }
            }
        }
@@ -1306,25 +1364,49 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
            eventNoHistory.recycle();
        }

        // Send hover exit to the view group.  If there was a child, we will already have
        // sent the hover exit to it.
        if (action == MotionEvent.ACTION_HOVER_EXIT) {
            handled |= super.dispatchHoverEvent(event);
        }

        // Done.
        return handled;
    }

    @Override
    public boolean onHoverEvent(MotionEvent event) {
        // Handle the event only if leaf. This guarantees that
        // the leafs (or any custom class that returns true from
        // this method) will get a change to process the hover.
        //noinspection SimplifiableIfStatement
        if (getChildCount() == 0) {
            return super.onHoverEvent(event);
        }
    /**
     * Implement this method to intercept hover events before they are handled
     * by child views.
     * <p>
     * This method is called before dispatching a hover event to a child of
     * the view group or to the view group's own {@link #onHoverEvent} to allow
     * the view group a chance to intercept the hover event.
     * This method can also be used to watch all pointer motions that occur within
     * the bounds of the view group even when the pointer is hovering over
     * a child of the view group rather than over the view group itself.
     * </p><p>
     * The view group can prevent its children from receiving hover events by
     * implementing this method and returning <code>true</code> to indicate
     * that it would like to intercept hover events.  The view group must
     * continuously return <code>true</code> from {@link #onInterceptHoverEvent}
     * for as long as it wishes to continue intercepting hover events from
     * its children.
     * </p><p>
     * Interception preserves the invariant that at most one view can be
     * hovered at a time by transferring hover focus from the currently hovered
     * child to the view group or vice-versa as needed.
     * </p><p>
     * If this method returns <code>true</code> and a child is already hovered, then the
     * child view will first receive a hover exit event and then the view group
     * itself will receive a hover enter event in {@link #onHoverEvent}.
     * Likewise, if this method had previously returned <code>true</code> to intercept hover
     * events and instead returns <code>false</code> while the pointer is hovering
     * within the bounds of one of a child, then the view group will first receive a
     * hover exit event in {@link #onHoverEvent} and then the hovered child will
     * receive a hover enter event.
     * </p><p>
     * The default implementation always returns false.
     * </p>
     *
     * @param event The motion event that describes the hover.
     * @return True if the view group would like to intercept the hover event
     * and prevent its children from receiving it.
     */
    public boolean onInterceptHoverEvent(MotionEvent event) {
        return false;
    }

@@ -1335,7 +1417,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
        return MotionEvent.obtainNoHistory(event);
    }

    /** @hide */
    /**
     * {@inheritDoc}
     */
    @Override
    protected boolean dispatchGenericPointerEvent(MotionEvent event) {
        // Send the event to the child under the pointer.
@@ -1362,7 +1446,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
        return super.dispatchGenericPointerEvent(event);
    }

    /** @hide */
    /**
     * {@inheritDoc}
     */
    @Override
    protected boolean dispatchGenericFocusedEvent(MotionEvent event) {
        // Send the event to the focused child or to this view group if it has focus.