-
-
Save chrisbanes/11247418 to your computer and use it in GitHub Desktop.
<!-- | |
Copyright (C) 2014 Chris Banes | |
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. | |
--> | |
<?xml version="1.0" encoding="utf-8"?> | |
<resources> | |
<declare-styleable name="FloatLabelLayout"> | |
<attr name="floatLabelTextAppearance" format="reference" /> | |
<attr name="floatLabelPaddingLeft" format="reference|dimension" /> | |
<attr name="floatLabelPaddingRight" format="reference|dimension" /> | |
<attr name="floatLabelPaddingTop" format="reference|dimension" /> | |
<attr name="floatLabelPaddingBottom" format="reference|dimension" /> | |
<!-- The hint to display in the floating label --> | |
<attr name="floatLabelHint" format="reference|string" /> | |
</declare-styleable> | |
</resources> |
<?xml version="1.0" encoding="utf-8"?> | |
<selector xmlns:android="http://schemas.android.com/apk/res/android"> | |
<!-- The color when activated/focused (usually your app's accent color) --> | |
<item android:color="..." android:state_activated="true" /> | |
<!-- The color when not activated/focused (usually android:textColorSecondary) --> | |
<item android:color="..." /> | |
</selector> |
<?xml version="1.0" encoding="utf-8"?> | |
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | |
xmlns:app="http://schemas.android.com/apk/res-auto" | |
android:orientation="vertical" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent"> | |
<your.package.FloatLabelLayout | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
app:floatLabelTextAppearance="@style/TextAppearance.YourApp.FloatLabel"> | |
<EditText | |
android:id="@+id/edit_username" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:hint="@string/account_username_hint" | |
android:singleLine="true" | |
android:inputType="textNoSuggestions" | |
android:imeOptions="actionNext" | |
android:nextFocusDown="@+id/edit_password" /> | |
</your.package.FloatLabelLayout> | |
<your.package.FloatLabelLayout | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
app:floatLabelTextAppearance="@style/TextAppearance.YourApp.FloatLabel"> | |
<EditText | |
android:id="@+id/edit_password" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:hint="@string/account_password_hint" | |
android:singleLine="true" | |
android:inputType="textNoSuggestions" | |
android:imeOptions="actionDone" /> | |
</your.package.FloatLabelLayout> | |
</LinearLayout> |
<?xml version="1.0" encoding="utf-8"?> | |
<resources> | |
<style name="TextAppearance.YourApp.FloatLabel" parent="android:TextAppearance.Small"> | |
<item name="android:textColor">@color/float_label</item> | |
<item name="android:textSize">12sp</item> | |
</style> | |
</resources> |
/* | |
* Copyright 2014 Chris Banes | |
* | |
* 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. | |
*/ | |
import android.content.Context; | |
import android.content.res.TypedArray; | |
import android.os.Build; | |
import android.support.v4.view.ViewCompat; | |
import android.support.v4.view.ViewPropertyAnimatorListenerAdapter; | |
import android.text.Editable; | |
import android.text.TextUtils; | |
import android.text.TextWatcher; | |
import android.util.AttributeSet; | |
import android.util.TypedValue; | |
import android.view.View; | |
import android.view.ViewGroup; | |
import android.view.animation.AnimationUtils; | |
import android.view.animation.Interpolator; | |
import android.widget.EditText; | |
import android.widget.LinearLayout; | |
import android.widget.TextView; | |
/** | |
* Layout which an {@link android.widget.EditText} to show a floating label when the hint is hidden | |
* due to the user inputting text. | |
* | |
* @see <a href="https://dribbble.com/shots/1254439--GIF-Mobile-Form-Interaction">Matt D. Smith on Dribble</a> | |
* @see <a href="http://bradfrostweb.com/blog/post/float-label-pattern/">Brad Frost's blog post</a> | |
*/ | |
public class FloatLabelLayout extends LinearLayout { | |
private static final long ANIMATION_DURATION = 150; | |
private static final float DEFAULT_LABEL_PADDING_LEFT = 3f; | |
private static final float DEFAULT_LABEL_PADDING_TOP = 4f; | |
private static final float DEFAULT_LABEL_PADDING_RIGHT = 3f; | |
private static final float DEFAULT_LABEL_PADDING_BOTTOM = 4f; | |
private EditText mEditText; | |
private TextView mLabel; | |
private CharSequence mHint; | |
private Interpolator mInterpolator; | |
public FloatLabelLayout(Context context) { | |
this(context, null); | |
} | |
public FloatLabelLayout(Context context, AttributeSet attrs) { | |
this(context, attrs, 0); | |
} | |
public FloatLabelLayout(Context context, AttributeSet attrs, int defStyle) { | |
super(context, attrs, defStyle); | |
setOrientation(VERTICAL); | |
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FloatLabelLayout); | |
int leftPadding = a.getDimensionPixelSize( | |
R.styleable.FloatLabelLayout_floatLabelPaddingLeft, | |
dipsToPix(DEFAULT_LABEL_PADDING_LEFT)); | |
int topPadding = a.getDimensionPixelSize( | |
R.styleable.FloatLabelLayout_floatLabelPaddingTop, | |
dipsToPix(DEFAULT_LABEL_PADDING_TOP)); | |
int rightPadding = a.getDimensionPixelSize( | |
R.styleable.FloatLabelLayout_floatLabelPaddingRight, | |
dipsToPix(DEFAULT_LABEL_PADDING_RIGHT)); | |
int bottomPadding = a.getDimensionPixelSize( | |
R.styleable.FloatLabelLayout_floatLabelPaddingBottom, | |
dipsToPix(DEFAULT_LABEL_PADDING_BOTTOM)); | |
mHint = a.getText(R.styleable.FloatLabelLayout_floatLabelHint); | |
mLabel = new TextView(context); | |
mLabel.setPadding(leftPadding, topPadding, rightPadding, bottomPadding); | |
mLabel.setVisibility(INVISIBLE); | |
mLabel.setText(mHint); | |
ViewCompat.setPivotX(mLabel, 0f); | |
ViewCompat.setPivotY(mLabel, 0f); | |
mLabel.setTextAppearance(context, | |
a.getResourceId(R.styleable.FloatLabelLayout_floatLabelTextAppearance, | |
android.R.style.TextAppearance_Small)); | |
a.recycle(); | |
addView(mLabel, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); | |
mInterpolator = AnimationUtils.loadInterpolator(context, | |
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP | |
? android.R.interpolator.fast_out_slow_in | |
: android.R.anim.decelerate_interpolator); | |
} | |
@Override | |
public final void addView(View child, int index, ViewGroup.LayoutParams params) { | |
if (child instanceof EditText) { | |
setEditText((EditText) child); | |
} | |
// Carry on adding the View... | |
super.addView(child, index, params); | |
} | |
private void setEditText(EditText editText) { | |
// If we already have an EditText, throw an exception | |
if (mEditText != null) { | |
throw new IllegalArgumentException("We already have an EditText, can only have one"); | |
} | |
mEditText = editText; | |
// Update the label visibility with no animation | |
updateLabelVisibility(false); | |
// Add a TextWatcher so that we know when the text input has changed | |
mEditText.addTextChangedListener(new TextWatcher() { | |
@Override | |
public void afterTextChanged(Editable s) { | |
updateLabelVisibility(true); | |
} | |
@Override | |
public void beforeTextChanged(CharSequence s, int start, int count, int after) {} | |
@Override | |
public void onTextChanged(CharSequence s, int start, int before, int count) {} | |
}); | |
// Add focus listener to the EditText so that we can notify the label that it is activated. | |
// Allows the use of a ColorStateList for the text color on the label | |
mEditText.setOnFocusChangeListener(new OnFocusChangeListener() { | |
@Override | |
public void onFocusChange(View view, boolean focused) { | |
updateLabelVisibility(true); | |
} | |
}); | |
// If we do not have a valid hint, try and retrieve it from the EditText | |
if (TextUtils.isEmpty(mHint)) { | |
setHint(mEditText.getHint()); | |
} | |
} | |
private void updateLabelVisibility(boolean animate) { | |
boolean hasText = !TextUtils.isEmpty(mEditText.getText()); | |
boolean isFocused = mEditText.isFocused(); | |
mLabel.setActivated(isFocused); | |
if (hasText || isFocused) { | |
// We should be showing the label so do so if it isn't already | |
if (mLabel.getVisibility() != VISIBLE) { | |
showLabel(animate); | |
} | |
} else { | |
// We should not be showing the label so hide it | |
if (mLabel.getVisibility() == VISIBLE) { | |
hideLabel(animate); | |
} | |
} | |
} | |
/** | |
* @return the {@link android.widget.EditText} text input | |
*/ | |
public EditText getEditText() { | |
return mEditText; | |
} | |
/** | |
* @return the {@link android.widget.TextView} label | |
*/ | |
public TextView getLabel() { | |
return mLabel; | |
} | |
/** | |
* Set the hint to be displayed in the floating label | |
*/ | |
public void setHint(CharSequence hint) { | |
mHint = hint; | |
mLabel.setText(hint); | |
} | |
/** | |
* Show the label | |
*/ | |
private void showLabel(boolean animate) { | |
if (animate) { | |
mLabel.setVisibility(View.VISIBLE); | |
ViewCompat.setTranslationY(mLabel, mLabel.getHeight()); | |
float scale = mEditText.getTextSize() / mLabel.getTextSize(); | |
ViewCompat.setScaleX(mLabel, scale); | |
ViewCompat.setScaleY(mLabel, scale); | |
ViewCompat.animate(mLabel) | |
.translationY(0f) | |
.scaleY(1f) | |
.scaleX(1f) | |
.setDuration(ANIMATION_DURATION) | |
.setListener(null) | |
.setInterpolator(mInterpolator).start(); | |
} else { | |
mLabel.setVisibility(VISIBLE); | |
} | |
mEditText.setHint(null); | |
} | |
/** | |
* Hide the label | |
*/ | |
private void hideLabel(boolean animate) { | |
if (animate) { | |
float scale = mEditText.getTextSize() / mLabel.getTextSize(); | |
ViewCompat.setScaleX(mLabel, 1f); | |
ViewCompat.setScaleY(mLabel, 1f); | |
ViewCompat.setTranslationY(mLabel, 0f); | |
ViewCompat.animate(mLabel) | |
.translationY(mLabel.getHeight()) | |
.setDuration(ANIMATION_DURATION) | |
.scaleX(scale) | |
.scaleY(scale) | |
.setListener(new ViewPropertyAnimatorListenerAdapter() { | |
@Override | |
public void onAnimationEnd(View view) { | |
mLabel.setVisibility(INVISIBLE); | |
mEditText.setHint(mHint); | |
} | |
}) | |
.setInterpolator(mInterpolator).start(); | |
} else { | |
mLabel.setVisibility(INVISIBLE); | |
mEditText.setHint(mHint); | |
} | |
} | |
/** | |
* Helper method to convert dips to pixels. | |
*/ | |
private int dipsToPix(float dps) { | |
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dps, | |
getResources().getDisplayMetrics()); | |
} | |
} |
I noticed this only works with android 3.0 and higher.
With a couple more lines of code I was able to get to work all the way back to API 1 :)
private void showLabel() {
mLabel.setVisibility(View.VISIBLE);
// mLabel.setAlpha(0f);
// mLabel.setTranslationY(mLabel.getHeight());
// mLabel.animate()
// .alpha(1f)
// .translationY(0f)
// .setDuration(ANIMATION_DURATION)
// .setListener(null).start();
AnimationSet animationSet = new AnimationSet(true);
TranslateAnimation translateAnimation = new TranslateAnimation(
Animation.RELATIVE_TO_SELF,0, Animation.RELATIVE_TO_SELF,0,
Animation.ABSOLUTE, mLabel.getHeight(), Animation.ABSOLUTE, 0);
translateAnimation.setDuration(300);
animationSet.addAnimation(translateAnimation);
AlphaAnimation alphaAnimation = new AlphaAnimation(0f, 1f);
alphaAnimation.setDuration(300);
animationSet.addAnimation(alphaAnimation);
mLabel.startAnimation(animationSet);
}
private void hideLabel() {
// mLabel.setAlpha(1f);
// mLabel.setTranslationY(0f);
// mLabel.animate()
// .alpha(0f)
// .translationY(mLabel.getHeight())
// .setDuration(ANIMATION_DURATION)
// .setListener(new AnimatorListenerAdapter() {
// @Override
// public void onAnimationEnd(Animator animation) {
// mLabel.setVisibility(View.GONE);
// }
// }).start();
//
AnimationSet animationSet = new AnimationSet(true);
TranslateAnimation translateAnimation = new TranslateAnimation(
Animation.RELATIVE_TO_SELF,0, Animation.RELATIVE_TO_SELF,0,
Animation.ABSOLUTE,0, Animation.ABSOLUTE,mLabel.getHeight());
translateAnimation.setDuration(300);
animationSet.addAnimation(translateAnimation);
AlphaAnimation alphaAnimation = new AlphaAnimation(1f, 0f);
alphaAnimation.setDuration(300);
animationSet.addAnimation(alphaAnimation);
animationSet.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
mLabel.setVisibility(View.GONE);
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
mLabel.startAnimation(animationSet);
}
I'd like to add Gravity to this file because sometimes the TextViews need to to be displayed on top, but I don't know how to add it as a styleable. For now I just hard-coded it in the file. Please, help!
I agree with @DomenicoP
I've forked it to add a configurable "trigger". In my case, I would have liked the label to be displayed when the user focuses on the EditTextView so I added the option to do just that. You can see the changes here: https://gist.github.com/jromero/233cfd8ef22a8b8c29ea
@kenticus, you forgot to add one more thing to support older OS.
mEditText.setOnFocusChangeListener(new OnFocusChangeListener() {
@Override
public void onFocusChange(View view, boolean focused) {
if (android.os.Build.VERSION.SDK_INT>= android.os.Build.VERSION_CODES.HONEYCOMB) {
mLabel.setActivated(focused); // only available after API 11
}
}
});
so cool !!!
I've had success with this technique wrapping more than just EditText components. In fact, if you reduce the EditText to a TextView you can apply this technique to a wide range of other components, including Buttons (a technique I've used for fake Spinners).
@kenticus with nineoldandroids,
you can do it just like this
/**
* Show the label using an animation
*/
private void showLabel() {
mLabel.setVisibility(View.VISIBLE);
ViewHelper.setAlpha(mLabel, 0f);
ViewHelper.setTranslationY(mLabel, mLabel.getHeight());
ViewPropertyAnimator.animate(mLabel)
.alpha(1f)
.translationY(0f)
.setDuration(ANIMATION_DURATION)
.setListener(null).start();
}
/**
* Hide the label using an animation
*/
private void hideLabel() {
ViewHelper.setAlpha(mLabel, 1f);
ViewHelper.setTranslationY(mLabel, 0f);
ViewPropertyAnimator.animate(mLabel)
.alpha(0f)
.translationY(mLabel.getHeight())
.setDuration(ANIMATION_DURATION)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mLabel.setVisibility(View.GONE);
}
}).start();
}
Hi @chrisbanes I encountered an issue where my edittext background (partially) covered the label so I forked and added a buffer-gap attribute (floatLabelGapSize) between the label and edittext. Not sure if you want to include this or not (no pull requests on Gist)
https://gist.github.com/AndyFrench/6069c89bdd2567668415
To make things easier for pull requests and updates I turned this idea into a library. Feel free to check it out.
It's so great!
Great post. Thanks. I was just wondering, will this method still support the EditText tinting provided by Appcompat v21? I am assuming it will, considering you are still using a normal EditText control and not a custom extension of it
Thanks
@kenticus @findjigar The top margin applied to the EditText in order to make room for the label will be ignored before Android 3 because of this bug in LinearLayout so more work is needed to make it compatible with older versions. I chosed to use a vertical LinearLayout instead of a FrameLayout.
@vincentvanzyl Just started using this with the AppCompat-v21 and it works great.
thanks for this nice implementation! I lifted it to jcenter - if anyone else needs it:
https://github.com/ligi/FloatingLabelLayout
it's useful
I'm trying to figure out what's my problem with this. I pretty much copy paste the code and the animation is not smooth at the end when the label transforms into the hint. There's like a jump.
In other words, the position of the hint and the label is different when it's supposed to be the same.
Anyone having this problem??
PD: More details to this: The label text looks wider than the hint text and the final position when is going down is higher than the hint text.
The padding top custom attribute messes up the show/hide animations. Here's a fix:
mTopPadding = a.getDimensionPixelSize(
R.styleable.FloatLabelLayout_floatLabelPaddingTop,
dipsToPix(DEFAULT_LABEL_PADDING_TOP));
private void showLabel(boolean animate) {
if (animate) {
mLabel.setVisibility(View.VISIBLE);
ViewCompat.setTranslationY(mLabel, mLabel.getHeight() - mTopPadding);
float scale = mEditText.getTextSize() / mLabel.getTextSize();
ViewCompat.setScaleX(mLabel, scale);
ViewCompat.setScaleY(mLabel, scale);
ViewCompat.animate(mLabel)
.translationY(0f)
.scaleY(1f)
.scaleX(1f)
.setDuration(ANIMATION_DURATION)
.setListener(null)
.setInterpolator(mInterpolator).start();
} else {
mLabel.setVisibility(VISIBLE);
}
mEditText.setHint(null);
}
private void hideLabel(boolean animate) {
if (animate) {
float scale = mEditText.getTextSize() / mLabel.getTextSize();
ViewCompat.setScaleX(mLabel, 1f);
ViewCompat.setScaleY(mLabel, 1f);
ViewCompat.setTranslationY(mLabel, 0f);
ViewCompat.animate(mLabel)
.translationY(mLabel.getHeight() - mTopPadding)
.setDuration(ANIMATION_DURATION)
.scaleX(scale)
.scaleY(scale)
.setListener(new ViewPropertyAnimatorListenerAdapter() {
@Override
public void onAnimationEnd(View view) {
super.onAnimationEnd(view);
mLabel.setVisibility(INVISIBLE);
mEditText.setHint(mHint);
}
})
.setInterpolator(mInterpolator).start();
} else {
mLabel.setVisibility(INVISIBLE);
mEditText.setHint(mHint);
}
}
You should definitely set this up as a maven drop in - it's being used by so many people now :)
@chrisbanes: This is very nice code. I patched the FloatLabelLayout
and used a couple of those in my activity, but I see that there's an extra padding before the EditText
which doesn't vertically align the starting edges of the label and the inputText. Any ideas on why this is happening and how I can fix this?
This is the code that I used is in the Edit History.
EDIT: Ah! looks like the answer to my question is here http://stackoverflow.com/a/29312321/3028966.
@chrisbanes:thanks for sharing so perfect view ,I learn much more . Also, wo want to consult : how to add external resource to maven center in the android studio . like below figure
I can only select resource ,but I want to add all my resources to this inner maven .
my google email address : [email protected]
respect for your advise
Amazing!
How to change the line color programically?
Thank you,That's really cool.