Merge "Only handle onHoverEvent in actionable views."
diff --git a/api/current.txt b/api/current.txt
index 1bc0812..721e33e 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -21704,7 +21704,10 @@
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 @@
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 @@
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 @@
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);
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index c8f68c7..1148436 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -2262,6 +2262,8 @@
private OnTouchListener mOnTouchListener;
+ private OnHoverListener mOnHoverListener;
+
private OnGenericMotionListener mOnGenericMotionListener;
private OnDragListener mOnDragListener;
@@ -5118,9 +5120,6 @@
return true;
}
- if (mInputEventConsistencyVerifier != null) {
- mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
- }
return false;
}
@@ -5148,6 +5147,12 @@
|| 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 @@
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 @@
*
* @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 @@
*
* @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 @@
*
* @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,35 +5807,55 @@
/**
* 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}.
+ * 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 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.
- * </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) {
- switch (event.getAction()) {
- case MotionEvent.ACTION_HOVER_ENTER:
- setHovered(true);
- break;
+ final int viewFlags = mViewFlags;
+ if ((viewFlags & ENABLED_MASK) == DISABLED) {
+ return false;
+ }
- case MotionEvent.ACTION_HOVER_EXIT:
- setHovered(false);
- break;
+ 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,33 +5865,64 @@
* 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.
*
* @param event The motion event.
@@ -13102,6 +13171,24 @@
}
/**
+ * 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
* event is given to the view.
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index a6bce75..d6a6e2c 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -143,9 +143,16 @@
@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 @@
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();
-
- 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.
- eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory);
- eventNoHistory.setAction(MotionEvent.ACTION_HOVER_EXIT);
- handled |= dispatchTransformedGenericPointerEvent(eventNoHistory, mHoveredChild);
- eventNoHistory.setAction(action);
- mHoveredChild = null;
- } else {
- // Pointer is still within the child.
- //noinspection ConstantConditions
- handled |= dispatchTransformedGenericPointerEvent(event, mHoveredChild);
- }
- }
-
- // 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();
-
for (int i = childrenCount - 1; i >= 0; i--) {
final View child = children[i];
- if (!canViewReceivePointerEvents(child)
- || !isTransformedTouchPointInView(x, y, child, null)) {
- continue;
+ if (canViewReceivePointerEvents(child)
+ && isTransformedTouchPointInView(x, y, child, null)) {
+ newHoveredChild = child;
+ break;
}
+ }
+ }
+ }
- // Found the hovered child.
- mHoveredChild = child;
+ 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) {
- // 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.
- eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory);
- eventNoHistory.setAction(MotionEvent.ACTION_HOVER_ENTER);
- handled |= dispatchTransformedGenericPointerEvent(eventNoHistory, child);
- eventNoHistory.setAction(action);
-
- handled |= dispatchTransformedGenericPointerEvent(eventNoHistory, child);
- } else { /* must be ACTION_HOVER_ENTER */
- // Pointer entered the child.
- handled |= dispatchTransformedGenericPointerEvent(event, child);
+ dispatchTransformedGenericPointerEvent(
+ event, mHoveredChild); // move
}
- break;
+ eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory);
+ eventNoHistory.setAction(MotionEvent.ACTION_HOVER_EXIT);
+ dispatchTransformedGenericPointerEvent(
+ eventNoHistory, mHoveredChild); // 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;
+ }
+ }
+ }
+
+ // 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) {
+ 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 |= super.dispatchHoverEvent(eventNoHistory); // enter
+ eventNoHistory.setAction(action);
+
+ handled |= super.dispatchHoverEvent(eventNoHistory); // move
+ mHoveredSelf = true;
}
}
}
@@ -1306,25 +1364,49 @@
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 @@
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 @@
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.