summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Jeff Brown <jeffbrown@google.com> 2011-06-21 18:35:45 -0700
committer Jeff Brown <jeffbrown@google.com> 2011-06-21 21:15:07 -0700
commit87b7f805b94f5df53343264509f6d606d96dfb05 (patch)
treede0a5e414a627fa4360eb8363225e52fef68e9ff
parent194f4a7afd6be1600e360b553f6d9a2f03a3f45b (diff)
Send hover to all children under pointer.
Previously we only sent hover to the topmost child, but this doesn't handle cases where multiple children are overlapped to achieve certain special effects. Now we send hover to all children until one of them handles it. Also moved the call to send the accessibility event into the main dispatch function so that we can send the accessibility event for all innermost hovered views even when setHovered() might not be called. Change-Id: I6fb8b974db44b594c441deafc012b8415afdfac7
-rw-r--r--core/java/android/view/View.java58
-rw-r--r--core/java/android/view/ViewGroup.java223
2 files changed, 199 insertions, 82 deletions
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 114843612694..f70ca90222d4 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -2486,6 +2486,12 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit
Rect mLocalDirtyRect;
/**
+ * Set to true when the view is sending hover accessibility events because it
+ * is the innermost hovered view.
+ */
+ private boolean mSendingHoverAccessibilityEvents;
+
+ /**
* Consistency verifier for debugging purposes.
* @hide
*/
@@ -5200,6 +5206,21 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit
* @return True if the event was handled by the view, false otherwise.
*/
protected boolean dispatchHoverEvent(MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_HOVER_ENTER:
+ if (!hasHoveredChild() && !mSendingHoverAccessibilityEvents) {
+ mSendingHoverAccessibilityEvents = true;
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
+ }
+ break;
+ case MotionEvent.ACTION_HOVER_EXIT:
+ if (mSendingHoverAccessibilityEvents) {
+ mSendingHoverAccessibilityEvents = false;
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
+ }
+ break;
+ }
+
if (mOnHoverListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& mOnHoverListener.onHover(this, event)) {
return true;
@@ -5209,6 +5230,16 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit
}
/**
+ * Returns true if the view has a child to which it has recently sent
+ * {@link MotionEvent#ACTION_HOVER_ENTER}. If this view is hovered and
+ * it does not have a hovered child, then it must be the innermost hovered view.
+ * @hide
+ */
+ protected boolean hasHoveredChild() {
+ return false;
+ }
+
+ /**
* Dispatch a generic motion event to the view under the first pointer.
* <p>
* Do not call this method directly.
@@ -5840,13 +5871,7 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit
* @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) {
+ if (isHoverable()) {
switch (event.getAction()) {
case MotionEvent.ACTION_HOVER_ENTER:
setHovered(true);
@@ -5857,11 +5882,26 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit
}
return true;
}
-
return false;
}
/**
+ * Returns true if the view should handle {@link #onHoverEvent}
+ * by calling {@link #setHovered} to change its hovered state.
+ *
+ * @return True if the view is hoverable.
+ */
+ private boolean isHoverable() {
+ final int viewFlags = mViewFlags;
+ if ((viewFlags & ENABLED_MASK) == DISABLED) {
+ return false;
+ }
+
+ return (viewFlags & CLICKABLE) == CLICKABLE
+ || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE;
+ }
+
+ /**
* Returns true if the view is currently hovered.
*
* @return True if the view is currently hovered.
@@ -5918,8 +5958,6 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit
* @see #setHovered
*/
public void onHoverChanged(boolean hovered) {
- sendAccessibilityEvent(hovered ? AccessibilityEvent.TYPE_VIEW_HOVER_ENTER
- : AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
}
/**
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index d6a6e2ceddb9..e928f802b55f 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -143,11 +143,12 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
@ViewDebug.ExportedProperty(category = "events")
private float mLastTouchDownY;
- // 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;
+ // First hover target in the linked list of hover targets.
+ // The hover targets are children which have received ACTION_HOVER_ENTER.
+ // They might not have actually handled the hover event, but we will
+ // continue sending hover events to them as long as the pointer remains over
+ // their bounds and the view group does not intercept hover.
+ private HoverTarget mFirstHoverTarget;
// True if the view group itself received a hover event.
// It might not have actually handled the hover event.
@@ -1240,80 +1241,120 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
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;
+ MotionEvent eventNoHistory = event;
+ boolean handled = false;
+
+ // Send events to the hovered children and build a new list of hover targets until
+ // one is found that handles the event.
+ HoverTarget firstOldHoverTarget = mFirstHoverTarget;
+ mFirstHoverTarget = 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;
+ HoverTarget lastHoverTarget = null;
for (int i = childrenCount - 1; i >= 0; i--) {
final View child = children[i];
- if (canViewReceivePointerEvents(child)
- && isTransformedTouchPointInView(x, y, child, null)) {
- newHoveredChild = child;
+ if (!canViewReceivePointerEvents(child)
+ || !isTransformedTouchPointInView(x, y, child, null)) {
+ continue;
+ }
+
+ // Obtain a hover target for this child. Dequeue it from the
+ // old hover target list if the child was previously hovered.
+ HoverTarget hoverTarget = firstOldHoverTarget;
+ final boolean wasHovered;
+ for (HoverTarget predecessor = null; ;) {
+ if (hoverTarget == null) {
+ hoverTarget = HoverTarget.obtain(child);
+ wasHovered = false;
+ break;
+ }
+
+ if (hoverTarget.child == child) {
+ if (predecessor != null) {
+ predecessor.next = hoverTarget.next;
+ } else {
+ firstOldHoverTarget = hoverTarget.next;
+ }
+ hoverTarget.next = null;
+ wasHovered = true;
+ break;
+ }
+
+ predecessor = hoverTarget;
+ hoverTarget = hoverTarget.next;
+ }
+
+ // Enqueue the hover target onto the new hover target list.
+ if (lastHoverTarget != null) {
+ lastHoverTarget.next = hoverTarget;
+ } else {
+ lastHoverTarget = hoverTarget;
+ mFirstHoverTarget = hoverTarget;
+ }
+
+ // Dispatch the event to the child.
+ if (action == MotionEvent.ACTION_HOVER_ENTER) {
+ if (!wasHovered) {
+ // Send the enter as is.
+ handled |= dispatchTransformedGenericPointerEvent(
+ event, child); // enter
+ }
+ } else if (action == MotionEvent.ACTION_HOVER_MOVE) {
+ if (!wasHovered) {
+ // Synthesize an enter from a move.
+ eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory);
+ eventNoHistory.setAction(MotionEvent.ACTION_HOVER_ENTER);
+ handled |= dispatchTransformedGenericPointerEvent(
+ eventNoHistory, child); // enter
+ eventNoHistory.setAction(action);
+
+ handled |= dispatchTransformedGenericPointerEvent(
+ eventNoHistory, child); // move
+ } else {
+ // Send the move as is.
+ handled |= dispatchTransformedGenericPointerEvent(event, child);
+ }
+ }
+ if (handled) {
break;
}
}
}
}
- MotionEvent eventNoHistory = event;
- boolean handled = false;
+ // Send exit events to all previously hovered children that are no longer hovered.
+ while (firstOldHoverTarget != null) {
+ final View child = firstOldHoverTarget.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) {
- dispatchTransformedGenericPointerEvent(
- event, mHoveredChild); // move
- }
- eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory);
- eventNoHistory.setAction(MotionEvent.ACTION_HOVER_EXIT);
+ // Exit the old hovered child.
+ if (action == MotionEvent.ACTION_HOVER_EXIT) {
+ // Send the exit as is.
+ handled |= dispatchTransformedGenericPointerEvent(
+ event, child); // exit
+ } else {
+ // Synthesize an exit from a move or enter.
+ // Ignore the result because hover focus has moved to a different view.
+ if (action == MotionEvent.ACTION_HOVER_MOVE) {
dispatchTransformedGenericPointerEvent(
- eventNoHistory, mHoveredChild); // exit
- eventNoHistory.setAction(action);
+ event, child); // move
}
- mHoveredChild = null;
+ eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory);
+ eventNoHistory.setAction(MotionEvent.ACTION_HOVER_EXIT);
+ dispatchTransformedGenericPointerEvent(
+ eventNoHistory, child); // exit
+ eventNoHistory.setAction(action);
}
- 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;
- }
- }
+ final HoverTarget nextOldHoverTarget = firstOldHoverTarget.next;
+ firstOldHoverTarget.recycle();
+ firstOldHoverTarget = nextOldHoverTarget;
}
- // Send events to the view group itself if it is hovered.
+ // Send events to the view group itself if no children have handled it.
boolean newHoveredSelf = !handled;
if (newHoveredSelf == mHoveredSelf) {
if (newHoveredSelf) {
@@ -1368,6 +1409,12 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
return handled;
}
+ /** @hide */
+ @Override
+ protected boolean hasHoveredChild() {
+ return mFirstHoverTarget != null;
+ }
+
/**
* Implement this method to intercept hover events before they are handled
* by child views.
@@ -3423,10 +3470,6 @@ 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();
@@ -3490,7 +3533,6 @@ 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;
@@ -3504,10 +3546,6 @@ 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;
@@ -3565,7 +3603,6 @@ 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;
@@ -3578,10 +3615,6 @@ 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;
@@ -5328,4 +5361,50 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
}
}
}
+
+ /* Describes a hovered view. */
+ private static final class HoverTarget {
+ private static final int MAX_RECYCLED = 32;
+ private static final Object sRecycleLock = new Object();
+ private static HoverTarget sRecycleBin;
+ private static int sRecycledCount;
+
+ // The hovered child view.
+ public View child;
+
+ // The next target in the target list.
+ public HoverTarget next;
+
+ private HoverTarget() {
+ }
+
+ public static HoverTarget obtain(View child) {
+ final HoverTarget target;
+ synchronized (sRecycleLock) {
+ if (sRecycleBin == null) {
+ target = new HoverTarget();
+ } else {
+ target = sRecycleBin;
+ sRecycleBin = target.next;
+ sRecycledCount--;
+ target.next = null;
+ }
+ }
+ target.child = child;
+ return target;
+ }
+
+ public void recycle() {
+ synchronized (sRecycleLock) {
+ if (sRecycledCount < MAX_RECYCLED) {
+ next = sRecycleBin;
+ sRecycleBin = this;
+ sRecycledCount += 1;
+ } else {
+ next = null;
+ }
+ child = null;
+ }
+ }
+ }
}