diff options
| -rw-r--r-- | core/java/android/view/View.java | 58 | ||||
| -rw-r--r-- | core/java/android/view/ViewGroup.java | 223 |
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; + } + } + } } |