summaryrefslogtreecommitdiff
path: root/core/java/android
diff options
context:
space:
mode:
authorVladislav Kaznacheev <kaznacheev@google.com>2016-11-21 14:11:00 -0800
committerVladislav Kaznacheev <kaznacheev@google.com>2016-11-22 09:32:07 -0800
commitf847ee3c3d68e58b0a1a545bd7358ebb32f6948a (patch)
tree12b56e4f5093dddf876833a87982c11c48fcc675 /core/java/android
parent76932df9ec7f7c2a18f9d899767846c8d7ede4fc (diff)
Implement tooltip support in View
Adding View.setTooltip/getTooltip and 'tooltip' layout attribute. Following Material Design spec for styles and behavior. Bug: 31515376 Test: cts-tradefed run singleCommand cts -m CtsViewTestCases --test android.view.cts.TooltipTest Change-Id: I2d2527f642cd7446ffc88d4beffc7b81d7a2f6d6
Diffstat (limited to 'core/java/android')
-rw-r--r--core/java/android/view/View.java303
-rw-r--r--core/java/android/view/ViewConfiguration.java63
-rw-r--r--core/java/android/view/ViewGroup.java106
-rw-r--r--core/java/android/view/ViewRootImpl.java34
4 files changed, 480 insertions, 26 deletions
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 02a85216cc20..84d7548363d1 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -37,6 +37,7 @@ import android.annotation.LayoutRes;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.Size;
+import android.annotation.TestApi;
import android.annotation.UiThread;
import android.content.ClipData;
import android.content.Context;
@@ -111,6 +112,7 @@ import android.widget.ScrollBarDrawable;
import com.android.internal.R;
import com.android.internal.util.Predicate;
+import com.android.internal.view.TooltipPopup;
import com.android.internal.view.menu.MenuBuilder;
import com.android.internal.widget.ScrollBarUtils;
@@ -1196,6 +1198,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
private static Paint sDebugPaint;
+ /**
+ * <p>Indicates this view can display a tooltip on hover or long press.</p>
+ * {@hide}
+ */
+ static final int TOOLTIP = 0x40000000;
+
/** @hide */
@IntDef(flag = true,
value = {
@@ -3619,6 +3627,39 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
ListenerInfo mListenerInfo;
+ private static class TooltipInfo {
+ /**
+ * Text to be displayed in a tooltip popup.
+ */
+ @Nullable
+ CharSequence mTooltip;
+
+ /**
+ * View-relative position of the tooltip anchor point.
+ */
+ int mAnchorX;
+ int mAnchorY;
+
+ /**
+ * The tooltip popup.
+ */
+ @Nullable
+ TooltipPopup mTooltipPopup;
+
+ /**
+ * Set to true if the tooltip was shown as a result of a long click.
+ */
+ boolean mTooltipFromLongClick;
+
+ /**
+ * Keep these Runnables so that they can be used to reschedule.
+ */
+ Runnable mShowTooltipRunnable;
+ Runnable mHideTooltipRunnable;
+ }
+
+ TooltipInfo mTooltipInfo;
+
// Temporary values used to hold (x,y) coordinates when delegating from the
// two-arg performLongClick() method to the legacy no-arg version.
private float mLongClickX = Float.NaN;
@@ -4576,6 +4617,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
}
break;
+ case R.styleable.View_tooltip:
+ setTooltip(a.getText(attr));
+ break;
}
}
@@ -5712,6 +5756,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
final boolean isAnchored = !Float.isNaN(x) && !Float.isNaN(y);
handled = isAnchored ? showContextMenu(x, y) : showContextMenu();
}
+ if ((mViewFlags & TOOLTIP) == TOOLTIP) {
+ if (!handled) {
+ handled = showLongClickTooltip((int) x, (int) y);
+ }
+ }
if (handled) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
@@ -10603,17 +10652,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
return true;
}
- // Long clickable items don't necessarily have to be clickable.
- if (((mViewFlags & CLICKABLE) == CLICKABLE
- || (mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
- && (event.getRepeatCount() == 0)) {
- // For the purposes of menu anchoring and drawable hotspots,
- // key events are considered to be at the center of the view.
- final float x = getWidth() / 2f;
- final float y = getHeight() / 2f;
- setPressed(true, x, y);
- checkForLongClick(0, x, y);
- return true;
+ if (event.getRepeatCount() == 0) {
+ // Long clickable items don't necessarily have to be clickable.
+ final boolean clickable = (mViewFlags & CLICKABLE) == CLICKABLE
+ || (mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE;
+ if (clickable || (mViewFlags & TOOLTIP) == TOOLTIP) {
+ // For the purposes of menu anchoring and drawable hotspots,
+ // key events are considered to be at the center of the view.
+ final float x = getWidth() / 2f;
+ final float y = getHeight() / 2f;
+ if (clickable) {
+ setPressed(true, x, y);
+ }
+ checkForLongClick(0, x, y);
+ return true;
+ }
}
}
@@ -11160,15 +11213,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
final int viewFlags = mViewFlags;
final int action = event.getAction();
+ final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
+ || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
+ || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
+
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
- return (((viewFlags & CLICKABLE) == CLICKABLE
- || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
- || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
+ return clickable;
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
@@ -11176,11 +11231,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
}
}
- if (((viewFlags & CLICKABLE) == CLICKABLE ||
- (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
- (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
+ if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
+ if ((viewFlags & TOOLTIP) == TOOLTIP) {
+ handleTooltipUp();
+ }
+ if (!clickable) {
+ removeTapCallback();
+ removeLongPressCallback();
+ mInContextButtonPress = false;
+ mHasPerformedLongPress = false;
+ mIgnoreNextUpEvent = false;
+ break;
+ }
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
@@ -11196,7 +11260,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
- }
+ }
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
@@ -11236,6 +11300,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
+ if (!clickable) {
+ checkForLongClick(0, x, y);
+ break;
+ }
+
if (performButtonActionOnTouchDown(event)) {
break;
}
@@ -11261,7 +11330,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
break;
case MotionEvent.ACTION_CANCEL:
- setPressed(false);
+ if (clickable) {
+ setPressed(false);
+ }
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
@@ -11270,16 +11341,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
break;
case MotionEvent.ACTION_MOVE:
- drawableHotspotChanged(x, y);
+ if (clickable) {
+ drawableHotspotChanged(x, y);
+ }
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
+ // Remove any future long press/tap checks
removeTapCallback();
+ removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
- // Remove any future long press/tap checks
- removeLongPressCallback();
-
setPressed(false);
}
}
@@ -11311,7 +11383,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
*/
private void removeLongPressCallback() {
if (mPendingCheckForLongPress != null) {
- removeCallbacks(mPendingCheckForLongPress);
+ removeCallbacks(mPendingCheckForLongPress);
}
}
@@ -15379,6 +15451,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
cleanupDraw();
mCurrentAnimation = null;
+
+ if ((mViewFlags & TOOLTIP) == TOOLTIP) {
+ hideTooltip();
+ }
}
private void cleanupDraw() {
@@ -21031,7 +21107,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
}
private void checkForLongClick(int delayOffset, float x, float y) {
- if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
+ if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
@@ -21039,6 +21115,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
}
mPendingCheckForLongPress.setAnchor(x, y);
mPendingCheckForLongPress.rememberWindowAttachCount();
+ mPendingCheckForLongPress.rememberPressedState();
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
@@ -22439,10 +22516,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
private int mOriginalWindowAttachCount;
private float mX;
private float mY;
+ private boolean mOriginalPressedState;
@Override
public void run() {
- if (isPressed() && (mParent != null)
+ if ((mOriginalPressedState == isPressed()) && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if (performLongClick(mX, mY)) {
mHasPerformedLongPress = true;
@@ -22458,6 +22536,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
public void rememberWindowAttachCount() {
mOriginalWindowAttachCount = mWindowAttachCount;
}
+
+ public void rememberPressedState() {
+ mOriginalPressedState = isPressed();
+ }
}
private final class CheckForTap implements Runnable {
@@ -23246,6 +23328,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
*/
public Surface mDragSurface;
+
+ /**
+ * The view that currently has a tooltip displayed.
+ */
+ View mTooltipHost;
+
/**
* Creates a new set of attachment information with the specified
* events handler and thread.
@@ -23982,4 +24070,167 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
return mAttachInfo.mTmpLocation[0] == insets.getStableInsetLeft()
&& mAttachInfo.mTmpLocation[1] == insets.getStableInsetTop();
}
+
+ /**
+ * Sets the tooltip text which will be displayed in a small popup next to the view.
+ * <p>
+ * The tooltip will be displayed:
+ * <li>On long click, unless is not handled otherwise (by OnLongClickListener or a context
+ * menu). </li>
+ * <li>On hover, after a brief delay since the pointer has stopped moving </li>
+ *
+ * @param tooltip the tooltip text, or null if no tooltip is required
+ */
+ public final void setTooltip(@Nullable CharSequence tooltip) {
+ if (TextUtils.isEmpty(tooltip)) {
+ setFlags(0, TOOLTIP);
+ hideTooltip();
+ mTooltipInfo = null;
+ } else {
+ setFlags(TOOLTIP, TOOLTIP);
+ if (mTooltipInfo == null) {
+ mTooltipInfo = new TooltipInfo();
+ mTooltipInfo.mShowTooltipRunnable = this::showHoverTooltip;
+ mTooltipInfo.mHideTooltipRunnable = this::hideTooltip;
+ }
+ mTooltipInfo.mTooltip = tooltip;
+ if (mTooltipInfo.mTooltipPopup != null && mTooltipInfo.mTooltipPopup.isShowing()) {
+ mTooltipInfo.mTooltipPopup.updateContent(mTooltipInfo.mTooltip);
+ }
+ }
+ }
+
+ /**
+ * Returns the view's tooltip text.
+ *
+ * @return the tooltip text
+ */
+ @Nullable
+ public final CharSequence getTooltip() {
+ return mTooltipInfo != null ? mTooltipInfo.mTooltip : null;
+ }
+
+ private boolean showTooltip(int x, int y, boolean fromLongClick) {
+ if (mAttachInfo == null) {
+ return false;
+ }
+ if ((mViewFlags & ENABLED_MASK) != ENABLED) {
+ return false;
+ }
+ final CharSequence tooltipText = getTooltip();
+ if (TextUtils.isEmpty(tooltipText)) {
+ return false;
+ }
+ hideTooltip();
+ mTooltipInfo.mTooltipFromLongClick = fromLongClick;
+ mTooltipInfo.mTooltipPopup = new TooltipPopup(getContext());
+ mTooltipInfo.mTooltipPopup.show(this, x, y, tooltipText);
+ mAttachInfo.mTooltipHost = this;
+ return true;
+ }
+
+ void hideTooltip() {
+ if (mTooltipInfo == null) {
+ return;
+ }
+ removeCallbacks(mTooltipInfo.mShowTooltipRunnable);
+ if (mTooltipInfo.mTooltipPopup == null) {
+ return;
+ }
+ mTooltipInfo.mTooltipPopup.hide();
+ mTooltipInfo.mTooltipPopup = null;
+ mTooltipInfo.mTooltipFromLongClick = false;
+ if (mAttachInfo != null) {
+ mAttachInfo.mTooltipHost = null;
+ }
+ }
+
+ private boolean showLongClickTooltip(int x, int y) {
+ removeCallbacks(mTooltipInfo.mShowTooltipRunnable);
+ removeCallbacks(mTooltipInfo.mHideTooltipRunnable);
+ return showTooltip(x, y, true);
+ }
+
+ private void showHoverTooltip() {
+ showTooltip(mTooltipInfo.mAnchorX, mTooltipInfo.mAnchorY, false);
+ }
+
+ boolean dispatchTooltipHoverEvent(MotionEvent event) {
+ if (mTooltipInfo == null) {
+ return false;
+ }
+ switch(event.getAction()) {
+ case MotionEvent.ACTION_HOVER_MOVE:
+ if ((mViewFlags & TOOLTIP) != TOOLTIP || (mViewFlags & ENABLED_MASK) != ENABLED) {
+ break;
+ }
+ if (!mTooltipInfo.mTooltipFromLongClick) {
+ if (mTooltipInfo.mTooltipPopup == null) {
+ // Schedule showing the tooltip after a timeout.
+ mTooltipInfo.mAnchorX = (int) event.getX();
+ mTooltipInfo.mAnchorY = (int) event.getY();
+ removeCallbacks(mTooltipInfo.mShowTooltipRunnable);
+ postDelayed(mTooltipInfo.mShowTooltipRunnable,
+ ViewConfiguration.getHoverTooltipShowTimeout());
+ }
+
+ // Hide hover-triggered tooltip after a period of inactivity.
+ // Match the timeout used by NativeInputManager to hide the mouse pointer
+ // (depends on SYSTEM_UI_FLAG_LOW_PROFILE being set).
+ final int timeout;
+ if ((getWindowSystemUiVisibility() & SYSTEM_UI_FLAG_LOW_PROFILE)
+ == SYSTEM_UI_FLAG_LOW_PROFILE) {
+ timeout = ViewConfiguration.getHoverTooltipHideShortTimeout();
+ } else {
+ timeout = ViewConfiguration.getHoverTooltipHideTimeout();
+ }
+ removeCallbacks(mTooltipInfo.mHideTooltipRunnable);
+ postDelayed(mTooltipInfo.mHideTooltipRunnable, timeout);
+ }
+ return true;
+
+ case MotionEvent.ACTION_HOVER_EXIT:
+ if (!mTooltipInfo.mTooltipFromLongClick) {
+ hideTooltip();
+ }
+ break;
+ }
+ return false;
+ }
+
+ void handleTooltipKey(KeyEvent event) {
+ switch (event.getAction()) {
+ case KeyEvent.ACTION_DOWN:
+ if (event.getRepeatCount() == 0) {
+ hideTooltip();
+ }
+ break;
+
+ case KeyEvent.ACTION_UP:
+ handleTooltipUp();
+ break;
+ }
+ }
+
+ private void handleTooltipUp() {
+ if (mTooltipInfo == null || mTooltipInfo.mTooltipPopup == null) {
+ return;
+ }
+ removeCallbacks(mTooltipInfo.mHideTooltipRunnable);
+ postDelayed(mTooltipInfo.mHideTooltipRunnable,
+ ViewConfiguration.getLongPressTooltipHideTimeout());
+ }
+
+ /**
+ * @return The content view of the tooltip popup currently being shown, or null if the tooltip
+ * is not showing.
+ * @hide
+ */
+ @TestApi
+ public View getTooltipView() {
+ if (mTooltipInfo == null || mTooltipInfo.mTooltipPopup == null) {
+ return null;
+ }
+ return mTooltipInfo.mTooltipPopup.getContentView();
+ }
}
diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java
index 33b488fbc6b4..6d2f850b94f4 100644
--- a/core/java/android/view/ViewConfiguration.java
+++ b/core/java/android/view/ViewConfiguration.java
@@ -16,6 +16,7 @@
package android.view;
+import android.annotation.TestApi;
import android.app.AppGlobals;
import android.content.Context;
import android.content.res.Configuration;
@@ -230,6 +231,29 @@ public class ViewConfiguration {
private static final long ACTION_MODE_HIDE_DURATION_DEFAULT = 2000;
/**
+ * Defines the duration in milliseconds before an end of a long press causes a tooltip to be
+ * hidden.
+ */
+ private static final int LONG_PRESS_TOOLTIP_HIDE_TIMEOUT = 1500;
+
+ /**
+ * Defines the duration in milliseconds before a hover event causes a tooltip to be shown.
+ */
+ private static final int HOVER_TOOLTIP_SHOW_TIMEOUT = 500;
+
+ /**
+ * Defines the duration in milliseconds before mouse inactivity causes a tooltip to be hidden.
+ * (default variant to be used when {@link View#SYSTEM_UI_FLAG_LOW_PROFILE} is not set).
+ */
+ private static final int HOVER_TOOLTIP_HIDE_TIMEOUT = 15000;
+
+ /**
+ * Defines the duration in milliseconds before mouse inactivity causes a tooltip to be hidden
+ * (short version to be used when {@link View#SYSTEM_UI_FLAG_LOW_PROFILE} is set).
+ */
+ private static final int HOVER_TOOLTIP_HIDE_SHORT_TIMEOUT = 3000;
+
+ /**
* Configuration values for overriding {@link #hasPermanentMenuKey()} behavior.
* These constants must match the definition in res/values/config.xml.
*/
@@ -800,4 +824,43 @@ public class ViewConfiguration {
public boolean isFadingMarqueeEnabled() {
return mFadingMarqueeEnabled;
}
+
+ /**
+ * @return the duration in milliseconds before an end of a long press causes a tooltip to be
+ * hidden
+ * @hide
+ */
+ @TestApi
+ public static int getLongPressTooltipHideTimeout() {
+ return LONG_PRESS_TOOLTIP_HIDE_TIMEOUT;
+ }
+
+ /**
+ * @return the duration in milliseconds before a hover event causes a tooltip to be shown
+ * @hide
+ */
+ @TestApi
+ public static int getHoverTooltipShowTimeout() {
+ return HOVER_TOOLTIP_SHOW_TIMEOUT;
+ }
+
+ /**
+ * @return the duration in milliseconds before mouse inactivity causes a tooltip to be hidden
+ * (default variant to be used when {@link View#SYSTEM_UI_FLAG_LOW_PROFILE} is not set).
+ * @hide
+ */
+ @TestApi
+ public static int getHoverTooltipHideTimeout() {
+ return HOVER_TOOLTIP_HIDE_TIMEOUT;
+ }
+
+ /**
+ * @return the duration in milliseconds before mouse inactivity causes a tooltip to be hidden
+ * (shorter variant to be used when {@link View#SYSTEM_UI_FLAG_LOW_PROFILE} is set).
+ * @hide
+ */
+ @TestApi
+ public static int getHoverTooltipHideShortTimeout() {
+ return HOVER_TOOLTIP_HIDE_SHORT_TIMEOUT;
+ }
}
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index e39cb96ce59e..c0191ce0b791 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -199,6 +199,13 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
// It might not have actually handled the hover event.
private boolean mHoveredSelf;
+ // The child capable of showing a tooltip and currently under the pointer.
+ private View mTooltipHoverTarget;
+
+ // True if the view group is capable of showing a tooltip and the pointer is directly
+ // over the view group but not one of its child views.
+ private boolean mTooltipHoveredSelf;
+
/**
* Internal flags.
*
@@ -1970,6 +1977,104 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
}
}
+ @Override
+ boolean dispatchTooltipHoverEvent(MotionEvent event) {
+ final int action = event.getAction();
+ switch (action) {
+ case MotionEvent.ACTION_HOVER_ENTER:
+ break;
+
+ case MotionEvent.ACTION_HOVER_MOVE:
+ View newTarget = null;
+
+ // Check what the child under the pointer says about the tooltip.
+ final int childrenCount = mChildrenCount;
+ if (childrenCount != 0) {
+ final float x = event.getX();
+ final float y = event.getY();
+
+ final ArrayList<View> preorderedList = buildOrderedChildList();
+ final boolean customOrder = preorderedList == null
+ && isChildrenDrawingOrderEnabled();
+ final View[] children = mChildren;
+ for (int i = childrenCount - 1; i >= 0; i--) {
+ final int childIndex =
+ getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
+ final View child =
+ getAndVerifyPreorderedView(preorderedList, children, childIndex);
+ final PointF point = getLocalPoint();
+ if (isTransformedTouchPointInView(x, y, child, point)) {
+ if (dispatchTooltipHoverEvent(event, child)) {
+ newTarget = child;
+ }
+ break;
+ }
+ }
+ if (preorderedList != null) preorderedList.clear();
+ }
+
+ if (mTooltipHoverTarget != newTarget) {
+ if (mTooltipHoverTarget != null) {
+ event.setAction(MotionEvent.ACTION_HOVER_EXIT);
+ mTooltipHoverTarget.dispatchTooltipHoverEvent(event);
+ event.setAction(action);
+ }
+ mTooltipHoverTarget = newTarget;
+ }
+
+ if (mTooltipHoverTarget != null) {
+ if (mTooltipHoveredSelf) {
+ mTooltipHoveredSelf = false;
+ event.setAction(MotionEvent.ACTION_HOVER_EXIT);
+ super.dispatchTooltipHoverEvent(event);
+ event.setAction(action);
+ }
+ return true;
+ }
+
+ mTooltipHoveredSelf = super.dispatchTooltipHoverEvent(event);
+ return mTooltipHoveredSelf;
+
+ case MotionEvent.ACTION_HOVER_EXIT:
+ if (mTooltipHoverTarget != null) {
+ mTooltipHoverTarget.dispatchTooltipHoverEvent(event);
+ mTooltipHoverTarget = null;
+ } else if (mTooltipHoveredSelf) {
+ super.dispatchTooltipHoverEvent(event);
+ mTooltipHoveredSelf = false;
+ }
+ break;
+ }
+ return false;
+ }
+
+ private boolean dispatchTooltipHoverEvent(MotionEvent event, View child) {
+ final boolean result;
+ if (!child.hasIdentityMatrix()) {
+ MotionEvent transformedEvent = getTransformedMotionEvent(event, child);
+ result = child.dispatchTooltipHoverEvent(transformedEvent);
+ transformedEvent.recycle();
+ } else {
+ final float offsetX = mScrollX - child.mLeft;
+ final float offsetY = mScrollY - child.mTop;
+ event.offsetLocation(offsetX, offsetY);
+ result = child.dispatchTooltipHoverEvent(event);
+ event.offsetLocation(-offsetX, -offsetY);
+ }
+ return result;
+ }
+
+ private void exitTooltipHoverTargets() {
+ if (mTooltipHoveredSelf || mTooltipHoverTarget != null) {
+ final long now = SystemClock.uptimeMillis();
+ MotionEvent event = MotionEvent.obtain(now, now,
+ MotionEvent.ACTION_HOVER_EXIT, 0.0f, 0.0f, 0);
+ event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
+ dispatchTooltipHoverEvent(event);
+ event.recycle();
+ }
+ }
+
/** @hide */
@Override
protected boolean hasHoveredChild() {
@@ -3186,6 +3291,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
// Similarly, set ACTION_EXIT to all hover targets and clear them.
exitHoverTargets();
+ exitTooltipHoverTargets();
// In case view is detached while transition is running
mLayoutCalledWhileSuppressed = false;
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 1ff8fb0bd8ee..e030e767732e 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -3589,6 +3589,10 @@ public final class ViewRootImpl implements ViewParent,
mAttachInfo.mKeyDispatchState.reset();
mView.dispatchWindowFocusChanged(hasWindowFocus);
mAttachInfo.mTreeObserver.dispatchOnWindowFocusChange(hasWindowFocus);
+
+ if (mAttachInfo.mTooltipHost != null) {
+ mAttachInfo.mTooltipHost.hideTooltip();
+ }
}
// Note: must be done after the focus change callbacks,
@@ -4206,6 +4210,10 @@ public final class ViewRootImpl implements ViewParent,
private int processKeyEvent(QueuedInputEvent q) {
final KeyEvent event = (KeyEvent)q.mEvent;
+ if (mAttachInfo.mTooltipHost != null) {
+ mAttachInfo.mTooltipHost.handleTooltipKey(event);
+ }
+
// If the key's purpose is to exit touch mode then we consume it
// and consider it handled.
if (checkForLeavingTouchModeAndConsume(event)) {
@@ -4232,6 +4240,10 @@ public final class ViewRootImpl implements ViewParent,
ensureTouchMode(true);
}
+ if (action == MotionEvent.ACTION_DOWN && mAttachInfo.mTooltipHost != null) {
+ mAttachInfo.mTooltipHost.hideTooltip();
+ }
+
// Offset the scroll position.
if (mCurScrollY != 0) {
event.offsetLocation(0, mCurScrollY);
@@ -4425,6 +4437,7 @@ public final class ViewRootImpl implements ViewParent,
mAttachInfo.mHandlingPointerEvent = true;
boolean handled = eventTarget.dispatchPointerEvent(event);
maybeUpdatePointerIcon(event);
+ maybeUpdateTooltip(event);
mAttachInfo.mHandlingPointerEvent = false;
if (mAttachInfo.mUnbufferedDispatchRequested && !mUnbufferedInputDispatch) {
mUnbufferedInputDispatch = true;
@@ -4512,6 +4525,27 @@ public final class ViewRootImpl implements ViewParent,
return true;
}
+ private void maybeUpdateTooltip(MotionEvent event) {
+ if (event.getPointerCount() != 1) {
+ return;
+ }
+ final int action = event.getActionMasked();
+ if (action != MotionEvent.ACTION_HOVER_ENTER
+ && action != MotionEvent.ACTION_HOVER_MOVE
+ && action != MotionEvent.ACTION_HOVER_EXIT) {
+ return;
+ }
+ AccessibilityManager manager = AccessibilityManager.getInstance(mContext);
+ if (manager.isEnabled() && manager.isTouchExplorationEnabled()) {
+ return;
+ }
+ if (mView == null) {
+ Slog.d(mTag, "maybeUpdateTooltip called after view was removed");
+ return;
+ }
+ mView.dispatchTooltipHoverEvent(event);
+ }
+
/**
* Performs synthesis of new input events from unhandled input events.
*/