diff --git a/mobile/android/base/moz.build b/mobile/android/base/moz.build
index c3fb48d0e51..bc5f00adbd5 100644
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -415,6 +415,7 @@ gbjar.sources += [
'widget/DoorHanger.java',
'widget/EllipsisTextView.java',
'widget/FaviconView.java',
+ 'widget/FloatingHintEditText.java',
'widget/FlowLayout.java',
'widget/GeckoActionProvider.java',
'widget/GeckoPopupMenu.java',
diff --git a/mobile/android/base/resources/color/floating_hint_text.xml b/mobile/android/base/resources/color/floating_hint_text.xml
new file mode 100644
index 00000000000..dcec219f4c7
--- /dev/null
+++ b/mobile/android/base/resources/color/floating_hint_text.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/mobile/android/base/resources/values-v11/themes.xml b/mobile/android/base/resources/values-v11/themes.xml
index 42c207926be..a2f034ab434 100644
--- a/mobile/android/base/resources/values-v11/themes.xml
+++ b/mobile/android/base/resources/values-v11/themes.xml
@@ -60,6 +60,7 @@
- @drawable/ab_copy
- @drawable/ab_paste
- @drawable/ab_select_all
+ - @style/FloatingHintEditText
diff --git a/mobile/android/base/resources/values/attrs.xml b/mobile/android/base/resources/values/attrs.xml
index deff0790a5e..0c27b8751dd 100644
--- a/mobile/android/base/resources/values/attrs.xml
+++ b/mobile/android/base/resources/values/attrs.xml
@@ -244,5 +244,9 @@
+
+
+
+
diff --git a/mobile/android/base/resources/values/colors.xml b/mobile/android/base/resources/values/colors.xml
index a4ec2edd134..58b43352e1f 100644
--- a/mobile/android/base/resources/values/colors.xml
+++ b/mobile/android/base/resources/values/colors.xml
@@ -50,6 +50,7 @@
#666666
#7F828A
+ #33b5e5
#FF9500
diff --git a/mobile/android/base/resources/values/styles.xml b/mobile/android/base/resources/values/styles.xml
index 8d2a7e53e4d..76a4a54f2ba 100644
--- a/mobile/android/base/resources/values/styles.xml
+++ b/mobile/android/base/resources/values/styles.xml
@@ -684,4 +684,8 @@
- @dimen/menu_item_row_height
+
+
diff --git a/mobile/android/base/resources/values/themes.xml b/mobile/android/base/resources/values/themes.xml
index 6a3430d4b34..0033cf8cbc2 100644
--- a/mobile/android/base/resources/values/themes.xml
+++ b/mobile/android/base/resources/values/themes.xml
@@ -92,6 +92,7 @@
- @style/Widget.MenuItemActionBar
- @style/GeckoActionBar.Button
- @style/Widget.MenuItemSecondaryActionBar
+ - @style/FloatingHintEditText
diff --git a/mobile/android/base/widget/FloatingHintEditText.java b/mobile/android/base/widget/FloatingHintEditText.java
new file mode 100644
index 00000000000..2a87c314666
--- /dev/null
+++ b/mobile/android/base/widget/FloatingHintEditText.java
@@ -0,0 +1,168 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.widget;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.FontMetricsInt;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.widget.EditText;
+
+import org.mozilla.gecko.R;
+
+public class FloatingHintEditText extends EditText {
+ private static enum Animation { NONE, SHRINK, GROW }
+ private static final float HINT_SCALE = 0.6f;
+ private static final int ANIMATION_STEPS = 6;
+
+ private final Paint floatingHintPaint = new Paint();
+ private final ColorStateList floatingHintColors;
+ private final ColorStateList normalHintColors;
+ private final int defaultFloatingHintColor;
+ private final int defaultNormalHintColor;
+
+ private boolean wasEmpty;
+
+ private int animationFrame;
+ private Animation animation = Animation.NONE;
+
+ public FloatingHintEditText(Context context) {
+ this(context, null);
+ }
+
+ public FloatingHintEditText(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.floatingHintEditTextStyle);
+ }
+
+ public FloatingHintEditText(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ floatingHintColors = getResources().getColorStateList(R.color.floating_hint_text);
+ normalHintColors = getHintTextColors();
+ defaultFloatingHintColor = floatingHintColors.getDefaultColor();
+ defaultNormalHintColor = normalHintColors.getDefaultColor();
+ wasEmpty = TextUtils.isEmpty(getText());
+ }
+
+ @Override
+ public int getCompoundPaddingTop() {
+ final FontMetricsInt metrics = getPaint().getFontMetricsInt();
+ final int floatingHintHeight = (int) ((metrics.bottom - metrics.top) * HINT_SCALE);
+ return super.getCompoundPaddingTop() + floatingHintHeight;
+ }
+
+ @Override
+ protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
+ super.onTextChanged(text, start, lengthBefore, lengthAfter);
+
+ final boolean isEmpty = TextUtils.isEmpty(getText());
+
+ // The empty state hasn't changed, so the hint stays the same.
+ if (wasEmpty == isEmpty) {
+ return;
+ }
+
+ wasEmpty = isEmpty;
+
+ // Don't animate if we aren't visible.
+ if (!isShown()) {
+ return;
+ }
+
+ if (isEmpty) {
+ animation = Animation.GROW;
+
+ // The TextView will show a hint since the field is empty, but since we're animating
+ // from the floating hint, we don't want the normal hint to appear yet. We set it to
+ // transparent here, then restore the hint color after the animation has finished.
+ setHintTextColor(Color.TRANSPARENT);
+ } else {
+ animation = Animation.SHRINK;
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ if (TextUtils.isEmpty(getHint())) {
+ return;
+ }
+
+ final boolean isAnimating = (animation != Animation.NONE);
+
+ // The large hint is drawn by Android, so do nothing.
+ if (!isAnimating && TextUtils.isEmpty(getText())) {
+ return;
+ }
+
+ final Paint paint = getPaint();
+ final float hintPosX = getCompoundPaddingLeft() + getScrollX();
+ final float normalHintPosY = getBaseline();
+ final float floatingHintPosY = normalHintPosY + paint.getFontMetricsInt().top + getScrollY();
+ final float normalHintSize = getTextSize();
+ final float floatingHintSize = normalHintSize * HINT_SCALE;
+ final int[] stateSet = getDrawableState();
+ final int floatingHintColor = floatingHintColors.getColorForState(stateSet, defaultFloatingHintColor);
+
+ floatingHintPaint.set(paint);
+
+ // If we're not animating, we're showing the floating hint, so draw it and bail.
+ if (!isAnimating) {
+ drawHint(canvas, floatingHintSize, floatingHintColor, hintPosX, floatingHintPosY);
+ return;
+ }
+
+ // We are animating, so draw the linearly interpolated frame.
+ final int normalHintColor = normalHintColors.getColorForState(stateSet, defaultNormalHintColor);
+ if (animation == Animation.SHRINK) {
+ drawAnimationFrame(canvas, normalHintSize, floatingHintSize,
+ hintPosX, normalHintPosY, floatingHintPosY, normalHintColor, floatingHintColor);
+ } else {
+ drawAnimationFrame(canvas, floatingHintSize, normalHintSize,
+ hintPosX, floatingHintPosY, normalHintPosY, floatingHintColor, normalHintColor);
+ }
+
+ animationFrame++;
+
+ if (animationFrame == ANIMATION_STEPS) {
+ // After the grow animation has finished, restore the normal TextView hint color that we
+ // removed in our onTextChanged listener.
+ if (animation == Animation.GROW) {
+ setHintTextColor(normalHintColors);
+ }
+
+ animation = Animation.NONE;
+ animationFrame = 0;
+ }
+
+ invalidate();
+ }
+
+ private void drawAnimationFrame(Canvas canvas, float fromSize, float toSize,
+ float hintPosX, float fromY, float toY, int fromColor, int toColor) {
+ final float textSize = lerp(fromSize, toSize);
+ final float hintPosY = lerp(fromY, toY);
+ final int color = Color.rgb((int) lerp(Color.red(fromColor), Color.red(toColor)),
+ (int) lerp(Color.green(fromColor), Color.green(toColor)),
+ (int) lerp(Color.blue(fromColor), Color.blue(toColor)));
+ drawHint(canvas, textSize, color, hintPosX, hintPosY);
+ }
+
+ private void drawHint(Canvas canvas, float textSize, int color, float x, float y) {
+ floatingHintPaint.setTextSize(textSize);
+ floatingHintPaint.setColor(color);
+ canvas.drawText(getHint().toString(), x, y, floatingHintPaint);
+ }
+
+ private float lerp(float from, float to) {
+ final float alpha = (float) animationFrame / (ANIMATION_STEPS - 1);
+ return from * (1 - alpha) + to * alpha;
+ }
+}