Merge "Add MotionEvent.HOVER_ENTER and HOVER_EXIT."

This commit is contained in:
Jeff Brown
2011-03-24 15:38:04 -07:00
committed by Android (Google) Code Review
9 changed files with 730 additions and 136 deletions

View File

@@ -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
@@ -194,6 +197,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
@@ -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) {

View File

@@ -1622,6 +1622,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,22 +4649,80 @@ 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>
@@ -5223,14 +5287,91 @@ 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.
*
@@ -9882,6 +10023,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];

View File

@@ -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,51 +1195,121 @@ 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);
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);
}
if (handled) {
return true;
handled |= dispatchTransformedGenericPointerEvent(eventNoHistory, child);
} else { /* must be ACTION_HOVER_ENTER */
// Pointer entered the child.
handled |= dispatchTransformedGenericPointerEvent(event, child);
}
break;
}
}
// No child handled the event. Send it to this view group.
return super.dispatchGenericMotionEvent(event);
}
// Recycle the copy of the event that we made.
if (eventNoHistory != event) {
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;
}
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;
}
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);
}
return false;
}
/**
* 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}
*/
@@ -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;
}
@@ -1475,6 +1578,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.
@@ -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;

View File

@@ -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;