diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3c117df
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,65 @@
+# Built application files
+*.apk
+*.ap_
+
+# Files for the ART/Dalvik VM
+*.dex
+
+# Java class files
+*.class
+
+# Generated files
+bin/
+gen/
+out/
+
+# Gradle files
+.gradle/
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Proguard folder generated by Eclipse
+proguard/
+
+# Log Files
+*.log
+
+# Android Studio Navigation editor temp files
+.navigation/
+
+# Android Studio captures folder
+captures/
+
+# IntelliJ
+*.iml
+.idea/workspace.xml
+.idea/tasks.xml
+.idea/gradle.xml
+.idea/assetWizardSettings.xml
+.idea/dictionaries
+.idea/libraries
+.idea/caches
+
+# Keystore files
+# Uncomment the following line if you do not want to check your keystore files in.
+#*.jks
+
+# External native build folder generated in Android Studio 2.2 and later
+.externalNativeBuild
+
+# Google Services (e.g. APIs or Firebase)
+google-services.json
+
+# Freeline
+freeline.py
+freeline/
+freeline_project_description.json
+
+# fastlane
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots
+fastlane/test_output
+fastlane/readme.md
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+ * A value of {@code 0.0} indicates that the layout is fully expanded. + * A value of {@code 1.0} indicates that the layout is fully collapsed. + */ + void setExpansionFraction(float fraction) { + fraction = MathUtils.clamp(fraction, 0f, 1f); + + if (fraction != mExpandedFraction) { + mExpandedFraction = fraction; + calculateCurrentOffsets(); + } + } + + final boolean setState(final int[] state) { + mState = state; + + if (isStateful()) { + recalculate(); + return true; + } + + return false; + } + + final boolean isStateful() { + return (mCollapsedTextColor != null && mCollapsedTextColor.isStateful()) + || (mExpandedTextColor != null && mExpandedTextColor.isStateful()); + } + + float getExpansionFraction() { + return mExpandedFraction; + } + + float getCollapsedTextSize() { + return mCollapsedTextSize; + } + + float getExpandedTextSize() { + return mExpandedTextSize; + } + + private void calculateCurrentOffsets() { + calculateOffsets(mExpandedFraction); + } + + private void calculateOffsets(final float fraction) { + interpolateBounds(fraction); + mCurrentDrawX = lerp(mExpandedDrawX, mCollapsedDrawX, fraction, + mPositionInterpolator); + mCurrentDrawY = lerp(mExpandedDrawY, mCollapsedDrawY, fraction, + mPositionInterpolator); + + setInterpolatedTextSize(lerp(mExpandedTextSize, mCollapsedTextSize, + fraction, mTextSizeInterpolator)); + + if (mCollapsedTextColor != mExpandedTextColor) { + // If the collapsed and expanded text colors are different, blend them based on the + // fraction + mTextPaint.setColor(blendColors( + getCurrentExpandedTextColor(), getCurrentCollapsedTextColor(), fraction)); + } else { + mTextPaint.setColor(getCurrentCollapsedTextColor()); + } + + mTextPaint.setShadowLayer( + lerp(mExpandedShadowRadius, mCollapsedShadowRadius, fraction, null), + lerp(mExpandedShadowDx, mCollapsedShadowDx, fraction, null), + lerp(mExpandedShadowDy, mCollapsedShadowDy, fraction, null), + blendColors(mExpandedShadowColor, mCollapsedShadowColor, fraction)); + + ViewCompat.postInvalidateOnAnimation(mView); + } + + @ColorInt + private int getCurrentExpandedTextColor() { + if (mState != null) { + return mExpandedTextColor.getColorForState(mState, 0); + } else { + return mExpandedTextColor.getDefaultColor(); + } + } + + @ColorInt + private int getCurrentCollapsedTextColor() { + if (mState != null) { + return mCollapsedTextColor.getColorForState(mState, 0); + } else { + return mCollapsedTextColor.getDefaultColor(); + } + } + + private void calculateBaseOffsets() { + final float currentTextSize = mCurrentTextSize; + + // We then calculate the collapsed text size, using the same logic + calculateUsingTextSize(mCollapsedTextSize); + float width = mTextToDraw != null ? + mTextPaint.measureText(mTextToDraw, 0, mTextToDraw.length()) : 0; + final int collapsedAbsGravity = GravityCompat.getAbsoluteGravity(mCollapsedTextGravity, + mIsRtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR); + switch (collapsedAbsGravity & Gravity.VERTICAL_GRAVITY_MASK) { + case Gravity.BOTTOM: + mCollapsedDrawY = mCollapsedBounds.bottom; + break; + case Gravity.TOP: + mCollapsedDrawY = mCollapsedBounds.top - mTextPaint.ascent(); + break; + case Gravity.CENTER_VERTICAL: + default: + float textHeight = mTextPaint.descent() - mTextPaint.ascent(); + float textOffset = (textHeight / 2) - mTextPaint.descent(); + mCollapsedDrawY = mCollapsedBounds.centerY() + textOffset; + break; + } + switch (collapsedAbsGravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK) { + case Gravity.CENTER_HORIZONTAL: + mCollapsedDrawX = mCollapsedBounds.centerX() - (width / 2); + break; + case Gravity.RIGHT: + mCollapsedDrawX = mCollapsedBounds.right - width; + break; + case Gravity.LEFT: + default: + mCollapsedDrawX = mCollapsedBounds.left; + break; + } + + calculateUsingTextSize(mExpandedTextSize); + width = mTextToDraw != null + ? mTextPaint.measureText(mTextToDraw, 0, mTextToDraw.length()) : 0; + final int expandedAbsGravity = GravityCompat.getAbsoluteGravity(mExpandedTextGravity, + mIsRtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR); + switch (expandedAbsGravity & Gravity.VERTICAL_GRAVITY_MASK) { + case Gravity.BOTTOM: + mExpandedDrawY = mExpandedBounds.bottom; + break; + case Gravity.TOP: + mExpandedDrawY = mExpandedBounds.top - mTextPaint.ascent(); + break; + case Gravity.CENTER_VERTICAL: + default: + float textHeight = mTextPaint.descent() - mTextPaint.ascent(); + float textOffset = (textHeight / 2) - mTextPaint.descent(); + mExpandedDrawY = mExpandedBounds.centerY() + textOffset; + break; + } + switch (expandedAbsGravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK) { + case Gravity.CENTER_HORIZONTAL: + mExpandedDrawX = mExpandedBounds.centerX() - (width / 2); + break; + case Gravity.RIGHT: + mExpandedDrawX = mExpandedBounds.right - width; + break; + case Gravity.LEFT: + default: + mExpandedDrawX = mExpandedBounds.left; + break; + } + + // The bounds have changed so we need to clear the texture + clearTexture(); + // Now reset the text size back to the original + setInterpolatedTextSize(currentTextSize); + } + + private void interpolateBounds(float fraction) { + mCurrentBounds.left = lerp(mExpandedBounds.left, mCollapsedBounds.left, + fraction, mPositionInterpolator); + mCurrentBounds.top = lerp(mExpandedDrawY, mCollapsedDrawY, + fraction, mPositionInterpolator); + mCurrentBounds.right = lerp(mExpandedBounds.right, mCollapsedBounds.right, + fraction, mPositionInterpolator); + mCurrentBounds.bottom = lerp(mExpandedBounds.bottom, mCollapsedBounds.bottom, + fraction, mPositionInterpolator); + } + + public void draw(Canvas canvas) { + final int saveCount = canvas.save(); + + if (mTextToDraw != null && mDrawTitle) { + float x = mCurrentDrawX; + float y = mCurrentDrawY; + + final boolean drawTexture = mUseTexture && mExpandedTitleTexture != null; + + final float ascent; + final float descent; + if (drawTexture) { + ascent = mTextureAscent * mScale; + descent = mTextureDescent * mScale; + } else { + ascent = mTextPaint.ascent() * mScale; + descent = mTextPaint.descent() * mScale; + } + + if (DEBUG_DRAW) { + // Just a debug tool, which drawn a magenta rect in the text bounds + canvas.drawRect(mCurrentBounds.left, y + ascent, mCurrentBounds.right, y + descent, + DEBUG_DRAW_PAINT); + } + + if (drawTexture) { + y += ascent; + } + + if (mScale != 1f) { + canvas.scale(mScale, mScale, x, y); + } + + if (drawTexture) { + // If we should use a texture, draw it instead of text + canvas.drawBitmap(mExpandedTitleTexture, x, y, mTexturePaint); + } else { + canvas.drawText(mTextToDraw, 0, mTextToDraw.length(), x, y, mTextPaint); + } + } + + canvas.restoreToCount(saveCount); + } + + private boolean calculateIsRtl(CharSequence text) { + final boolean defaultIsRtl = ViewCompat.getLayoutDirection(mView) + == ViewCompat.LAYOUT_DIRECTION_RTL; + return (defaultIsRtl + ? TextDirectionHeuristicsCompat.FIRSTSTRONG_RTL + : TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR).isRtl(text, 0, text.length()); + } + + private void setInterpolatedTextSize(float textSize) { + calculateUsingTextSize(textSize); + + // Use our texture if the scale isn't 1.0 + mUseTexture = USE_SCALING_TEXTURE && mScale != 1f; + + if (mUseTexture) { + // Make sure we have an expanded texture if needed + ensureExpandedTexture(); + } + + ViewCompat.postInvalidateOnAnimation(mView); + } + + private boolean areTypefacesDifferent(Typeface first, Typeface second) { + return (first != null && !first.equals(second)) || (first == null && second != null); + } + + private void calculateUsingTextSize(final float textSize) { + if (mText == null) return; + + final float collapsedWidth = mCollapsedBounds.width(); + final float expandedWidth = mExpandedBounds.width(); + + final float availableWidth; + final float newTextSize; + boolean updateDrawText = false; + + if (isClose(textSize, mCollapsedTextSize)) { + newTextSize = mCollapsedTextSize; + mScale = 1f; + if (areTypefacesDifferent(mCurrentTypeface, mCollapsedTypeface)) { + mCurrentTypeface = mCollapsedTypeface; + updateDrawText = true; + } + availableWidth = collapsedWidth; + } else { + newTextSize = mExpandedTextSize; + if (areTypefacesDifferent(mCurrentTypeface, mExpandedTypeface)) { + mCurrentTypeface = mExpandedTypeface; + updateDrawText = true; + } + if (isClose(textSize, mExpandedTextSize)) { + // If we're close to the expanded text size, snap to it and use a scale of 1 + mScale = 1f; + } else { + // Else, we'll scale down from the expanded text size + mScale = textSize / mExpandedTextSize; + } + + final float textSizeRatio = mCollapsedTextSize / mExpandedTextSize; + // This is the size of the expanded bounds when it is scaled to match the + // collapsed text size + final float scaledDownWidth = expandedWidth * textSizeRatio; + + if (scaledDownWidth > collapsedWidth) { + // If the scaled down size is larger than the actual collapsed width, we need to + // cap the available width so that when the expanded text scales down, it matches + // the collapsed width + availableWidth = Math.min(collapsedWidth / textSizeRatio, expandedWidth); + } else { + // Otherwise we'll just use the expanded width + availableWidth = expandedWidth; + } + } + + if (availableWidth > 0) { + updateDrawText = (mCurrentTextSize != newTextSize) || mBoundsChanged || updateDrawText; + mCurrentTextSize = newTextSize; + mBoundsChanged = false; + } + + if (mTextToDraw == null || updateDrawText) { + mTextPaint.setTextSize(mCurrentTextSize); + mTextPaint.setTypeface(mCurrentTypeface); + // Use linear text scaling if we're scaling the canvas + mTextPaint.setLinearText(mScale != 1f); + + // If we don't currently have text to draw, or the text size has changed, ellipsize... + final CharSequence title = TextUtils.ellipsize(mText, mTextPaint, + availableWidth, TextUtils.TruncateAt.END); + if (!TextUtils.equals(title, mTextToDraw)) { + mTextToDraw = title; + mIsRtl = calculateIsRtl(mTextToDraw); + } + } + } + + private void ensureExpandedTexture() { + if (mExpandedTitleTexture != null || mExpandedBounds.isEmpty() + || TextUtils.isEmpty(mTextToDraw)) { + return; + } + + calculateOffsets(0f); + mTextureAscent = mTextPaint.ascent(); + mTextureDescent = mTextPaint.descent(); + + final int w = Math.round(mTextPaint.measureText(mTextToDraw, 0, mTextToDraw.length())); + final int h = Math.round(mTextureDescent - mTextureAscent); + + if (w <= 0 || h <= 0) { + return; // If the width or height are 0, return + } + + mExpandedTitleTexture = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); + + Canvas c = new Canvas(mExpandedTitleTexture); + c.drawText(mTextToDraw, 0, mTextToDraw.length(), 0, h - mTextPaint.descent(), mTextPaint); + + if (mTexturePaint == null) { + // Make sure we have a paint + mTexturePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); + } + } + + public void recalculate() { + if (mView.getHeight() > 0 && mView.getWidth() > 0) { + // If we've already been laid out, calculate everything now otherwise we'll wait + // until a layout + calculateBaseOffsets(); + calculateCurrentOffsets(); + } + } + + /** + * Set the title to display + * + * @param text + */ + void setText(CharSequence text) { + if (text == null || !text.equals(mText)) { + mText = text; + mTextToDraw = null; + clearTexture(); + recalculate(); + } + } + + CharSequence getText() { + return mText; + } + + private void clearTexture() { + if (mExpandedTitleTexture != null) { + mExpandedTitleTexture.recycle(); + mExpandedTitleTexture = null; + } + } + + /** + * Returns true if {@code value} is 'close' to it's closest decimal value. Close is currently + * defined as it's difference being < 0.001. + */ + private static boolean isClose(float value, float targetValue) { + return Math.abs(value - targetValue) < 0.001f; + } + + ColorStateList getExpandedTextColor() { + return mExpandedTextColor; + } + + ColorStateList getCollapsedTextColor() { + return mCollapsedTextColor; + } + + /** + * Blend {@code color1} and {@code color2} using the given ratio. + * + * @param ratio of which to blend. 0.0 will return {@code color1}, 0.5 will give an even blend, + * 1.0 will return {@code color2}. + */ + private static int blendColors(int color1, int color2, float ratio) { + final float inverseRatio = 1f - ratio; + float a = (Color.alpha(color1) * inverseRatio) + (Color.alpha(color2) * ratio); + float r = (Color.red(color1) * inverseRatio) + (Color.red(color2) * ratio); + float g = (Color.green(color1) * inverseRatio) + (Color.green(color2) * ratio); + float b = (Color.blue(color1) * inverseRatio) + (Color.blue(color2) * ratio); + return Color.argb((int) a, (int) r, (int) g, (int) b); + } + + private static float lerp(float startValue, float endValue, float fraction, + Interpolator interpolator) { + if (interpolator != null) { + fraction = interpolator.getInterpolation(fraction); + } + return OpenAnimationUtils.lerp(startValue, endValue, fraction); + } + + private static boolean rectEquals(Rect r, int left, int top, int right, int bottom) { + return !(r.left != left || r.top != top || r.right != right || r.bottom != bottom); + } +} diff --git a/library/src/main/java/br/com/opencraft/library/OpenDrawableUtils.java b/library/src/main/java/br/com/opencraft/library/OpenDrawableUtils.java new file mode 100644 index 0000000..a7665d3 --- /dev/null +++ b/library/src/main/java/br/com/opencraft/library/OpenDrawableUtils.java @@ -0,0 +1,46 @@ +package br.com.opencraft.library; + +import android.graphics.drawable.Drawable; +import android.graphics.drawable.DrawableContainer; +import android.util.Log; + +import java.lang.reflect.Method; + +public class OpenDrawableUtils { + + private static final String LOG_TAG = "DrawableUtils"; + + private static Method sSetConstantStateMethod; + private static boolean sSetConstantStateMethodFetched; + + private OpenDrawableUtils() {} + + static boolean setContainerConstantState(DrawableContainer drawable, + Drawable.ConstantState constantState) { + // We can use getDeclaredMethod() on v9+ + return setContainerConstantStateV9(drawable, constantState); + } + + private static boolean setContainerConstantStateV9(DrawableContainer drawable, + Drawable.ConstantState constantState) { + if (!sSetConstantStateMethodFetched) { + try { + sSetConstantStateMethod = DrawableContainer.class.getDeclaredMethod( + "setConstantState", DrawableContainer.DrawableContainerState.class); + sSetConstantStateMethod.setAccessible(true); + } catch (NoSuchMethodException e) { + Log.e(LOG_TAG, "Could not fetch setConstantState(). Oh well."); + } + sSetConstantStateMethodFetched = true; + } + if (sSetConstantStateMethod != null) { + try { + sSetConstantStateMethod.invoke(drawable, constantState); + return true; + } catch (Exception e) { + Log.e(LOG_TAG, "Could not invoke setConstantState(). Oh well."); + } + } + return false; + } +} diff --git a/library/src/main/java/br/com/opencraft/library/OpenInputLayout.java b/library/src/main/java/br/com/opencraft/library/OpenInputLayout.java new file mode 100644 index 0000000..f02a952 --- /dev/null +++ b/library/src/main/java/br/com/opencraft/library/OpenInputLayout.java @@ -0,0 +1,1524 @@ +package br.com.opencraft.library; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.DrawableContainer; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.DrawableRes; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.support.annotation.StyleRes; +import android.support.annotation.VisibleForTesting; +import android.support.design.widget.TextInputEditText; +import android.support.design.widget.TextInputLayout; +import android.support.v4.content.ContextCompat; +import android.support.v4.graphics.drawable.DrawableCompat; +import android.support.v4.view.AbsSavedState; +import android.support.v4.view.AccessibilityDelegateCompat; +import android.support.v4.view.GravityCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v4.widget.Space; +import android.support.v4.widget.TextViewCompat; +import android.support.v4.widget.ViewGroupUtils; +import android.support.v7.content.res.AppCompatResources; +import android.support.v7.widget.AppCompatDrawableManager; +import android.support.v7.widget.AppCompatTextView; +import android.support.v7.widget.WithHint; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.text.method.PasswordTransformationMethod; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseArray; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewStructure; +import android.view.accessibility.AccessibilityEvent; +import android.view.animation.AccelerateInterpolator; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; + + +/** + * >>> Customized version of TextInputLayout intended to allow the hint to move below + * >>> the edittext with some margin - João Rutkoski + *
+ *
+ * >>>TextInputLayout + * Layout which wraps an {@link android.widget.EditText} (or descendant) to show a floating label + * when the hint is hidden due to the user inputting text. + *
+ *
Also supports showing an error via {@link #setErrorEnabled(boolean)} and + * {@link #setError(CharSequence)}, and a character counter via + * {@link #setCounterEnabled(boolean)}.
+ *+ *
Password visibility toggling is also supported via the + * {@link #setPasswordVisibilityToggleEnabled(boolean)} API and related attribute. + * If enabled, a button is displayed to toggle between the password being displayed as plain-text + * or disguised, when your EditText is set to display a password.
+ *+ *
Note: When using the password toggle functionality, the 'end' compound + * drawable of the EditText will be overridden while the toggle is enabled. To ensure that any + * existing drawables are restored correctly, you should set those compound drawables relatively + * (start/end), opposed to absolutely (left/right).
+ *+ * The {@link TextInputEditText} class is provided to be used as a child of this layout. Using + * TextInputEditText allows TextInputLayout greater control over the visual aspects of any + * text input. An example usage is as so: + *
+ *
+ * <android.support.design.widget.TextInputLayout + * android:layout_width="match_parent" + * android:layout_height="wrap_content"> + * + * <android.support.design.widget.TextInputEditText + * android:layout_width="match_parent" + * android:layout_height="wrap_content" + * android:hint="@string/form_username"/> + * + * </android.support.design.widget.TextInputLayout> + *+ *
+ *
Note: The actual view hierarchy present under TextInputLayout is + * NOT guaranteed to match the view hierarchy as written in XML. As a result, + * calls to getParent() on children of the TextInputLayout -- such as an TextInputEditText -- + * may not return the TextInputLayout itself, but rather an intermediate View. If you need + * to access a View directly, set an {@code android:id} and use {@link View#findViewById(int)}. + */ + +public class OpenInputLayout extends LinearLayout implements WithHint { + + private static final int ANIMATION_DURATION = 200; + private static final int INVALID_MAX_LENGTH = -1; + + private static final String LOG_TAG = "TextInputLayout"; + + private final FrameLayout mInputFrame; + EditText mEditText; + private CharSequence mOriginalHint; + + private boolean mHintEnabled; + private CharSequence mHint; + + private Paint mTmpPaint; + private final Rect mTmpRect = new Rect(); + + private LinearLayout mIndicatorArea; + private int mIndicatorsAdded; + + private Typeface mTypeface; + + private boolean mErrorEnabled; + TextView mErrorView; + private int mErrorTextAppearance; + private boolean mErrorShown; + private CharSequence mError; + + boolean mCounterEnabled; + private TextView mCounterView; + private int mCounterMaxLength; + private int mCounterTextAppearance; + private int mCounterOverflowTextAppearance; + private boolean mCounterOverflowed; + + private boolean mPasswordToggleEnabled; + private Drawable mPasswordToggleDrawable; + private CharSequence mPasswordToggleContentDesc; + private OpenCheckableImageButton mPasswordToggleView; + private boolean mPasswordToggledVisible; + private Drawable mPasswordToggleDummyDrawable; + private Drawable mOriginalEditTextEndDrawable; + + private ColorStateList mPasswordToggleTintList; + private boolean mHasPasswordToggleTintList; + private PorterDuff.Mode mPasswordToggleTintMode; + private boolean mHasPasswordToggleTintMode; + + private ColorStateList mDefaultTextColor; + private ColorStateList mFocusedTextColor; + + // Only used for testing + private boolean mHintExpanded; + + private OpenCollapsingTextHelper mCollapsingTextHelper; + + private boolean mHintAnimationEnabled; + private ValueAnimator mAnimator; + + private boolean mHasReconstructedEditTextBackground; + private boolean mInDrawableStateChanged; + + private boolean mRestoringSavedState; + + //Defines whether the hint will move up or down + private boolean mMoveLabelUp = true; + private float mLabelSpacingExtra = 0f; + + public OpenInputLayout(Context context) { + this(context, null); + } + + public OpenInputLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public OpenInputLayout(Context context, AttributeSet attrs, int defStyleAttr) { + // Can't call through to super(Context, AttributeSet, int) since it doesn't exist on API 10 + super(context, attrs); + + mCollapsingTextHelper = new OpenCollapsingTextHelper(this, attrs); + +// ThemeUtils.checkAppCompatTheme(context); + + setOrientation(VERTICAL); + setWillNotDraw(false); + setAddStatesFromChildren(true); + + mInputFrame = new FrameLayout(context); + mInputFrame.setAddStatesFromChildren(true); + addView(mInputFrame); + + mCollapsingTextHelper.setTextSizeInterpolator(OpenAnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); + mCollapsingTextHelper.setPositionInterpolator(new AccelerateInterpolator()); + mCollapsingTextHelper.setCollapsedTextGravity(Gravity.TOP | GravityCompat.START); + + final TypedArray a = context.getTheme().obtainStyledAttributes( + attrs, + R.styleable.OpenInputLayout, + defStyleAttr, android.support.design.R.style.Widget_Design_TextInputLayout); + + mHintEnabled = a.getBoolean(R.styleable.OpenInputLayout_hintEnabled, true); + setHint(a.getText(R.styleable.OpenInputLayout_android_hint)); + mHintAnimationEnabled = a.getBoolean(R.styleable.OpenInputLayout_hintAnimationEnabled, true); + + if (a.hasValue(R.styleable.OpenInputLayout_android_textColorHint)) { + mDefaultTextColor = mFocusedTextColor = a.getColorStateList(R.styleable.OpenInputLayout_android_textColorHint); + } + + final int hintAppearance = a.getResourceId(R.styleable.OpenInputLayout_hintTextAppearance, -1); + if (hintAppearance != -1) { + setHintTextAppearance(a.getResourceId(R.styleable.OpenInputLayout_hintTextAppearance, 0)); + } + + mErrorTextAppearance = a.getResourceId(R.styleable.OpenInputLayout_errorTextAppearance, 0); + final boolean errorEnabled = a.getBoolean(R.styleable.OpenInputLayout_errorEnabled, false); + + final boolean counterEnabled = a.getBoolean(R.styleable.OpenInputLayout_counterEnabled, false); + setCounterMaxLength(a.getInt(R.styleable.OpenInputLayout_counterMaxLength, INVALID_MAX_LENGTH)); + mCounterTextAppearance = a.getResourceId(R.styleable.OpenInputLayout_counterTextAppearance, 0); + mCounterOverflowTextAppearance = a.getResourceId(R.styleable.OpenInputLayout_counterOverflowTextAppearance, 0); + mPasswordToggleEnabled = a.getBoolean(R.styleable.OpenInputLayout_passwordToggleEnabled, false); + mPasswordToggleDrawable = a.getDrawable(R.styleable.OpenInputLayout_passwordToggleDrawable); +// mPasswordToggleContentDesc = a.getText(R.styleable.OpenInputLayout_android_passwordToggleContentDescription); + if (a.hasValue(R.styleable.OpenInputLayout_moveLabelUp)) { + mMoveLabelUp = a.getBoolean(R.styleable.OpenInputLayout_moveLabelUp, true); + } + if (a.hasValue(R.styleable.OpenInputLayout_passwordToggleTint)) { + mHasPasswordToggleTintList = true; + mPasswordToggleTintList = a.getColorStateList(R.styleable.OpenInputLayout_passwordToggleTint); + } + + mLabelSpacingExtra = a.getDimension(R.styleable.OpenInputLayout_labelSpacingExtra, 0f); +// if (a.hasValue(R.styleable.OpenInputLayout_passwordToggleTintMode)) { +// mHasPasswordToggleTintMode = true; +// mPasswordToggleTintMode = ViewUtils.parseTintMode( +// a.getInt(android.support.design.R.styleable.OpenInputLayout_passwordToggleTintMode, -1), null); +// } + + a.recycle(); + + setErrorEnabled(errorEnabled); + setCounterEnabled(counterEnabled); + applyPasswordToggleTint(); + + if (ViewCompat.getImportantForAccessibility(this) + == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + // Make sure we're important for accessibility if we haven't been explicitly not + ViewCompat.setImportantForAccessibility(this, + ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + + //TODO: TEST ACCESSIBILITY +// ViewCompat.setAccessibilityDelegate(this, new TextInputLayout.TextInputAccessibilityDelegate()); + } + + @Override + public void addView(View child, int index, final ViewGroup.LayoutParams params) { + if (child instanceof EditText) { + // Make sure that the EditText is vertically at the bottom, so that it sits on the + // EditText's underline + FrameLayout.LayoutParams flp = new FrameLayout.LayoutParams(params); + flp.gravity = Gravity.CENTER_VERTICAL | (flp.gravity & ~Gravity.VERTICAL_GRAVITY_MASK); + mInputFrame.addView(child, flp); + + // Now use the EditText's LayoutParams as our own and update them to make enough space + // for the label + mInputFrame.setLayoutParams(params); + updateInputLayoutMargins(); + + setEditText((EditText) child); + } else { + // Carry on adding the View... + super.addView(child, index, params); + } + } + + /** + * Set the typeface to use for the hint and any label views (such as counter and error views). + * + * @param typeface typeface to use, or {@code null} to use the default. + */ + public void setTypeface(@Nullable Typeface typeface) { + if ((mTypeface != null && !mTypeface.equals(typeface)) + || (mTypeface == null && typeface != null)) { + mTypeface = typeface; + + mCollapsingTextHelper.setTypefaces(typeface); + if (mCounterView != null) { + mCounterView.setTypeface(typeface); + } + if (mErrorView != null) { + mErrorView.setTypeface(typeface); + } + } + } + + /** + * Returns the typeface used for the hint and any label views (such as counter and error views). + */ + @NonNull + public Typeface getTypeface() { + return mTypeface; + } + + @Override + public void dispatchProvideAutofillStructure(ViewStructure structure, int flags) { + if (mOriginalHint == null || mEditText == null) { + super.dispatchProvideAutofillStructure(structure, flags); + return; + } + + // Temporarily sets child's hint to its original value so it is properly set in the + // child's ViewStructure. + final CharSequence hint = mEditText.getHint(); + mEditText.setHint(mOriginalHint); + try { + super.dispatchProvideAutofillStructure(structure, flags); + } finally { + mEditText.setHint(hint); + } + } + + 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"); + } + + if (!(editText instanceof TextInputEditText)) { + Log.i(LOG_TAG, "EditText added is not a TextInputEditText. Please switch to using that" + + " class instead."); + } + + mEditText = editText; + + final boolean hasPasswordTransformation = hasPasswordTransformation(); + + // Use the EditText's typeface, and it's text size for our expanded text + if (!hasPasswordTransformation) { + // We don't want a monospace font just because we have a password field + mCollapsingTextHelper.setTypefaces(mEditText.getTypeface()); + } + mCollapsingTextHelper.setExpandedTextSize(mEditText.getTextSize()); + + final int editTextGravity = mEditText.getGravity(); + mCollapsingTextHelper.setCollapsedTextGravity( + Gravity.TOP | (editTextGravity & ~Gravity.VERTICAL_GRAVITY_MASK)); + mCollapsingTextHelper.setExpandedTextGravity(editTextGravity); + + // Add a TextWatcher so that we know when the text input has changed + mEditText.addTextChangedListener(new TextWatcher() { + @Override + public void afterTextChanged(Editable s) { + updateLabelState(!mRestoringSavedState); + if (mCounterEnabled) { + updateCounter(s.length()); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + }); + + // Use the EditText's hint colors if we don't have one set + if (mDefaultTextColor == null) { + mDefaultTextColor = mEditText.getHintTextColors(); + } + + // If we do not have a valid hint, try and retrieve it from the EditText, if enabled + if (mHintEnabled && TextUtils.isEmpty(mHint)) { + // Save the hint so it can be restored on dispatchProvideAutofillStructure(); + mOriginalHint = mEditText.getHint(); + setHint(mOriginalHint); + // Clear the EditText's hint as we will display it ourselves + mEditText.setHint(null); + } + + if (mCounterView != null) { + updateCounter(mEditText.getText().length()); + } + + if (mIndicatorArea != null) { + adjustIndicatorPadding(); + } + + updatePasswordToggleView(); + + // Update the label visibility with no animation, but force a state change + updateLabelState(false, true); + } + + private void updateInputLayoutMargins() { + // Create/update the LayoutParams so that we can add enough top margin + // to the EditText so make room for the label + final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mInputFrame.getLayoutParams(); + final int newMargin; + + if (mHintEnabled) { + if (mTmpPaint == null) { + mTmpPaint = new Paint(); + } + mTmpPaint.setTypeface(mCollapsingTextHelper.getCollapsedTypeface()); + mTmpPaint.setTextSize(mCollapsingTextHelper.getCollapsedTextSize()); + newMargin = mMoveLabelUp ? (int) -mTmpPaint.ascent() : (int) (mTmpPaint.descent() + (-mTmpPaint.ascent())); + } else { + newMargin = 0; + } + + if (mMoveLabelUp) { + if (newMargin != lp.topMargin) { + lp.topMargin = newMargin + (int) mLabelSpacingExtra; + mInputFrame.requestLayout(); + } + } else { + if (newMargin != lp.bottomMargin) { + lp.bottomMargin = newMargin + (int) mLabelSpacingExtra; + mInputFrame.requestLayout(); + } + } + } + + void updateLabelState(boolean animate) { + updateLabelState(animate, false); + } + + void updateLabelState(final boolean animate, final boolean force) { + final boolean isEnabled = isEnabled(); + final boolean hasText = mEditText != null && !TextUtils.isEmpty(mEditText.getText()); + final boolean isFocused = arrayContains(getDrawableState(), android.R.attr.state_focused); + final boolean isErrorShowing = !TextUtils.isEmpty(getError()); + + if (mDefaultTextColor != null) { + mCollapsingTextHelper.setExpandedTextColor(mDefaultTextColor); + } + + if (isEnabled && mCounterOverflowed && mCounterView != null) { + mCollapsingTextHelper.setCollapsedTextColor(mCounterView.getTextColors()); + } else if (isEnabled && isFocused && mFocusedTextColor != null) { + mCollapsingTextHelper.setCollapsedTextColor(mFocusedTextColor); + } else if (mDefaultTextColor != null) { + mCollapsingTextHelper.setCollapsedTextColor(mDefaultTextColor); + } + + if (hasText || (isEnabled() && (isFocused || isErrorShowing))) { + // We should be showing the label so do so if it isn't already + if (force || mHintExpanded) { + collapseHint(animate); + } + } else { + // We should not be showing the label so hide it + if (force || !mHintExpanded) { + expandHint(animate); + } + } + } + + /** + * Returns the {@link android.widget.EditText} used for text input. + */ + @Nullable + public EditText getEditText() { + return mEditText; + } + + /** + * Set the hint to be displayed in the floating label, if enabled. + * + * @attr ref android.support.design.R.styleable#OpenInputLayout_android_hint + * @see #setHintEnabled(boolean) + */ + public void setHint(@Nullable CharSequence hint) { + if (mHintEnabled) { + setHintInternal(hint); + sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + } + } + + private void setHintInternal(CharSequence hint) { + mHint = hint; + mCollapsingTextHelper.setText(hint); + } + + /** + * Returns the hint which is displayed in the floating label, if enabled. + * + * @return the hint, or null if there isn't one set, or the hint is not enabled. + * @attr ref android.support.design.R.styleable#OpenInputLayout_android_hint + */ + @Override + @Nullable + public CharSequence getHint() { + return mHintEnabled ? mHint : null; + } + + /** + * Sets whether the floating label functionality is enabled or not in this layout. + *
+ *
If enabled, any non-empty hint in the child EditText will be moved into the floating + * hint, and its existing hint will be cleared. If disabled, then any non-empty floating hint + * in this layout will be moved into the EditText, and this layout's hint will be cleared.
+ * + * @attr ref android.support.design.R.styleable#OpenInputLayout_hintEnabled + * @see #setHint(CharSequence) + * @see #isHintEnabled() + */ + public void setHintEnabled(boolean enabled) { + if (enabled != mHintEnabled) { + mHintEnabled = enabled; + + final CharSequence editTextHint = mEditText.getHint(); + if (!mHintEnabled) { + if (!TextUtils.isEmpty(mHint) && TextUtils.isEmpty(editTextHint)) { + // If the hint is disabled, but we have a hint set, and the EditText doesn't, + // pass it through... + mEditText.setHint(mHint); + } + // Now clear out any set hint + setHintInternal(null); + } else { + if (!TextUtils.isEmpty(editTextHint)) { + // If the hint is now enabled and the EditText has one set, we'll use it if + // we don't already have one, and clear the EditText's + if (TextUtils.isEmpty(mHint)) { + setHint(editTextHint); + } + mEditText.setHint(null); + } + } + + // Now update the EditText top margin + if (mEditText != null) { + updateInputLayoutMargins(); + } + } + } + + /** + * Returns whether the floating label functionality is enabled or not in this layout. + * + * @attr ref android.support.design.R.styleable#OpenInputLayout_hintEnabled + * @see #setHintEnabled(boolean) + */ + public boolean isHintEnabled() { + return mHintEnabled; + } + + /** + * Sets the hint text color, size, style from the specified TextAppearance resource. + * + * @attr ref android.support.design.R.styleable#OpenInputLayout_hintTextAppearance + */ + public void setHintTextAppearance(@StyleRes int resId) { + mCollapsingTextHelper.setCollapsedTextAppearance(resId); + mFocusedTextColor = mCollapsingTextHelper.getCollapsedTextColor(); + + if (mEditText != null) { + updateLabelState(false); + // Text size might have changed so update the top margin + updateInputLayoutMargins(); + } + } + + private void addIndicator(TextView indicator, int index) { + if (mIndicatorArea == null) { + mIndicatorArea = new LinearLayout(getContext()); + mIndicatorArea.setOrientation(LinearLayout.HORIZONTAL); + addView(mIndicatorArea, LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + + // Add a flexible spacer in the middle so that the left/right views stay pinned + final Space spacer = new Space(getContext()); + final LinearLayout.LayoutParams spacerLp = new LinearLayout.LayoutParams(0, 0, 1f); + mIndicatorArea.addView(spacer, spacerLp); + + if (mEditText != null) { + adjustIndicatorPadding(); + } + } + mIndicatorArea.setVisibility(View.VISIBLE); + mIndicatorArea.addView(indicator, index); + mIndicatorsAdded++; + } + + private void adjustIndicatorPadding() { + // Add padding to the error and character counter so that they match the EditText + ViewCompat.setPaddingRelative(mIndicatorArea, ViewCompat.getPaddingStart(mEditText), + 0, ViewCompat.getPaddingEnd(mEditText), mEditText.getPaddingBottom()); + } + + private void removeIndicator(TextView indicator) { + if (mIndicatorArea != null) { + mIndicatorArea.removeView(indicator); + if (--mIndicatorsAdded == 0) { + mIndicatorArea.setVisibility(View.GONE); + } + } + } + + /** + * Whether the error functionality is enabled or not in this layout. Enabling this + * functionality before setting an error message via {@link #setError(CharSequence)}, will mean + * that this layout will not change size when an error is displayed. + * + * @attr ref android.support.design.R.styleable#OpenInputLayout_errorEnabled + */ + public void setErrorEnabled(boolean enabled) { + if (mErrorEnabled != enabled) { + if (mErrorView != null) { + mErrorView.animate().cancel(); + } + + if (enabled) { + mErrorView = new AppCompatTextView(getContext()); + mErrorView.setId(android.support.design.R.id.textinput_error); + if (mTypeface != null) { + mErrorView.setTypeface(mTypeface); + } + boolean useDefaultColor = false; + try { + TextViewCompat.setTextAppearance(mErrorView, mErrorTextAppearance); + + if (Build.VERSION.SDK_INT >= 23 + && mErrorView.getTextColors().getDefaultColor() == Color.MAGENTA) { + // Caused by our theme not extending from Theme.Design*. On API 23 and + // above, unresolved theme attrs result in MAGENTA rather than an exception. + // Flag so that we use a decent default + useDefaultColor = true; + } + } catch (Exception e) { + // Caused by our theme not extending from Theme.Design*. Flag so that we use + // a decent default + useDefaultColor = true; + } + if (useDefaultColor) { + // Probably caused by our theme not extending from Theme.Design*. Instead + // we manually set something appropriate + TextViewCompat.setTextAppearance(mErrorView, + android.support.v7.appcompat.R.style.TextAppearance_AppCompat_Caption); + mErrorView.setTextColor(ContextCompat.getColor(getContext(), + android.support.v7.appcompat.R.color.error_color_material)); + } + mErrorView.setVisibility(INVISIBLE); + ViewCompat.setAccessibilityLiveRegion(mErrorView, + ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE); + addIndicator(mErrorView, 0); + } else { + mErrorShown = false; + updateEditTextBackground(); + removeIndicator(mErrorView); + mErrorView = null; + } + mErrorEnabled = enabled; + } + } + + /** + * Sets the text color and size for the error message from the specified + * TextAppearance resource. + * + * @attr ref android.support.design.R.styleable#OpenInputLayout_errorTextAppearance + */ + public void setErrorTextAppearance(@StyleRes int resId) { + mErrorTextAppearance = resId; + if (mErrorView != null) { + TextViewCompat.setTextAppearance(mErrorView, resId); + } + } + + /** + * Returns whether the error functionality is enabled or not in this layout. + * + * @attr ref android.support.design.R.styleable#OpenInputLayout_errorEnabled + * @see #setErrorEnabled(boolean) + */ + public boolean isErrorEnabled() { + return mErrorEnabled; + } + + /** + * Sets an error message that will be displayed below our {@link EditText}. If the + * {@code error} is {@code null}, the error message will be cleared. + *
+ * If the error functionality has not been enabled via {@link #setErrorEnabled(boolean)}, then
+ * it will be automatically enabled if {@code error} is not empty.
+ *
+ * @param error Error message to display, or null to clear
+ * @see #getError()
+ */
+ public void setError(@Nullable final CharSequence error) {
+ // Only animate if we're enabled, laid out, and we have a different error message
+ setError(error, ViewCompat.isLaidOut(this) && isEnabled()
+ && (mErrorView == null || !TextUtils.equals(mErrorView.getText(), error)));
+ }
+
+ private void setError(@Nullable final CharSequence error, final boolean animate) {
+ mError = error;
+
+ if (!mErrorEnabled) {
+ if (TextUtils.isEmpty(error)) {
+ // If error isn't enabled, and the error is empty, just return
+ return;
+ }
+ // Else, we'll assume that they want to enable the error functionality
+ setErrorEnabled(true);
+ }
+
+ mErrorShown = !TextUtils.isEmpty(error);
+
+ // Cancel any on-going animation
+ mErrorView.animate().cancel();
+
+ if (mErrorShown) {
+ mErrorView.setText(error);
+ mErrorView.setVisibility(VISIBLE);
+
+ if (animate) {
+ if (mErrorView.getAlpha() == 1f) {
+ // If it's currently 100% show, we'll animate it from 0
+ mErrorView.setAlpha(0f);
+ }
+ mErrorView.animate()
+ .alpha(1f)
+ .setDuration(ANIMATION_DURATION)
+ .setInterpolator(OpenAnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR)
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ mErrorView.setVisibility(VISIBLE);
+ }
+ }).start();
+ } else {
+ // Set alpha to 1f, just in case
+ mErrorView.setAlpha(1f);
+ }
+ } else {
+ if (mErrorView.getVisibility() == VISIBLE) {
+ if (animate) {
+ mErrorView.animate()
+ .alpha(0f)
+ .setDuration(ANIMATION_DURATION)
+ .setInterpolator(OpenAnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR)
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ mErrorView.setText(error);
+ mErrorView.setVisibility(INVISIBLE);
+ }
+ }).start();
+ } else {
+ mErrorView.setText(error);
+ mErrorView.setVisibility(INVISIBLE);
+ }
+ }
+ }
+
+ updateEditTextBackground();
+ updateLabelState(animate);
+ }
+
+ /**
+ * Whether the character counter functionality is enabled or not in this layout.
+ *
+ * @attr ref android.support.design.R.styleable#OpenInputLayout_counterEnabled
+ */
+ public void setCounterEnabled(boolean enabled) {
+ if (mCounterEnabled != enabled) {
+ if (enabled) {
+ mCounterView = new AppCompatTextView(getContext());
+ mCounterView.setId(android.support.design.R.id.textinput_counter);
+ if (mTypeface != null) {
+ mCounterView.setTypeface(mTypeface);
+ }
+ mCounterView.setMaxLines(1);
+ try {
+ TextViewCompat.setTextAppearance(mCounterView, mCounterTextAppearance);
+ } catch (Exception e) {
+ // Probably caused by our theme not extending from Theme.Design*. Instead
+ // we manually set something appropriate
+ TextViewCompat.setTextAppearance(mCounterView,
+ android.support.v7.appcompat.R.style.TextAppearance_AppCompat_Caption);
+ mCounterView.setTextColor(ContextCompat.getColor(getContext(),
+ android.support.v7.appcompat.R.color.error_color_material));
+ }
+ addIndicator(mCounterView, -1);
+ if (mEditText == null) {
+ updateCounter(0);
+ } else {
+ updateCounter(mEditText.getText().length());
+ }
+ } else {
+ removeIndicator(mCounterView);
+ mCounterView = null;
+ }
+ mCounterEnabled = enabled;
+ }
+ }
+
+ /**
+ * Returns whether the character counter functionality is enabled or not in this layout.
+ *
+ * @attr ref android.support.design.R.styleable#OpenInputLayout_counterEnabled
+ * @see #setCounterEnabled(boolean)
+ */
+ public boolean isCounterEnabled() {
+ return mCounterEnabled;
+ }
+
+ /**
+ * Sets the max length to display at the character counter.
+ *
+ * @param maxLength maxLength to display. Any value less than or equal to 0 will not be shown.
+ * @attr ref android.support.design.R.styleable#OpenInputLayout_counterMaxLength
+ */
+ public void setCounterMaxLength(int maxLength) {
+ if (mCounterMaxLength != maxLength) {
+ if (maxLength > 0) {
+ mCounterMaxLength = maxLength;
+ } else {
+ mCounterMaxLength = INVALID_MAX_LENGTH;
+ }
+ if (mCounterEnabled) {
+ updateCounter(mEditText == null ? 0 : mEditText.getText().length());
+ }
+ }
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ // Since we're set to addStatesFromChildren, we need to make sure that we set all
+ // children to enabled/disabled otherwise any enabled children will wipe out our disabled
+ // drawable state
+ recursiveSetEnabled(this, enabled);
+ super.setEnabled(enabled);
+ }
+
+ private static void recursiveSetEnabled(final ViewGroup vg, final boolean enabled) {
+ for (int i = 0, count = vg.getChildCount(); i < count; i++) {
+ final View child = vg.getChildAt(i);
+ child.setEnabled(enabled);
+ if (child instanceof ViewGroup) {
+ recursiveSetEnabled((ViewGroup) child, enabled);
+ }
+ }
+ }
+
+ /**
+ * Returns the max length shown at the character counter.
+ *
+ * @attr ref android.support.design.R.styleable#OpenInputLayout_counterMaxLength
+ */
+ public int getCounterMaxLength() {
+ return mCounterMaxLength;
+ }
+
+ void updateCounter(int length) {
+ boolean wasCounterOverflowed = mCounterOverflowed;
+ if (mCounterMaxLength == INVALID_MAX_LENGTH) {
+ mCounterView.setText(String.valueOf(length));
+ mCounterOverflowed = false;
+ } else {
+ mCounterOverflowed = length > mCounterMaxLength;
+ if (wasCounterOverflowed != mCounterOverflowed) {
+ TextViewCompat.setTextAppearance(mCounterView, mCounterOverflowed
+ ? mCounterOverflowTextAppearance : mCounterTextAppearance);
+ }
+ mCounterView.setText(getContext().getString(android.support.design.R.string.character_counter_pattern,
+ length, mCounterMaxLength));
+ }
+ if (mEditText != null && wasCounterOverflowed != mCounterOverflowed) {
+ updateLabelState(false);
+ updateEditTextBackground();
+ }
+ }
+
+ @SuppressLint("RestrictedApi")
+ private void updateEditTextBackground() {
+ if (mEditText == null) {
+ return;
+ }
+
+ Drawable editTextBackground = mEditText.getBackground();
+ if (editTextBackground == null) {
+ return;
+ }
+
+ ensureBackgroundDrawableStateWorkaround();
+
+// if (android.support.v7.widget.DrawableUtils.canSafelyMutateDrawable(editTextBackground)) {
+ editTextBackground = editTextBackground.mutate();
+// }
+
+ if (mErrorShown && mErrorView != null) {
+ // Set a color filter of the error color
+ editTextBackground.setColorFilter(
+ AppCompatDrawableManager.getPorterDuffColorFilter(
+ mErrorView.getCurrentTextColor(), PorterDuff.Mode.SRC_IN));
+ } else if (mCounterOverflowed && mCounterView != null) {
+ // Set a color filter of the counter color
+ editTextBackground.setColorFilter(
+ AppCompatDrawableManager.getPorterDuffColorFilter(
+ mCounterView.getCurrentTextColor(), PorterDuff.Mode.SRC_IN));
+ } else {
+ // Else reset the color filter and refresh the drawable state so that the
+ // normal tint is used
+ DrawableCompat.clearColorFilter(editTextBackground);
+ mEditText.refreshDrawableState();
+ }
+ }
+
+ private void ensureBackgroundDrawableStateWorkaround() {
+ final int sdk = Build.VERSION.SDK_INT;
+ if (sdk != 21 && sdk != 22) {
+ // The workaround is only required on API 21-22
+ return;
+ }
+ final Drawable bg = mEditText.getBackground();
+ if (bg == null) {
+ return;
+ }
+
+ if (!mHasReconstructedEditTextBackground) {
+ // This is gross. There is an issue in the platform which affects container Drawables
+ // where the first drawable retrieved from resources will propagate any changes
+ // (like color filter) to all instances from the cache. We'll try to workaround it...
+
+ final Drawable newBg = bg.getConstantState().newDrawable();
+
+ if (bg instanceof DrawableContainer) {
+ // If we have a Drawable container, we can try and set it's constant state via
+ // reflection from the new Drawable
+ mHasReconstructedEditTextBackground =
+ OpenDrawableUtils.setContainerConstantState(
+ (DrawableContainer) bg, newBg.getConstantState());
+ }
+
+ if (!mHasReconstructedEditTextBackground) {
+ // If we reach here then we just need to set a brand new instance of the Drawable
+ // as the background. This has the unfortunate side-effect of wiping out any
+ // user set padding, but I'd hope that use of custom padding on an EditText
+ // is limited.
+ ViewCompat.setBackground(mEditText, newBg);
+ mHasReconstructedEditTextBackground = true;
+ }
+ }
+ }
+
+ static class SavedState extends AbsSavedState {
+ CharSequence error;
+ boolean isPasswordToggledVisible;
+
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ SavedState(Parcel source, ClassLoader loader) {
+ super(source, loader);
+ error = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ isPasswordToggledVisible = (source.readInt() == 1);
+
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ TextUtils.writeToParcel(error, dest, flags);
+ dest.writeInt(isPasswordToggledVisible ? 1 : 0);
+ }
+
+ @Override
+ public String toString() {
+ return "TextInputLayout.SavedState{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " error=" + error + "}";
+ }
+
+ public static final Creator
+ * If you use an icon you should also set a description for its action
+ * using {@link #setPasswordVisibilityToggleContentDescription(CharSequence)}.
+ * This is used for accessibility.
+ * If you use an icon you should also set a description for its action
+ * using {@link #setPasswordVisibilityToggleContentDescription(CharSequence)}.
+ * This is used for accessibility.
+ * The content description will be read via screen readers or other accessibility
+ * systems to explain the action of the password visibility toggle.
+ * The content description will be read via screen readers or other accessibility
+ * systems to explain the action of the password visibility toggle.
+ * This will be used to describe the navigation action to users through mechanisms
+ * such as screen readers.
+ * When enabled, a button is placed at the end of the EditText which enables the user
+ * to switch between the field's input being visibly disguised or not.
+ * Subsequent calls to {@link #setPasswordVisibilityToggleDrawable(Drawable)} will
+ * automatically mutate the drawable and apply the specified tint and tint mode using
+ * {@link DrawableCompat#setTintList(Drawable, ColorStateList)}.null
if no error was set
+ * or if error displaying is not enabled.
+ *
+ * @see #setError(CharSequence)
+ */
+ @Nullable
+ public CharSequence getError() {
+ return mErrorEnabled ? mError : null;
+ }
+
+ /**
+ * Returns whether any hint state changes, due to being focused or non-empty text, are
+ * animated.
+ *
+ * @attr ref android.support.design.R.styleable#OpenInputLayout_hintAnimationEnabled
+ * @see #setHintAnimationEnabled(boolean)
+ */
+ public boolean isHintAnimationEnabled() {
+ return mHintAnimationEnabled;
+ }
+
+ /**
+ * Set whether any hint state changes, due to being focused or non-empty text, are
+ * animated.
+ *
+ * @attr ref android.support.design.R.styleable#OpenInputLayout_hintAnimationEnabled
+ * @see #isHintAnimationEnabled()
+ */
+ public void setHintAnimationEnabled(boolean enabled) {
+ mHintAnimationEnabled = enabled;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+
+ if (mHintEnabled) {
+ mCollapsingTextHelper.draw(canvas);
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ updatePasswordToggleView();
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ private void updatePasswordToggleView() {
+ if (mEditText == null) {
+ // If there is no EditText, there is nothing to update
+ return;
+ }
+
+ if (shouldShowPasswordIcon()) {
+ if (mPasswordToggleView == null) {
+ mPasswordToggleView = (OpenCheckableImageButton) LayoutInflater.from(getContext())
+ .inflate(android.support.design.R.layout.design_text_input_password_icon, mInputFrame, false);
+ mPasswordToggleView.setImageDrawable(mPasswordToggleDrawable);
+ mPasswordToggleView.setContentDescription(mPasswordToggleContentDesc);
+ mInputFrame.addView(mPasswordToggleView);
+
+ mPasswordToggleView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ passwordVisibilityToggleRequested(false);
+ }
+ });
+ }
+
+ if (mEditText != null && ViewCompat.getMinimumHeight(mEditText) <= 0) {
+ // We should make sure that the EditText has the same min-height as the password
+ // toggle view. This ensure focus works properly, and there is no visual jump
+ // if the password toggle is enabled/disabled.
+ mEditText.setMinimumHeight(ViewCompat.getMinimumHeight(mPasswordToggleView));
+ }
+
+ mPasswordToggleView.setVisibility(VISIBLE);
+ mPasswordToggleView.setChecked(mPasswordToggledVisible);
+
+ // We need to add a dummy drawable as the end compound drawable so that the text is
+ // indented and doesn't display below the toggle view
+ if (mPasswordToggleDummyDrawable == null) {
+ mPasswordToggleDummyDrawable = new ColorDrawable();
+ }
+ mPasswordToggleDummyDrawable.setBounds(0, 0, mPasswordToggleView.getMeasuredWidth(), 1);
+
+ final Drawable[] compounds = TextViewCompat.getCompoundDrawablesRelative(mEditText);
+ // Store the user defined end compound drawable so that we can restore it later
+ if (compounds[2] != mPasswordToggleDummyDrawable) {
+ mOriginalEditTextEndDrawable = compounds[2];
+ }
+ TextViewCompat.setCompoundDrawablesRelative(mEditText, compounds[0], compounds[1],
+ mPasswordToggleDummyDrawable, compounds[3]);
+
+ // Copy over the EditText's padding so that we match
+ mPasswordToggleView.setPadding(mEditText.getPaddingLeft(),
+ mEditText.getPaddingTop(), mEditText.getPaddingRight(),
+ mEditText.getPaddingBottom());
+ } else {
+ if (mPasswordToggleView != null && mPasswordToggleView.getVisibility() == VISIBLE) {
+ mPasswordToggleView.setVisibility(View.GONE);
+ }
+
+ if (mPasswordToggleDummyDrawable != null) {
+ // Make sure that we remove the dummy end compound drawable if it exists, and then
+ // clear it
+ final Drawable[] compounds = TextViewCompat.getCompoundDrawablesRelative(mEditText);
+ if (compounds[2] == mPasswordToggleDummyDrawable) {
+ TextViewCompat.setCompoundDrawablesRelative(mEditText, compounds[0],
+ compounds[1], mOriginalEditTextEndDrawable, compounds[3]);
+ mPasswordToggleDummyDrawable = null;
+ }
+ }
+ }
+ }
+
+ /**
+ * Set the icon to use for the password visibility toggle button.
+ *