Skip to content

Instantly share code, notes, and snippets.

@jromero
Forked from chrisbanes/FloatLabelLayout.java
Last active September 25, 2015 01:30
Show Gist options
  • Star 20 You must be signed in to star a gist
  • Fork 10 You must be signed in to fork a gist
  • Save jromero/233cfd8ef22a8b8c29ea to your computer and use it in GitHub Desktop.
Save jromero/233cfd8ef22a8b8c29ea to your computer and use it in GitHub Desktop.
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<resources>
<declare-styleable name="FloatLabelLayout">
<attr name="floatLabelTextAppearance" format="reference" />
<attr name="floatLabelTrigger" format="enum">
<enum name="text" value="0" />
<enum name="focus" value="1" />
</attr>
<attr name="floatLabelSidePadding" format="reference|dimension" />
</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 grey) -->
<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"
android:layout_marginTop="16dp"
app:floatLabelTrigger="text"
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"
android:layout_marginTop="16dp"
app:floatLabelTrigger="focus"
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">@drawable/example_colors_float_label</item>
<item name="android:textSize">11sp</item>
<item name="android:textStyle">bold</item>
</style>
</resources>
/*
* 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.
*/
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.FrameLayout;
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 FrameLayout {
private static final long ANIMATION_DURATION = 150;
private static final float DEFAULT_PADDING_LEFT_RIGHT_DP = 12f;
private static final String SAVED_SUPER_STATE = "SAVED_SUPER_STATE";
private static final String SAVED_LABEL_VISIBILITY = "SAVED_LABEL_VISIBILITY";
private static final String SAVED_HINT = "SAVED_HINT";
private EditText mEditText;
private TextView mLabel;
private Trigger mTrigger;
private CharSequence mHint;
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);
final TypedArray a = context
.obtainStyledAttributes(attrs, R.styleable.FloatLabelLayout);
final int sidePadding = a.getDimensionPixelSize(
R.styleable.FloatLabelLayout_floatLabelSidePadding,
dipsToPix(DEFAULT_PADDING_LEFT_RIGHT_DP));
mLabel = new TextView(context);
mLabel.setPadding(sidePadding, 0, sidePadding, 0);
mLabel.setVisibility(INVISIBLE);
mLabel.setTextAppearance(context,
a.getResourceId(R.styleable.FloatLabelLayout_floatLabelTextAppearance,
android.R.style.TextAppearance_Small)
);
int triggerInt = a.getInt(R.styleable.FloatLabelLayout_floatLabelTrigger, Trigger.TYPE.getValue());
mTrigger = Trigger.fromValue(triggerInt);
addView(mLabel, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
a.recycle();
}
@Override
protected Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putParcelable(SAVED_SUPER_STATE, super.onSaveInstanceState());
bundle.putInt(SAVED_LABEL_VISIBILITY, mLabel.getVisibility());
bundle.putCharSequence(SAVED_HINT, mHint);
return bundle;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
mLabel.setVisibility(bundle.getInt(SAVED_LABEL_VISIBILITY));
mHint = bundle.getCharSequence(SAVED_HINT);
// retrieve super state
state = bundle.getParcelable(SAVED_SUPER_STATE);
}
super.onRestoreInstanceState(state);
}
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
if (child instanceof 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");
}
// Update the layout params so that the EditText is at the bottom, with enough top
// margin to show the label
final LayoutParams lp = new LayoutParams(params);
lp.gravity = Gravity.BOTTOM;
lp.topMargin = (int) mLabel.getTextSize();
params = lp;
setEditText((EditText) child);
}
// Carry on adding the View...
super.addView(child, index, params);
}
protected void setEditText(EditText editText) {
mEditText = editText;
mLabel.setText(mEditText.getHint());
if (mHint == null) {
mHint = mEditText.getHint();
}
// Add a TextWatcher so that we know when the text input has changed
mEditText.addTextChangedListener(mTextWatcher);
// 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(mOnFocusChangeListener);
// if view already had focus we need to manually call the listener
if (mTrigger == Trigger.FOCUS && mEditText.isFocused()) {
mOnFocusChangeListener.onFocusChange(mEditText, true);
}
}
/**
* @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;
}
/**
* Show the label using an animation
*/
protected void showLabel() {
if (mLabel.getVisibility() != View.VISIBLE) {
mLabel.setVisibility(View.VISIBLE);
mLabel.setAlpha(0f);
mLabel.setTranslationY(mLabel.getHeight());
mLabel.animate()
.alpha(1f)
.translationY(0f)
.setDuration(ANIMATION_DURATION)
.setListener(null).start();
}
}
/**
* Hide the label using an animation
*/
protected 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();
}
/**
* Helper method to convert dips to pixels.
*/
private int dipsToPix(float dps) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dps,
getResources().getDisplayMetrics());
}
private OnFocusChangeListener mOnFocusChangeListener = new OnFocusChangeListener() {
@Override
public void onFocusChange(View view, boolean focused) {
mLabel.setActivated(focused);
if (mTrigger == Trigger.FOCUS) {
if (focused) {
mEditText.setHint("");
showLabel();
} else {
if (TextUtils.isEmpty(mEditText.getText())) {
mEditText.setHint(mHint);
hideLabel();
}
}
}
}
};
private TextWatcher mTextWatcher = new TextWatcher() {
@Override
public void afterTextChanged(Editable s) {
// only takes affect if mTrigger is set to TYPE
if (mTrigger != Trigger.TYPE) {
return;
}
if (TextUtils.isEmpty(s)) {
hideLabel();
} else {
showLabel();
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
};
public static enum Trigger {
TYPE(0),
FOCUS(1);
private final int mValue;
private Trigger(int i) {
mValue = i;
}
public int getValue() {
return mValue;
}
public static Trigger fromValue(int value) {
Trigger[] triggers = Trigger.values();
for (int i = 0; i < triggers.length; i++) {
if (triggers[i].getValue() == value) {
return triggers[i];
}
}
throw new IllegalArgumentException(value + " is not a valid value for " + Trigger.class.getSimpleName());
}
}
}
@kyleduo
Copy link

kyleduo commented Oct 23, 2014

Works well! Thanks for your work!

@KennethYo
Copy link

nice work!

@a-latta
Copy link

a-latta commented Aug 14, 2015

Nice but this requires minimum API 14. Any workaround for supporting API 10?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment