Merge \"Add disabled support to TextInputLayout\" into nyc-support-24.1-dev

am: 93bfbc677a

Change-Id: Ie50888c295cfb7ae9f53f4b8371758862302eefc

GitOrigin-RevId=19b449e5b243acede2f373cb88b50974b9ea77e8

PiperOrigin-RevId: 140563834
This commit is contained in:
Chris Banes 2016-11-30 05:01:21 +00:00 committed by Travis Collins
parent f6abdecacf
commit fc28a7a905
7 changed files with 276 additions and 55 deletions

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false" android:color="?android:attr/textColorTertiary"/>
<item android:color="?attr/textColorError"/>
</selector>

View File

@ -77,13 +77,13 @@
</style>
<style name="TextAppearance.Design.Error" parent="TextAppearance.AppCompat.Caption">
<item name="android:textColor">?attr/textColorError</item>
<item name="android:textColor">@color/design_error</item>
</style>
<style name="TextAppearance.Design.Counter" parent="TextAppearance.AppCompat.Caption"/>
<style name="TextAppearance.Design.Counter.Overflow" parent="TextAppearance.AppCompat.Caption">
<item name="android:textColor">?attr/textColorError</item>
<item name="android:textColor">@color/design_error</item>
</style>
<style name="TextAppearance.Design.Snackbar.Message" parent="android:TextAppearance">

View File

@ -16,6 +16,7 @@
package android.support.design.widget;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
@ -25,10 +26,12 @@ import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.os.Build;
import android.support.annotation.ColorInt;
import android.support.design.R;
import android.support.v4.text.TextDirectionHeuristicsCompat;
import android.support.v4.view.GravityCompat;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.TintTypedArray;
import android.text.TextPaint;
import android.text.TextUtils;
import android.view.Gravity;
@ -63,8 +66,8 @@ final class CollapsingTextHelper {
private int mCollapsedTextGravity = Gravity.CENTER_VERTICAL;
private float mExpandedTextSize = 15;
private float mCollapsedTextSize = 15;
private int mExpandedTextColor;
private int mCollapsedTextColor;
private ColorStateList mExpandedTextColor;
private ColorStateList mCollapsedTextColor;
private float mExpandedDrawY;
private float mCollapsedDrawY;
@ -89,6 +92,8 @@ final class CollapsingTextHelper {
private float mScale;
private float mCurrentTextSize;
private int[] mState;
private boolean mBoundsChanged;
private final TextPaint mTextPaint;
@ -136,14 +141,14 @@ final class CollapsingTextHelper {
}
}
void setCollapsedTextColor(int textColor) {
void setCollapsedTextColor(ColorStateList textColor) {
if (mCollapsedTextColor != textColor) {
mCollapsedTextColor = textColor;
recalculate();
}
}
void setExpandedTextColor(int textColor) {
void setExpandedTextColor(ColorStateList textColor) {
if (mExpandedTextColor != textColor) {
mExpandedTextColor = textColor;
recalculate();
@ -194,12 +199,11 @@ final class CollapsingTextHelper {
}
void setCollapsedTextAppearance(int resId) {
TypedArray a = mView.getContext().obtainStyledAttributes(resId,
TintTypedArray a = TintTypedArray.obtainStyledAttributes(mView.getContext(), resId,
android.support.v7.appcompat.R.styleable.TextAppearance);
if (a.hasValue(android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor)) {
mCollapsedTextColor = a.getColor(
android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor,
mCollapsedTextColor);
mCollapsedTextColor = a.getColorStateList(
android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor);
}
if (a.hasValue(android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize)) {
mCollapsedTextSize = a.getDimensionPixelSize(
@ -224,12 +228,11 @@ final class CollapsingTextHelper {
}
void setExpandedTextAppearance(int resId) {
TypedArray a = mView.getContext().obtainStyledAttributes(resId,
TintTypedArray a = TintTypedArray.obtainStyledAttributes(mView.getContext(), resId,
android.support.v7.appcompat.R.styleable.TextAppearance);
if (a.hasValue(android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor)) {
mExpandedTextColor = a.getColor(
android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor,
mExpandedTextColor);
mExpandedTextColor = a.getColorStateList(
android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor);
}
if (a.hasValue(android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize)) {
mExpandedTextSize = a.getDimensionPixelSize(
@ -310,6 +313,22 @@ final class CollapsingTextHelper {
}
}
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;
}
@ -339,9 +358,10 @@ final class CollapsingTextHelper {
if (mCollapsedTextColor != mExpandedTextColor) {
// If the collapsed and expanded text colors are different, blend them based on the
// fraction
mTextPaint.setColor(blendColors(mExpandedTextColor, mCollapsedTextColor, fraction));
mTextPaint.setColor(blendColors(
getCurrentExpandedTextColor(), getCurrentCollapsedTextColor(), fraction));
} else {
mTextPaint.setColor(mCollapsedTextColor);
mTextPaint.setColor(getCurrentCollapsedTextColor());
}
mTextPaint.setShadowLayer(
@ -353,6 +373,24 @@ final class CollapsingTextHelper {
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;
@ -644,11 +682,11 @@ final class CollapsingTextHelper {
return Math.abs(value - targetValue) < 0.001f;
}
int getExpandedTextColor() {
ColorStateList getExpandedTextColor() {
return mExpandedTextColor;
}
int getCollapsedTextColor() {
ColorStateList getCollapsedTextColor() {
return mCollapsedTextColor;
}

View File

@ -17,6 +17,7 @@
package android.support.design.widget;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
@ -724,6 +725,9 @@ public class CollapsingToolbarLayout extends FrameLayout {
if (d != null && d.isStateful()) {
changed |= d.setState(state);
}
if (mCollapsingTextHelper != null) {
changed |= mCollapsingTextHelper.setState(state);
}
if (changed) {
invalidate();
@ -801,7 +805,16 @@ public class CollapsingToolbarLayout extends FrameLayout {
* @param color The new text color in ARGB format
*/
public void setCollapsedTitleTextColor(@ColorInt int color) {
mCollapsingTextHelper.setCollapsedTextColor(color);
setCollapsedTitleTextColor(ColorStateList.valueOf(color));
}
/**
* Sets the text colors of the collapsed title.
*
* @param colors ColorStateList containing the new text colors
*/
public void setCollapsedTitleTextColor(@NonNull ColorStateList colors) {
mCollapsingTextHelper.setCollapsedTextColor(colors);
}
/**
@ -840,7 +853,16 @@ public class CollapsingToolbarLayout extends FrameLayout {
* @param color The new text color in ARGB format
*/
public void setExpandedTitleColor(@ColorInt int color) {
mCollapsingTextHelper.setExpandedTextColor(color);
setExpandedTitleTextColor(ColorStateList.valueOf(color));
}
/**
* Sets the text colors of the expanded title.
*
* @param colors ColorStateList containing the new text colors
*/
public void setExpandedTitleTextColor(@NonNull ColorStateList colors) {
mCollapsingTextHelper.setExpandedTextColor(colors);
}
/**

View File

@ -34,6 +34,7 @@ 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.R;
import android.support.v4.content.ContextCompat;
import android.support.v4.graphics.drawable.DrawableCompat;
@ -139,12 +140,16 @@ public class TextInputLayout extends LinearLayout {
private ColorStateList mDefaultTextColor;
private ColorStateList mFocusedTextColor;
// Only used for testing
private boolean mHintExpanded;
private final CollapsingTextHelper mCollapsingTextHelper = new CollapsingTextHelper(this);
private boolean mHintAnimationEnabled;
private ValueAnimatorCompat mAnimator;
private boolean mHasReconstructedEditTextBackground;
private boolean mInDrawableStateChanged;
public TextInputLayout(Context context) {
this(context, null);
@ -165,12 +170,15 @@ public class TextInputLayout extends LinearLayout {
setAddStatesFromChildren(true);
mInputFrame = new FrameLayout(context);
mInputFrame.setAddStatesFromChildren(true);
addView(mInputFrame);
mCollapsingTextHelper.setTextSizeInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
mCollapsingTextHelper.setPositionInterpolator(new AccelerateInterpolator());
mCollapsingTextHelper.setCollapsedTextGravity(Gravity.TOP | GravityCompat.START);
mHintExpanded = mCollapsingTextHelper.getExpansionFraction() == 1f;
final TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,
R.styleable.TextInputLayout, defStyleAttr, R.style.Widget_Design_TextInputLayout);
mHintEnabled = a.getBoolean(R.styleable.TextInputLayout_hintEnabled, true);
@ -363,23 +371,24 @@ public class TextInputLayout extends LinearLayout {
}
private void updateLabelState(boolean animate) {
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.getDefaultColor());
mCollapsingTextHelper.setExpandedTextColor(mDefaultTextColor);
}
if (mCounterOverflowed && mCounterView != null) {
mCollapsingTextHelper.setCollapsedTextColor(mCounterView.getCurrentTextColor());
} else if (isFocused && mFocusedTextColor != null) {
mCollapsingTextHelper.setCollapsedTextColor(mFocusedTextColor.getDefaultColor());
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.getDefaultColor());
mCollapsingTextHelper.setCollapsedTextColor(mDefaultTextColor);
}
if (hasText || isFocused || isErrorShowing) {
if (hasText || (isEnabled() && (isFocused || isErrorShowing))) {
// We should be showing the label so do so if it isn't already
collapseHint(animate);
} else {
@ -488,7 +497,7 @@ public class TextInputLayout extends LinearLayout {
*/
public void setHintTextAppearance(@StyleRes int resId) {
mCollapsingTextHelper.setCollapsedTextAppearance(resId);
mFocusedTextColor = ColorStateList.valueOf(mCollapsingTextHelper.getCollapsedTextColor());
mFocusedTextColor = mCollapsingTextHelper.getCollapsedTextColor();
if (mEditText != null) {
updateLabelState(false);
@ -595,6 +604,12 @@ public class TextInputLayout extends LinearLayout {
* @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) {
@ -606,9 +621,6 @@ public class TextInputLayout extends LinearLayout {
setErrorEnabled(true);
}
// Only animate if we've been laid out already and we have a different error
final boolean animate = ViewCompat.isLaidOut(this)
&& !TextUtils.equals(mErrorView.getText(), error);
mErrorShown = !TextUtils.isEmpty(error);
// Cancel any on-going animation
@ -659,7 +671,7 @@ public class TextInputLayout extends LinearLayout {
}
updateEditTextBackground();
updateLabelState(true);
updateLabelState(animate);
}
/**
@ -727,6 +739,25 @@ public class TextInputLayout extends LinearLayout {
}
}
@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.
*
@ -757,13 +788,17 @@ public class TextInputLayout extends LinearLayout {
}
private void updateEditTextBackground() {
ensureBackgroundDrawableStateWorkaround();
if (mEditText == null) {
return;
}
Drawable editTextBackground = mEditText.getBackground();
if (editTextBackground == null) {
return;
}
ensureBackgroundDrawableStateWorkaround();
if (android.support.v7.widget.DrawableUtils.canSafelyMutateDrawable(editTextBackground)) {
editTextBackground = editTextBackground.mutate();
}
@ -1215,13 +1250,6 @@ public class TextInputLayout extends LinearLayout {
}
}
@Override
public void refreshDrawableState() {
super.refreshDrawableState();
// Drawable state has changed so see if we need to update the label
updateLabelState(ViewCompat.isLaidOut(this));
}
private void collapseHint(boolean animate) {
if (mAnimator != null && mAnimator.isRunning()) {
mAnimator.cancel();
@ -1231,6 +1259,39 @@ public class TextInputLayout extends LinearLayout {
} else {
mCollapsingTextHelper.setExpansionFraction(1f);
}
mHintExpanded = false;
}
@Override
protected void drawableStateChanged() {
if (mInDrawableStateChanged) {
// Some of the calls below will update the drawable state of child views. Since we're
// using addStatesFromChildren we can get into infinite recursion, hence we'll just
// exit in this instance
return;
}
mInDrawableStateChanged = true;
super.drawableStateChanged();
final int[] state = getDrawableState();
boolean changed = false;
// Drawable state has changed so see if we need to update the label
updateLabelState(ViewCompat.isLaidOut(this) && isEnabled());
updateEditTextBackground();
if (mCollapsingTextHelper != null) {
changed |= mCollapsingTextHelper.setState(state);
}
if (changed) {
invalidate();
}
mInDrawableStateChanged = false;
}
private void expandHint(boolean animate) {
@ -1242,6 +1303,7 @@ public class TextInputLayout extends LinearLayout {
} else {
mCollapsingTextHelper.setExpansionFraction(0f);
}
mHintExpanded = true;
}
private void animateToExpansionFraction(final float target) {
@ -1263,6 +1325,11 @@ public class TextInputLayout extends LinearLayout {
mAnimator.start();
}
@VisibleForTesting
final boolean isHintExpanded() {
return mHintExpanded;
}
private class TextInputAccessibilityDelegate extends AccessibilityDelegateCompat {
@Override
public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {

View File

@ -211,4 +211,27 @@ public class TestUtilsActions {
}
};
}
public static ViewAction setEnabled(final boolean enabled) {
return new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return isDisplayed();
}
@Override
public String getDescription() {
return "set enabled";
}
@Override
public void perform(UiController uiController, View view) {
uiController.loopMainThreadUntilIdle();
view.setEnabled(enabled);
uiController.loopMainThreadUntilIdle();
}
};
}
}

View File

@ -16,28 +16,35 @@
package android.support.design.widget;
import android.app.Activity;
import android.support.design.test.R;
import android.support.test.annotation.UiThreadTest;
import android.test.suitebuilder.annotation.SmallTest;
import android.widget.EditText;
import org.junit.Test;
import static android.support.design.testutils.TestUtilsActions.setText;
import static android.support.design.testutils.TestUtilsActions.setEnabled;
import static android.support.design.testutils.TextInputLayoutActions.setError;
import static android.support.design.testutils.TextInputLayoutActions.setErrorEnabled;
import static android.support.design.testutils.TextInputLayoutActions.setPasswordVisibilityToggleEnabled;
import static android.support.design.testutils.TextInputLayoutActions
.setPasswordVisibilityToggleEnabled;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.typeText;
import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.isEnabled;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import android.app.Activity;
import android.support.design.test.R;
import android.support.test.espresso.NoMatchingViewException;
import android.support.test.espresso.ViewAssertion;
import android.test.suitebuilder.annotation.SmallTest;
import android.view.View;
import android.widget.EditText;
import org.junit.Test;
@SmallTest
public class TextInputLayoutTest extends BaseInstrumentationTestCase<TextInputLayoutActivity> {
@ -51,6 +58,14 @@ public class TextInputLayoutTest extends BaseInstrumentationTestCase<TextInputLa
super(TextInputLayoutActivity.class);
}
@Test
public void testTypingTextCollapsesHint() {
// Type some text
onView(withId(R.id.textinput_edittext)).perform(typeText(INPUT_TEXT));
// ...and check that the hint has collapsed
onView(withId(R.id.textinput)).check(isHintExpanded(false));
}
@Test
public void testSetErrorEnablesErrorIsDisplayed() {
onView(withId(R.id.textinput)).perform(setError(ERROR_MESSAGE_1));
@ -83,8 +98,8 @@ public class TextInputLayoutTest extends BaseInstrumentationTestCase<TextInputLa
@Test
public void testPasswordToggleClick() {
// Set some text on the EditText
onView(withId(R.id.textinput_edittext_pwd)).perform(setText(INPUT_TEXT));
// Type some text on the EditText
onView(withId(R.id.textinput_edittext_pwd)).perform(typeText(INPUT_TEXT));
final Activity activity = mActivityTestRule.getActivity();
final EditText textInput = (EditText) activity.findViewById(R.id.textinput_edittext_pwd);
@ -105,7 +120,8 @@ public class TextInputLayoutTest extends BaseInstrumentationTestCase<TextInputLa
final EditText textInput = (EditText) activity.findViewById(R.id.textinput_edittext_pwd);
// Set some text on the EditText
onView(withId(R.id.textinput_edittext_pwd)).perform(setText(INPUT_TEXT));
onView(withId(R.id.textinput_edittext_pwd))
.perform(typeText(INPUT_TEXT));
// Assert that the password is disguised
assertNotEquals(INPUT_TEXT, textInput.getLayout().getText().toString());
@ -124,8 +140,8 @@ public class TextInputLayoutTest extends BaseInstrumentationTestCase<TextInputLa
final Activity activity = mActivityTestRule.getActivity();
final EditText textInput = (EditText) activity.findViewById(R.id.textinput_edittext_pwd);
// Set some text on the EditText
onView(withId(R.id.textinput_edittext_pwd)).perform(setText(INPUT_TEXT));
// Type some text on the EditText
onView(withId(R.id.textinput_edittext_pwd)).perform(typeText(INPUT_TEXT));
// Assert that the password is disguised
assertNotEquals(INPUT_TEXT, textInput.getLayout().getText().toString());
@ -138,4 +154,39 @@ public class TextInputLayoutTest extends BaseInstrumentationTestCase<TextInputLa
// Check that the password is disguised again
assertNotEquals(INPUT_TEXT, textInput.getLayout().getText().toString());
}
@Test
public void testSetEnabledFalse() {
// First click on the EditText, so that it is focused and the hint collapses...
onView(withId(R.id.textinput_edittext)).perform(click());
// Now disable the TextInputLayout and check that the hint expands
onView(withId(R.id.textinput))
.perform(setEnabled(false))
.check(isHintExpanded(true));
// Finally check that the EditText is no longer enabled
onView(withId(R.id.textinput_edittext)).check(matches(not(isEnabled())));
}
@Test
public void testSetEnabledFalseWithText() {
// First set some text, then disable the TextInputLayout
onView(withId(R.id.textinput_edittext))
.perform(typeText(INPUT_TEXT));
onView(withId(R.id.textinput)).perform(setEnabled(false));
// Now check that the EditText is no longer enabled
onView(withId(R.id.textinput_edittext)).check(matches(not(isEnabled())));
}
static ViewAssertion isHintExpanded(final boolean expanded) {
return new ViewAssertion() {
@Override
public void check(View view, NoMatchingViewException noViewFoundException) {
assertTrue(view instanceof TextInputLayout);
assertEquals(expanded, ((TextInputLayout) view).isHintExpanded());
}
};
}
}