diff options
| -rw-r--r-- | api/current.txt | 9 | ||||
| -rw-r--r-- | core/java/android/view/View.java | 147 | ||||
| -rw-r--r-- | core/java/android/view/ViewGroup.java | 242 |
3 files changed, 290 insertions, 108 deletions
diff --git a/api/current.txt b/api/current.txt index 1bc0812c2946..721e33e36291 100644 --- a/api/current.txt +++ b/api/current.txt @@ -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); diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index c8f68c7b8ce8..114843612694 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -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,35 +5807,55 @@ 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) { - 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 @@ 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. * * @param event The motion event. @@ -13102,6 +13171,24 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit } /** + * 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 a6bce7597419..d6a6e2ceddb9 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -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(); - - 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; } + } + } + } + + MotionEvent eventNoHistory = event; + boolean handled = false; - // Found the hovered child. - mHoveredChild = child; + // 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 @@ 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. |