summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--api/current.txt3
-rw-r--r--api/system-current.txt3
-rw-r--r--api/test-current.txt8
-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
-rw-r--r--core/java/com/android/internal/view/TooltipPopup.java137
-rw-r--r--core/res/res/anim/tooltip_enter.xml23
-rw-r--r--core/res/res/anim/tooltip_exit.xml23
-rw-r--r--core/res/res/drawable/tooltip_frame.xml21
-rw-r--r--core/res/res/layout/tooltip.xml40
-rw-r--r--core/res/res/values/attrs.xml14
-rw-r--r--core/res/res/values/colors.xml3
-rw-r--r--core/res/res/values/config.xml3
-rw-r--r--core/res/res/values/dimens.xml15
-rw-r--r--core/res/res/values/public.xml1
-rw-r--r--core/res/res/values/strings.xml3
-rw-r--r--core/res/res/values/styles.xml10
-rw-r--r--core/res/res/values/symbols.xml5
-rw-r--r--core/res/res/values/themes.xml9
-rw-r--r--core/res/res/values/themes_material.xml8
22 files changed, 808 insertions, 27 deletions
diff --git a/api/current.txt b/api/current.txt
index 9d93eebe1d54..a38ff53577b4 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -1357,6 +1357,7 @@ package android {
field public static final int toYDelta = 16843209; // 0x10101c9
field public static final int toYScale = 16843205; // 0x10101c5
field public static final int toolbarStyle = 16843946; // 0x10104aa
+ field public static final int tooltip = 16844084; // 0x1010534
field public static final int top = 16843182; // 0x10101ae
field public static final int topBright = 16842955; // 0x10100cb
field public static final int topDark = 16842951; // 0x10100c7
@@ -42833,6 +42834,7 @@ package android.view {
method public java.lang.Object getTag(int);
method public int getTextAlignment();
method public int getTextDirection();
+ method public final java.lang.CharSequence getTooltip();
method public final int getTop();
method protected float getTopFadingEdgeStrength();
method protected int getTopPaddingOffset();
@@ -43121,6 +43123,7 @@ package android.view {
method public void setTag(int, java.lang.Object);
method public void setTextAlignment(int);
method public void setTextDirection(int);
+ method public final void setTooltip(java.lang.CharSequence);
method public final void setTop(int);
method public void setTouchDelegate(android.view.TouchDelegate);
method public final void setTransitionName(java.lang.String);
diff --git a/api/system-current.txt b/api/system-current.txt
index e8874647ce26..bcf4897c8661 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -1468,6 +1468,7 @@ package android {
field public static final int toYDelta = 16843209; // 0x10101c9
field public static final int toYScale = 16843205; // 0x10101c5
field public static final int toolbarStyle = 16843946; // 0x10104aa
+ field public static final int tooltip = 16844084; // 0x1010534
field public static final int top = 16843182; // 0x10101ae
field public static final int topBright = 16842955; // 0x10100cb
field public static final int topDark = 16842951; // 0x10100c7
@@ -45997,6 +45998,7 @@ package android.view {
method public java.lang.Object getTag(int);
method public int getTextAlignment();
method public int getTextDirection();
+ method public final java.lang.CharSequence getTooltip();
method public final int getTop();
method protected float getTopFadingEdgeStrength();
method protected int getTopPaddingOffset();
@@ -46285,6 +46287,7 @@ package android.view {
method public void setTag(int, java.lang.Object);
method public void setTextAlignment(int);
method public void setTextDirection(int);
+ method public final void setTooltip(java.lang.CharSequence);
method public final void setTop(int);
method public void setTouchDelegate(android.view.TouchDelegate);
method public final void setTransitionName(java.lang.String);
diff --git a/api/test-current.txt b/api/test-current.txt
index 0a76d78ef8cc..fa75e9b4d0a5 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -1357,6 +1357,7 @@ package android {
field public static final int toYDelta = 16843209; // 0x10101c9
field public static final int toYScale = 16843205; // 0x10101c5
field public static final int toolbarStyle = 16843946; // 0x10104aa
+ field public static final int tooltip = 16844084; // 0x1010534
field public static final int top = 16843182; // 0x10101ae
field public static final int topBright = 16842955; // 0x10100cb
field public static final int topDark = 16842951; // 0x10100c7
@@ -43078,6 +43079,8 @@ package android.view {
method public java.lang.Object getTag(int);
method public int getTextAlignment();
method public int getTextDirection();
+ method public final java.lang.CharSequence getTooltip();
+ method public android.view.View getTooltipView();
method public final int getTop();
method protected float getTopFadingEdgeStrength();
method protected int getTopPaddingOffset();
@@ -43366,6 +43369,7 @@ package android.view {
method public void setTag(int, java.lang.Object);
method public void setTextAlignment(int);
method public void setTextDirection(int);
+ method public final void setTooltip(java.lang.CharSequence);
method public final void setTop(int);
method public void setTouchDelegate(android.view.TouchDelegate);
method public final void setTransitionName(java.lang.String);
@@ -43648,10 +43652,14 @@ package android.view {
method public static deprecated int getEdgeSlop();
method public static deprecated int getFadingEdgeLength();
method public static deprecated long getGlobalActionKeyTimeout();
+ method public static int getHoverTooltipHideShortTimeout();
+ method public static int getHoverTooltipHideTimeout();
+ method public static int getHoverTooltipShowTimeout();
method public static int getJumpTapTimeout();
method public static int getKeyRepeatDelay();
method public static int getKeyRepeatTimeout();
method public static int getLongPressTimeout();
+ method public static int getLongPressTooltipHideTimeout();
method public static deprecated int getMaximumDrawingCacheSize();
method public static deprecated int getMaximumFlingVelocity();
method public static deprecated int getMinimumFlingVelocity();
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.
*/
diff --git a/core/java/com/android/internal/view/TooltipPopup.java b/core/java/com/android/internal/view/TooltipPopup.java
new file mode 100644
index 000000000000..4f48b9645c69
--- /dev/null
+++ b/core/java/com/android/internal/view/TooltipPopup.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.view;
+
+import android.content.Context;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.TextView;
+
+public class TooltipPopup {
+ private final Context mContext;
+
+ private final View mContentView;
+ private final TextView mMessageView;
+
+ private final WindowManager.LayoutParams mLayoutParams = new WindowManager.LayoutParams();
+ private final Rect mTmpDisplayFrame = new Rect();
+ private final int[] mTmpAnchorPos = new int[2];
+
+ public TooltipPopup(Context context) {
+ mContext = context;
+
+ mContentView = LayoutInflater.from(mContext).inflate(
+ com.android.internal.R.layout.tooltip, null);
+ mMessageView = (TextView) mContentView.findViewById(
+ com.android.internal.R.id.message);
+
+ mLayoutParams.setTitle(
+ mContext.getString(com.android.internal.R.string.tooltip_popup_title));
+ mLayoutParams.packageName = mContext.getOpPackageName();
+ mLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL;
+ mLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
+ mLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
+ mLayoutParams.format = PixelFormat.TRANSLUCENT;
+ mLayoutParams.windowAnimations = com.android.internal.R.style.Animation_Tooltip;
+ mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+ }
+
+ public void show(View anchorView, int anchorX, int anchorY, CharSequence tooltipText) {
+ if (isShowing()) {
+ hide();
+ }
+
+ mMessageView.setText(tooltipText);
+
+ computePosition(anchorView, anchorX, anchorY, mLayoutParams);
+
+ WindowManager wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
+ wm.addView(mContentView, mLayoutParams);
+ }
+
+ public void hide() {
+ if (!isShowing()) {
+ return;
+ }
+
+ WindowManager wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
+ wm.removeView(mContentView);
+ }
+
+ public View getContentView() {
+ return mContentView;
+ }
+
+ public boolean isShowing() {
+ return mContentView.getParent() != null;
+ }
+
+ public void updateContent(CharSequence tooltipText) {
+ mMessageView.setText(tooltipText);
+ }
+
+ private void computePosition(View anchorView, int anchorX, int anchorY,
+ WindowManager.LayoutParams outParams) {
+ final int tooltipPreciseAnchorThreshold = mContext.getResources().getDimensionPixelOffset(
+ com.android.internal.R.dimen.tooltip_precise_anchor_threshold);
+
+ final int offsetX;
+ if (anchorView.getWidth() >= tooltipPreciseAnchorThreshold) {
+ // Wide view. Align the tooltip horizontally to the precise X position.
+ offsetX = anchorX;
+ } else {
+ // Otherwise anchor the tooltip to the view center.
+ offsetX = anchorView.getWidth() / 2; // Center on the view horizontally.
+ }
+
+ final int offsetBelow;
+ final int offsetAbove;
+ if (anchorView.getHeight() >= tooltipPreciseAnchorThreshold) {
+ // Tall view. Align the tooltip vertically to the precise Y position.
+ offsetBelow = anchorY;
+ offsetAbove = anchorY;
+ } else {
+ // Otherwise anchor the tooltip to the view center.
+ offsetBelow = anchorView.getHeight(); // Place below the view in most cases.
+ offsetAbove = 0; // Place above the view if the tooltip does not fit below.
+ }
+
+ outParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
+
+ final int tooltipOffset = mContext.getResources().getDimensionPixelOffset(
+ com.android.internal.R.dimen.tooltip_y_offset);
+
+ anchorView.getWindowVisibleDisplayFrame(mTmpDisplayFrame);
+ anchorView.getLocationInWindow(mTmpAnchorPos);
+ outParams.x = mTmpAnchorPos[0] + offsetX - mTmpDisplayFrame.width() / 2;
+ outParams.y = mTmpAnchorPos[1] + offsetBelow + tooltipOffset;
+
+ final int spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+ mContentView.measure(spec, spec);
+ final int tooltipHeight = mContentView.getMeasuredHeight();
+
+ if (outParams.y + tooltipHeight > mTmpDisplayFrame.height()) {
+ // The tooltip does not fit below the anchor point, show above instead.
+ outParams.y = mTmpAnchorPos[1] + offsetAbove - (tooltipOffset + tooltipHeight);
+ }
+ }
+}
diff --git a/core/res/res/anim/tooltip_enter.xml b/core/res/res/anim/tooltip_enter.xml
new file mode 100644
index 000000000000..7eceb4c8c86b
--- /dev/null
+++ b/core/res/res/anim/tooltip_enter.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2016, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+ android:interpolator="@interpolator/decelerate_quad"
+ android:fromAlpha="0.0" android:toAlpha="1.0"
+ android:duration="@android:integer/config_tooltipAnimTime" />
diff --git a/core/res/res/anim/tooltip_exit.xml b/core/res/res/anim/tooltip_exit.xml
new file mode 100644
index 000000000000..e346ca943eaf
--- /dev/null
+++ b/core/res/res/anim/tooltip_exit.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2016, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+ android:interpolator="@interpolator/accelerate_quad"
+ android:fromAlpha="1.0" android:toAlpha="0.0"
+ android:duration="@android:integer/config_tooltipAnimTime" />
diff --git a/core/res/res/drawable/tooltip_frame.xml b/core/res/res/drawable/tooltip_frame.xml
new file mode 100644
index 000000000000..14130c899e96
--- /dev/null
+++ b/core/res/res/drawable/tooltip_frame.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="?attr/tooltipBackgroundColor" />
+ <corners android:radius="@dimen/tooltip_corner_radius" />
+</shape> \ No newline at end of file
diff --git a/core/res/res/layout/tooltip.xml b/core/res/res/layout/tooltip.xml
new file mode 100644
index 000000000000..0aa6a8781d91
--- /dev/null
+++ b/core/res/res/layout/tooltip.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2016, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@android:id/message"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/tooltip_height"
+ android:layout_margin="@dimen/tooltip_margin"
+ android:paddingStart="@dimen/tooltip_horizontal_padding"
+ android:paddingEnd="@dimen/tooltip_horizontal_padding"
+ android:gravity="center"
+ android:background="?android:attr/tooltipFrameBackground"
+ android:textAppearance="@style/TextAppearance.Tooltip"
+ android:textColor="?android:attr/tooltipForegroundColor"
+ android:singleLine="true"
+ android:ellipsize="end"
+ />
+
+</LinearLayout>
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 2d61e6ec3bc5..acafbcfd428a 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -582,7 +582,7 @@
<!-- Image elements -->
<!-- ============== -->
<eat-comment />
-i
+
<!-- Background that can be used behind parts of a UI that provide
details on data the user is selecting. For example, this is
the background element of PreferenceActivity's embedded
@@ -1008,6 +1008,15 @@ i
<!-- Background to use for toasts -->
<attr name="toastFrameBackground" format="reference" />
+ <!-- Background to use for tooltip popups -->
+ <attr name="tooltipFrameBackground" format="reference" />
+
+ <!-- Foreground color to use for tooltip popups -->
+ <attr name="tooltipForegroundColor" format="reference|color" />
+
+ <!-- Background color to use for tooltip popups -->
+ <attr name="tooltipBackgroundColor" format="reference|color" />
+
<!-- Theme to use for Search Dialogs -->
<attr name="searchDialogTheme" format="reference" />
@@ -2865,6 +2874,9 @@ i
{@link android.view.View#forceHasOverlappingRendering(boolean)}. -->
<attr name="forceHasOverlappingRendering" format="boolean" />
+ <!-- Defines text displayed in a small popup window on hover or long press. -->
+ <attr name="tooltip" format="string" localization="suggested" />
+
</declare-styleable>
<!-- Attributes that can be assigned to a tag for a particular View. -->
diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml
index de86cefb5bc7..0995bc3f0799 100644
--- a/core/res/res/values/colors.xml
+++ b/core/res/res/values/colors.xml
@@ -186,4 +186,7 @@
<color name="resize_shadow_start_color">#2a000000</color>
<color name="resize_shadow_end_color">#00000000</color>
+
+ <color name="tooltip_background_dark">#e6616161</color>
+ <color name="tooltip_background_light">#e6FFFFFF</color>
</resources>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index d4119d072a46..a7c5b2ab3c80 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -128,6 +128,9 @@
<integer name="config_activityShortDur">150</integer>
<integer name="config_activityDefaultDur">220</integer>
+ <!-- The duration (in milliseconds) of the tooltip show/hide animations. -->
+ <integer name="config_tooltipAnimTime">150</integer>
+
<!-- Duration for the dim animation behind a dialog. This may be either
a percentage, which is relative to the duration of the enter/open
animation of the window being shown that is dimming behind, or it may
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 91d7227c6a60..5efa55ca1138 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -487,4 +487,19 @@
<!-- Minimum "smallest width" of the display for cascading menus to be enabled. -->
<dimen name="cascading_menus_min_smallest_width">720dp</dimen>
+
+ <!-- Tooltip dimensions. -->
+ <!-- Vertical offset from the edge of the anchor view. -->
+ <dimen name="tooltip_y_offset">20dp</dimen>
+ <!-- Height of the tooltip. -->
+ <dimen name="tooltip_height">32dp</dimen>
+ <!-- The tooltip does not get closer than this to the window edge -->
+ <dimen name="tooltip_margin">8dp</dimen>
+ <!-- Left/right padding of the tooltip text. -->
+ <dimen name="tooltip_horizontal_padding">16dp</dimen>
+ <!-- Border corner radius of the tooltip window. -->
+ <dimen name="tooltip_corner_radius">2dp</dimen>
+ <!-- View with the height equal or above this threshold will have a tooltip anchored
+ to the mouse/touch position -->
+ <dimen name="tooltip_precise_anchor_threshold">96dp</dimen>
</resources>
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index ed685822ee51..ae82128b88cf 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -2759,6 +2759,7 @@
<public name="fontStyle" />
<public name="font" />
<public name="fontWeight" />
+ <public name="tooltip" />
</public-group>
<public-group type="style" first-id="0x010302e0">
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index d42ec9067ac4..baf3cd817d5b 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -4450,4 +4450,7 @@
<!-- Label used by Telephony code, assigned as the display name for conference calls [CHAR LIMIT=60] -->
<string name="conference_call">Conference Call</string>
+
+ <!-- Title for a tooltip popup window [CHAR LIMIT=NONE] -->
+ <string name="tooltip_popup_title">Tooltip Popup</string>
</resources>
diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml
index 937428b12c60..0f756b94a321 100644
--- a/core/res/res/values/styles.xml
+++ b/core/res/res/values/styles.xml
@@ -159,6 +159,11 @@ please see styles_device_defaults.xml.
<item name="windowExitAnimation">@anim/toast_exit</item>
</style>
+ <style name="Animation.Tooltip">
+ <item name="windowEnterAnimation">@anim/tooltip_enter</item>
+ <item name="windowExitAnimation">@anim/tooltip_exit</item>
+ </style>
+
<style name="Animation.DropDownDown">
<item name="windowEnterAnimation">@anim/grow_fade_in</item>
<item name="windowExitAnimation">@anim/shrink_fade_out</item>
@@ -950,6 +955,11 @@ please see styles_device_defaults.xml.
<item name="fontFamily">sans-serif-condensed</item>
</style>
+ <style name="TextAppearance.Tooltip">
+ <item name="fontFamily">sans-serif</item>
+ <item name="textSize">14sp</item>
+ </style>
+
<style name="Widget.ActivityChooserView">
<item name="gravity">center</item>
<item name="background">@drawable/ab_share_pack_holo_dark</item>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index c719664c38a8..5b608b84c51d 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -435,6 +435,8 @@
<java-symbol type="dimen" name="search_view_preferred_height" />
<java-symbol type="dimen" name="textview_error_popup_default_width" />
<java-symbol type="dimen" name="toast_y_offset" />
+ <java-symbol type="dimen" name="tooltip_precise_anchor_threshold" />
+ <java-symbol type="dimen" name="tooltip_y_offset" />
<java-symbol type="dimen" name="action_bar_stacked_max_height" />
<java-symbol type="dimen" name="action_bar_stacked_tab_max_width" />
<java-symbol type="dimen" name="notification_text_size" />
@@ -1123,6 +1125,7 @@
<java-symbol type="string" name="demo_starting_message" />
<java-symbol type="string" name="demo_restarting_message" />
<java-symbol type="string" name="conference_call" />
+ <java-symbol type="string" name="tooltip_popup_title" />
<java-symbol type="plurals" name="bugreport_countdown" />
@@ -1373,6 +1376,7 @@
<java-symbol type="layout" name="textview_hint" />
<java-symbol type="layout" name="time_picker_legacy" />
<java-symbol type="layout" name="time_picker_dialog" />
+ <java-symbol type="layout" name="tooltip" />
<java-symbol type="layout" name="transient_notification" />
<java-symbol type="layout" name="voice_interaction_session" />
<java-symbol type="layout" name="web_text_view_dropdown" />
@@ -1423,6 +1427,7 @@
<java-symbol type="style" name="Animation.DropDownUp" />
<java-symbol type="style" name="Animation.DropDownDown" />
<java-symbol type="style" name="Animation.PopupWindow" />
+ <java-symbol type="style" name="Animation.Tooltip" />
<java-symbol type="style" name="Animation.TypingFilter" />
<java-symbol type="style" name="Animation.TypingFilterRestore" />
<java-symbol type="style" name="Animation.Dream" />
diff --git a/core/res/res/values/themes.xml b/core/res/res/values/themes.xml
index 5b2522f97c8b..357eb4b88c52 100644
--- a/core/res/res/values/themes.xml
+++ b/core/res/res/values/themes.xml
@@ -439,6 +439,11 @@ please see themes_device_defaults.xml.
<item name="lightRadius">@dimen/light_radius</item>
<item name="ambientShadowAlpha">@dimen/ambient_shadow_alpha</item>
<item name="spotShadowAlpha">@dimen/spot_shadow_alpha</item>
+
+ <!-- Tooltip popup properties -->
+ <item name="tooltipFrameBackground">@drawable/tooltip_frame</item>
+ <item name="tooltipForegroundColor">@color/bright_foreground_light</item>
+ <item name="tooltipBackgroundColor">@color/tooltip_background_light</item>
</style>
<!-- Variant of {@link #Theme} with no title bar -->
@@ -553,6 +558,10 @@ please see themes_device_defaults.xml.
<item name="floatingToolbarItemBackgroundDrawable">@drawable/item_background_material_light</item>
<item name="floatingToolbarOpenDrawable">@drawable/ic_menu_moreoverflow_material_light</item>
<item name="floatingToolbarPopupBackgroundDrawable">@drawable/floating_popup_background_light</item>
+
+ <!-- Tooltip popup colors -->
+ <item name="tooltipForegroundColor">@color/bright_foreground_dark</item>
+ <item name="tooltipBackgroundColor">@color/tooltip_background_dark</item>
</style>
<!-- Variant of {@link #Theme_Light} with no title bar -->
diff --git a/core/res/res/values/themes_material.xml b/core/res/res/values/themes_material.xml
index 0de773bc3724..92bb3ea4681d 100644
--- a/core/res/res/values/themes_material.xml
+++ b/core/res/res/values/themes_material.xml
@@ -398,6 +398,10 @@ please see themes_device_defaults.xml.
<item name="colorControlHighlight">@color/ripple_material_dark</item>
<item name="colorButtonNormal">@color/btn_default_material_dark</item>
<item name="colorSwitchThumbNormal">@color/switch_thumb_material_dark</item>
+
+ <!-- Tooltip popup properties -->
+ <item name="tooltipForegroundColor">@color/foreground_material_light</item>
+ <item name="tooltipBackgroundColor">@color/tooltip_background_light</item>
</style>
<!-- Material theme (light version). -->
@@ -762,6 +766,10 @@ please see themes_device_defaults.xml.
<item name="colorControlHighlight">@color/ripple_material_light</item>
<item name="colorButtonNormal">@color/btn_default_material_light</item>
<item name="colorSwitchThumbNormal">@color/switch_thumb_material_light</item>
+
+ <!-- Tooltip popup properties -->
+ <item name="tooltipForegroundColor">@color/foreground_material_dark</item>
+ <item name="tooltipBackgroundColor">@color/tooltip_background_dark</item>
</style>
<!-- Variant of the material (light) theme that has a solid (opaque) action bar