diff options
| author | Jeff Brown <jeffbrown@google.com> | 2011-03-07 16:56:21 -0800 |
|---|---|---|
| committer | Jeff Brown <jeffbrown@google.com> | 2011-03-24 15:37:04 -0700 |
| commit | a032cc008618b83ecbbede537517d1e7998e3264 (patch) | |
| tree | 735a1f6f5fd7dc5607a0edb18a85abc831e5b7de /core/java | |
| parent | e9f66af90a886cc55fc20c14375d8572bdf6dbd3 (diff) | |
Add MotionEvent.HOVER_ENTER and HOVER_EXIT.
The input dispatcher sends a HOVER_ENTER to a window before dispatching
it any HOVER_MOVE events. For compatibility reasons, the window will
*also* receive the HOVER_MOVE. When the pointer moves into a different
window or the pointer goes down or when events are canceled for some reason,
the input dispatcher sends a HOVER_EXIT to the previously hovered window.
The view hierarchy behavior is similar. All views under the pointer
receive onHoverEvent with HOVER_ENTER followed by any number of HOVER_MOVE
events. When the pointer leaves a view, the view receives HOVER_EXIT.
Similarly, if a parent view decides to capture hover by returning true
from onHoverEvent, the hovered descendants will receive HOVER_EXIT.
The default behavior of onHoverEvent is to update the view's hovered
state by calling setHovered(true/false). Views can query their current
hovered state using isHovered().
For testing purposes, the hovered state is mapped to the pressed
drawable state. This will change in a subsequent commit with the
introduction of a new hovered drawable state.
Change-Id: Ib76a7a90236c8f2c7336e55773acade6346cacbe
Diffstat (limited to 'core/java')
| -rw-r--r-- | core/java/android/view/MotionEvent.java | 43 | ||||
| -rw-r--r-- | core/java/android/view/View.java | 152 | ||||
| -rw-r--r-- | core/java/android/view/ViewGroup.java | 212 | ||||
| -rw-r--r-- | core/java/com/android/internal/widget/PointerLocationView.java | 6 |
4 files changed, 360 insertions, 53 deletions
diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java index a17db5d42a4b..3c344796e374 100644 --- a/core/java/android/view/MotionEvent.java +++ b/core/java/android/view/MotionEvent.java @@ -172,6 +172,8 @@ public final class MotionEvent extends InputEvent implements Parcelable { * recent point, as well as any intermediate points since the last * hover move event. * <p> + * This action is always delivered to the window or view under the pointer. + * </p><p> * This action is not a touch event so it is delivered to * {@link View#onGenericMotionEvent(MotionEvent)} rather than * {@link View#onTouchEvent(MotionEvent)}. @@ -184,8 +186,9 @@ public final class MotionEvent extends InputEvent implements Parcelable { * vertical and/or horizontal scroll offsets. Use {@link #getAxisValue(int)} * to retrieve the information from {@link #AXIS_VSCROLL} and {@link #AXIS_HSCROLL}. * The pointer may or may not be down when this event is dispatched. - * This action is always delivered to the winder under the pointer, which - * may not be the window currently touched. + * <p></p> + * This action is always delivered to the window or view under the pointer, which + * may not be the window or view currently touched. * <p> * This action is not a touch event so it is delivered to * {@link View#onGenericMotionEvent(MotionEvent)} rather than @@ -195,6 +198,32 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int ACTION_SCROLL = 8; /** + * Constant for {@link #getAction}: The pointer is not down but has entered the + * boundaries of a window or view. + * <p> + * This action is always delivered to the window or view under the pointer. + * </p><p> + * This action is not a touch event so it is delivered to + * {@link View#onGenericMotionEvent(MotionEvent)} rather than + * {@link View#onTouchEvent(MotionEvent)}. + * </p> + */ + public static final int ACTION_HOVER_ENTER = 9; + + /** + * Constant for {@link #getAction}: The pointer is not down but has exited the + * boundaries of a window or view. + * <p> + * This action is always delivered to the window or view that was previously under the pointer. + * </p><p> + * This action is not a touch event so it is delivered to + * {@link View#onGenericMotionEvent(MotionEvent)} rather than + * {@link View#onTouchEvent(MotionEvent)}. + * </p> + */ + public static final int ACTION_HOVER_EXIT = 10; + + /** * Bits in the action code that represent a pointer index, used with * {@link #ACTION_POINTER_DOWN} and {@link #ACTION_POINTER_UP}. Shifting * down by {@link #ACTION_POINTER_INDEX_SHIFT} provides the actual pointer @@ -1354,9 +1383,9 @@ public final class MotionEvent extends InputEvent implements Parcelable { /** * Returns true if this motion event is a touch event. * <p> - * Specifically excludes pointer events with action {@link #ACTION_HOVER_MOVE} - * or {@link #ACTION_SCROLL} because they are not actually touch events - * (the pointer is not down). + * Specifically excludes pointer events with action {@link #ACTION_HOVER_MOVE}, + * {@link #ACTION_HOVER_ENTER}, {@link #ACTION_HOVER_EXIT}, or {@link #ACTION_SCROLL} + * because they are not actually touch events (the pointer is not down). * </p> * @return True if this motion event is a touch event. * @hide @@ -2313,6 +2342,10 @@ public final class MotionEvent extends InputEvent implements Parcelable { return "ACTION_HOVER_MOVE"; case ACTION_SCROLL: return "ACTION_SCROLL"; + case ACTION_HOVER_ENTER: + return "ACTION_HOVER_ENTER"; + case ACTION_HOVER_EXIT: + return "ACTION_HOVER_EXIT"; } int index = (action & ACTION_POINTER_INDEX_MASK) >> ACTION_POINTER_INDEX_SHIFT; switch (action & ACTION_MASK) { diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index c729ccd8b325..6c11b0303a6f 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -1623,6 +1623,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility private static final int AWAKEN_SCROLL_BARS_ON_ATTACH = 0x08000000; /** + * Indicates that the view has received HOVER_ENTER. Cleared on HOVER_EXIT. + * @hide + */ + private static final int HOVERED = 0x10000000; + + /** * Indicates that pivotX or pivotY were explicitly set and we should not assume the center * for transform operations * @@ -4643,23 +4649,81 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * <p> * Generic motion events with source class {@link InputDevice#SOURCE_CLASS_POINTER} * are delivered to the view under the pointer. All other generic motion events are - * delivered to the focused view. + * delivered to the focused view. Hover events are handled specially and are delivered + * to {@link #onHoverEvent}. * </p> * * @param event The motion event to be dispatched. * @return True if the event was handled by the view, false otherwise. */ public boolean dispatchGenericMotionEvent(MotionEvent event) { + final int source = event.getSource(); + if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) { + final int action = event.getAction(); + if (action == MotionEvent.ACTION_HOVER_ENTER + || action == MotionEvent.ACTION_HOVER_MOVE + || action == MotionEvent.ACTION_HOVER_EXIT) { + if (dispatchHoverEvent(event)) { + return true; + } + } else if (dispatchGenericPointerEvent(event)) { + return true; + } + } else if (dispatchGenericFocusedEvent(event)) { + return true; + } + //noinspection SimplifiableIfStatement if (mOnGenericMotionListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnGenericMotionListener.onGenericMotion(this, event)) { return true; } - return onGenericMotionEvent(event); } /** + * Dispatch a hover event. + * <p> + * Do not call this method directly. Call {@link #dispatchGenericMotionEvent} instead. + * </p> + * + * @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) { + return onHoverEvent(event); + } + + /** + * Dispatch a generic motion event to the view under the first pointer. + * <p> + * Do not call this method directly. Call {@link #dispatchGenericMotionEvent} instead. + * </p> + * + * @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; + } + + /** + * Dispatch a generic motion event to the currently focused view. + * <p> + * Do not call this method directly. Call {@link #dispatchGenericMotionEvent} instead. + * </p> + * + * @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; + } + + /** * Dispatch a pointer event. * <p> * Dispatches touch related pointer events to {@link #onTouchEvent} and all @@ -5223,15 +5287,92 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * </code> * * @param event The generic motion event being processed. - * - * @return Return true if you have consumed the event, false if you haven't. - * The default implementation always returns false. + * @return True if the event was handled, false otherwise. */ public boolean onGenericMotionEvent(MotionEvent event) { return false; } /** + * 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} 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. + * </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. + */ + public boolean onHoverEvent(MotionEvent event) { + final int viewFlags = mViewFlags; + + if (((viewFlags & CLICKABLE) != CLICKABLE && + (viewFlags & LONG_CLICKABLE) != LONG_CLICKABLE)) { + // Nothing to do if the view is not clickable. + return false; + } + + if ((viewFlags & ENABLED_MASK) == DISABLED) { + // A disabled view that is clickable still consumes the hover events, it just doesn't + // respond to them. + return true; + } + + switch (event.getAction()) { + case MotionEvent.ACTION_HOVER_ENTER: + setHovered(true); + break; + + case MotionEvent.ACTION_HOVER_EXIT: + setHovered(false); + break; + } + + return true; + } + + /** + * Returns true if the view is currently hovered. + * + * @return True if the view is currently hovered. + */ + public boolean isHovered() { + return (mPrivateFlags & HOVERED) != 0; + } + + /** + * Sets whether the view is currently hovered. + * + * @param hovered True if the view is hovered. + */ + public void setHovered(boolean hovered) { + if (hovered) { + if ((mPrivateFlags & HOVERED) == 0) { + mPrivateFlags |= HOVERED; + refreshDrawableState(); + } + } else { + if ((mPrivateFlags & HOVERED) != 0) { + mPrivateFlags &= ~HOVERED; + refreshDrawableState(); + } + } + } + + /** * Implement this method to handle touch screen motion events. * * @param event The motion event. @@ -9877,6 +10018,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility // windows to better match their app. viewStateIndex |= VIEW_STATE_ACCELERATED; } + if ((privateFlags & HOVERED) != 0) viewStateIndex |= VIEW_STATE_PRESSED; // temporary drawableState = VIEW_STATE_SETS[viewStateIndex]; diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 8dc86ac63dc6..377f08350767 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -147,6 +147,9 @@ 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. + private View mHoveredChild; + /** * Internal flags. * @@ -1140,13 +1143,50 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager return false; } - /** - * {@inheritDoc} - */ + /** @hide */ @Override - public boolean dispatchGenericMotionEvent(MotionEvent event) { - if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { - // Send the event to the child under the pointer. + 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) { + 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 if (action == MotionEvent.ACTION_HOVER_MOVE) { + // Pointer is still within the child. + 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; @@ -1155,45 +1195,88 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager for (int i = childrenCount - 1; i >= 0; i--) { final View child = children[i]; - if ((child.mViewFlags & VISIBILITY_MASK) != VISIBLE - && child.getAnimation() == null) { - // Skip invisible child unless it is animating. + if (!canViewReceivePointerEvents(child) + || !isTransformedTouchPointInView(x, y, child, null)) { continue; } - if (!isTransformedTouchPointInView(x, y, child, null)) { - // Scroll point is out of child's bounds. - continue; + // Found the hovered child. + mHoveredChild = child; + 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); } + break; + } + } + } - final float offsetX = mScrollX - child.mLeft; - final float offsetY = mScrollY - child.mTop; - final boolean handled; - if (!child.hasIdentityMatrix()) { - MotionEvent transformedEvent = MotionEvent.obtain(event); - transformedEvent.offsetLocation(offsetX, offsetY); - transformedEvent.transform(child.getInverseMatrix()); - handled = child.dispatchGenericMotionEvent(transformedEvent); - transformedEvent.recycle(); - } else { - event.offsetLocation(offsetX, offsetY); - handled = child.dispatchGenericMotionEvent(event); - event.offsetLocation(-offsetX, -offsetY); - } + // Recycle the copy of the event that we made. + if (eventNoHistory != event) { + eventNoHistory.recycle(); + } - if (handled) { - return true; - } + // 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; + } + + private static MotionEvent obtainMotionEventNoHistoryOrSelf(MotionEvent event) { + if (event.getHistorySize() == 0) { + return event; + } + return MotionEvent.obtainNoHistory(event); + } + + /** @hide */ + @Override + protected boolean dispatchGenericPointerEvent(MotionEvent event) { + // Send the event to the child under the pointer. + 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; } - } - // No child handled the event. Send it to this view group. - return super.dispatchGenericMotionEvent(event); + if (dispatchTransformedGenericPointerEvent(event, child)) { + return true; + } + } } + // No child handled the event. Send it to this view group. + return super.dispatchGenericPointerEvent(event); + } + + /** @hide */ + @Override + protected boolean dispatchGenericFocusedEvent(MotionEvent event) { // Send the event to the focused child or to this view group if it has focus. if ((mPrivateFlags & (FOCUSED | HAS_BOUNDS)) == (FOCUSED | HAS_BOUNDS)) { - return super.dispatchGenericMotionEvent(event); + return super.dispatchGenericFocusedEvent(event); } else if (mFocused != null && (mFocused.mPrivateFlags & HAS_BOUNDS) == HAS_BOUNDS) { return mFocused.dispatchGenericMotionEvent(event); } @@ -1201,6 +1284,33 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** + * Dispatches a generic pointer event to a child, taking into account + * transformations that apply to the child. + * + * @param event The event to send. + * @param child The view to send the event to. + * @return {@code true} if the child handled the event. + */ + private boolean dispatchTransformedGenericPointerEvent(MotionEvent event, View child) { + final float offsetX = mScrollX - child.mLeft; + final float offsetY = mScrollY - child.mTop; + + boolean handled; + if (!child.hasIdentityMatrix()) { + MotionEvent transformedEvent = MotionEvent.obtain(event); + transformedEvent.offsetLocation(offsetX, offsetY); + transformedEvent.transform(child.getInverseMatrix()); + handled = child.dispatchGenericMotionEvent(transformedEvent); + transformedEvent.recycle(); + } else { + event.offsetLocation(offsetX, offsetY); + handled = child.dispatchGenericMotionEvent(event); + event.offsetLocation(-offsetX, -offsetY); + } + return handled; + } + + /** * {@inheritDoc} */ @Override @@ -1213,8 +1323,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final int actionMasked = action & MotionEvent.ACTION_MASK; // Handle an initial down. - if (actionMasked == MotionEvent.ACTION_DOWN - || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { + if (actionMasked == MotionEvent.ACTION_DOWN) { // Throw away all previous state when starting a new touch gesture. // The framework may have dropped the up or cancel event for the previous gesture // due to an app switch, ANR, or some other state change. @@ -1268,14 +1377,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager for (int i = childrenCount - 1; i >= 0; i--) { final View child = children[i]; - if ((child.mViewFlags & VISIBILITY_MASK) != VISIBLE - && child.getAnimation() == null) { - // Skip invisible child unless it is animating. - continue; - } - - if (!isTransformedTouchPointInView(x, y, child, null)) { - // New pointer is out of child's bounds. + if (!canViewReceivePointerEvents(child) + || !isTransformedTouchPointInView(x, y, child, null)) { continue; } @@ -1476,6 +1579,15 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** + * Returns true if a child view can receive pointer events. + * @hide + */ + private static boolean canViewReceivePointerEvents(View child) { + return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE + || child.getAnimation() != null; + } + + /** * Returns true if a child view contains the specified point when transformed * into its coordinate space. * Child must not be null. @@ -3244,6 +3356,10 @@ 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(); @@ -3307,6 +3423,7 @@ 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; @@ -3320,6 +3437,10 @@ 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; @@ -3377,6 +3498,7 @@ 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; @@ -3389,6 +3511,10 @@ 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; diff --git a/core/java/com/android/internal/widget/PointerLocationView.java b/core/java/com/android/internal/widget/PointerLocationView.java index 076a1cbe3785..c34cb9e12524 100644 --- a/core/java/com/android/internal/widget/PointerLocationView.java +++ b/core/java/com/android/internal/widget/PointerLocationView.java @@ -357,6 +357,12 @@ public class PointerLocationView extends View { case MotionEvent.ACTION_HOVER_MOVE: prefix = "HOVER MOVE"; break; + case MotionEvent.ACTION_HOVER_ENTER: + prefix = "HOVER ENTER"; + break; + case MotionEvent.ACTION_HOVER_EXIT: + prefix = "HOVER EXIT"; + break; case MotionEvent.ACTION_SCROLL: prefix = "SCROLL"; break; |
