Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a032cc00 authored by Jeff Brown's avatar Jeff Brown
Browse files

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
parent e9f66af9
Loading
Loading
Loading
Loading
+59 −0
Original line number Diff line number Diff line
@@ -218638,6 +218638,28 @@
 visibility="public"
>
</field>
<field name="ACTION_HOVER_ENTER"
 type="int"
 transient="false"
 volatile="false"
 value="9"
 static="true"
 final="true"
 deprecated="not deprecated"
 visibility="public"
>
</field>
<field name="ACTION_HOVER_EXIT"
 type="int"
 transient="false"
 volatile="false"
 value="10"
 static="true"
 final="true"
 deprecated="not deprecated"
 visibility="public"
>
</field>
<field name="ACTION_HOVER_MOVE"
 type="int"
 transient="false"
@@ -223461,6 +223483,17 @@
 visibility="public"
>
</method>
<method name="isHovered"
 return="boolean"
 abstract="false"
 native="false"
 synchronized="false"
 static="false"
 final="false"
 deprecated="not deprecated"
 visibility="public"
>
</method>
<method name="isInEditMode"
 return="boolean"
 abstract="false"
@@ -223936,6 +223969,19 @@
<parameter name="event" type="android.view.MotionEvent">
</parameter>
</method>
<method name="onHoverEvent"
 return="boolean"
 abstract="false"
 native="false"
 synchronized="false"
 static="false"
 final="false"
 deprecated="not deprecated"
 visibility="public"
>
<parameter name="event" type="android.view.MotionEvent">
</parameter>
</method>
<method name="onKeyDown"
 return="boolean"
 abstract="false"
@@ -224976,6 +225022,19 @@
<parameter name="horizontalScrollBarEnabled" type="boolean">
</parameter>
</method>
<method name="setHovered"
 return="void"
 abstract="false"
 native="false"
 synchronized="false"
 static="false"
 final="false"
 deprecated="not deprecated"
 visibility="public"
>
<parameter name="hovered" type="boolean">
</parameter>
</method>
<method name="setId"
 return="void"
 abstract="false"
+38 −5
Original line number Diff line number Diff line
@@ -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) {
+147 −5
Original line number Diff line number Diff line
@@ -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.
     *
@@ -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];

+169 −43
Original line number Diff line number Diff line
@@ -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);

                        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) {
        // 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.dispatchGenericMotionEvent(event);
        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;
+6 −0
Original line number Diff line number Diff line
@@ -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;
Loading