Skip to content

Commit

Permalink
[TextInputLayout] Add line mask expansion animation to filled box und…
Browse files Browse the repository at this point in the history
…erline.

Add an expanding animation for the focused underline that's triggered when the edit text gains focus. The line mask animation is built to calculate its bounds based on the hint CollapsingTextHelper's expansion fraction, so it seamlessly follows that animation.

PiperOrigin-RevId: 424703815
  • Loading branch information
afohrman authored and pekingme committed Jan 28, 2022
1 parent 5386328 commit 2463946
Showing 1 changed file with 67 additions and 33 deletions.
100 changes: 67 additions & 33 deletions lib/java/com/google/android/material/textfield/TextInputLayout.java
Expand Up @@ -185,6 +185,7 @@ public class TextInputLayout extends LinearLayout {

/** Duration for the label's scale up and down animations. */
private static final int LABEL_SCALE_ANIMATION_DURATION = 167;

private static final long PLACEHOLDER_FADE_DURATION = 87;
private static final long PLACEHOLDER_START_DELAY = 67;

Expand Down Expand Up @@ -241,7 +242,8 @@ public class TextInputLayout extends LinearLayout {
private boolean isProvidingHint;

@Nullable private MaterialShapeDrawable boxBackground;
@Nullable private MaterialShapeDrawable boxUnderline;
@Nullable private MaterialShapeDrawable boxUnderlineDefault;
@Nullable private MaterialShapeDrawable boxUnderlineFocused;
@NonNull private ShapeAppearanceModel shapeAppearanceModel;
private boolean areCornerRadiiRtl;

Expand Down Expand Up @@ -731,7 +733,8 @@ public TextInputLayout(@NonNull Context context, @Nullable AttributeSet attrs, i
int endIconDrawableId = a.getResourceId(R.styleable.TextInputLayout_endIconDrawable, 0);
endIconDelegates.append(END_ICON_CUSTOM, new CustomEndIconDelegate(this, endIconDrawableId));
endIconDelegates.append(END_ICON_NONE, new NoEndIconDelegate(this));
endIconDelegates.append(END_ICON_PASSWORD_TOGGLE,
endIconDelegates.append(
END_ICON_PASSWORD_TOGGLE,
new PasswordToggleEndIconDelegate(
this,
endIconDrawableId == 0
Expand Down Expand Up @@ -957,19 +960,22 @@ private void assignBoxBackgroundByMode() {
switch (boxBackgroundMode) {
case BOX_BACKGROUND_FILLED:
boxBackground = new MaterialShapeDrawable(shapeAppearanceModel);
boxUnderline = new MaterialShapeDrawable();
boxUnderlineDefault = new MaterialShapeDrawable();
boxUnderlineFocused = new MaterialShapeDrawable();
break;
case BOX_BACKGROUND_OUTLINE:
if (hintEnabled && !(boxBackground instanceof CutoutDrawable)) {
boxBackground = new CutoutDrawable(shapeAppearanceModel);
} else {
boxBackground = new MaterialShapeDrawable(shapeAppearanceModel);
}
boxUnderline = null;
boxUnderlineDefault = null;
boxUnderlineFocused = null;
break;
case BOX_BACKGROUND_NONE:
boxBackground = null;
boxUnderline = null;
boxUnderlineDefault = null;
boxUnderlineFocused = null;
break;
default:
throw new IllegalArgumentException(
Expand Down Expand Up @@ -1036,7 +1042,7 @@ private void adjustFilledEditTextPaddingForLargeFont() {
/**
* Set the value to use for the EditText's collapsed top padding in box mode.
*
* <p> Customized boxCollapsedPaddingTop will be disabled if the font scale is larger than 1.3.
* <p>Customized boxCollapsedPaddingTop will be disabled if the font scale is larger than 1.3.
*
* @param boxCollapsedPaddingTop the value to use for the EditText's collapsed top padding
* @attr ref com.google.android.material.R.styleable#TextInputLayout_boxCollapsedPaddingTop
Expand Down Expand Up @@ -1628,8 +1634,8 @@ public EditText getEditText() {
}

/**
* Sets the minimum width in terms of ems of the text field. The layout will be at least
* {@code minEms} wide if its {@code layout_width} is set to {@code wrap_content}.
* Sets the minimum width in terms of ems of the text field. The layout will be at least {@code
* minEms} wide if its {@code layout_width} is set to {@code wrap_content}.
*
* @param minEms The minimum width in terms of ems to be set
* @attr ref com.google.android.material.R.styleable#TextInputLayout_android_minEms
Expand All @@ -1653,8 +1659,8 @@ public int getMinEms() {
}

/**
* Sets the maximum width in terms of ems of the text field. The layout will be at most
* {@code maxEms} wide if its {@code layout_width} is set to {@code wrap_content}.
* Sets the maximum width in terms of ems of the text field. The layout will be at most {@code
* maxEms} wide if its {@code layout_width} is set to {@code wrap_content}.
*
* @param maxEms The maximum width in terms of ems to be set
* @attr ref com.google.android.material.R.styleable#TextInputLayout_android_maxEms
Expand Down Expand Up @@ -2910,12 +2916,18 @@ private void applyBoxAttributes() {

private void applyBoxUnderlineAttributes() {
// Exit if the underline is not being drawn by TextInputLayout.
if (boxUnderline == null) {
if (boxUnderlineDefault == null || boxUnderlineFocused == null) {
return;
}

if (canDrawStroke()) {
boxUnderline.setFillColor(ColorStateList.valueOf(boxStrokeColor));
// If the edit text is focused, set boxUnderlineDefault to defaultStrokeColor to use it as the
// backdrop for the focused underline expansion.
boxUnderlineDefault.setFillColor(
editText.isFocused()
? ColorStateList.valueOf(defaultStrokeColor)
: ColorStateList.valueOf(boxStrokeColor));
boxUnderlineFocused.setFillColor(ColorStateList.valueOf(boxStrokeColor));
}
invalidate();
}
Expand Down Expand Up @@ -3179,8 +3191,7 @@ public void onRtlPropertiesChanged(int layoutDirection) {
shouldCornersBeRtl ? boxCornerRadiusTopLeft : boxCornerRadiusTopRight,
shouldCornersBeRtl ? boxCornerRadiusTopRight : boxCornerRadiusTopLeft,
shouldCornersBeRtl ? boxCornerRadiusBottomLeft : boxCornerRadiusBottomRight,
shouldCornersBeRtl ? boxCornerRadiusBottomRight : boxCornerRadiusBottomLeft
);
shouldCornersBeRtl ? boxCornerRadiusBottomRight : boxCornerRadiusBottomLeft);
}
}

Expand Down Expand Up @@ -4192,9 +4203,13 @@ protected void onLayout(boolean changed, int left, int top, int right, int botto
}

private void updateBoxUnderlineBounds(@NonNull Rect bounds) {
if (boxUnderline != null) {
if (boxUnderlineDefault != null) {
int top = bounds.bottom - boxStrokeWidthDefaultPx;
boxUnderlineDefault.setBounds(bounds.left, top, bounds.right, bounds.bottom);
}
if (boxUnderlineFocused != null) {
int top = bounds.bottom - boxStrokeWidthFocusedPx;
boxUnderline.setBounds(bounds.left, top, bounds.right, bounds.bottom);
boxUnderlineFocused.setBounds(bounds.left, top, bounds.right, bounds.bottom);
}
}

Expand All @@ -4218,11 +4233,27 @@ private void drawHint(@NonNull Canvas canvas) {
}

private void drawBoxUnderline(Canvas canvas) {
if (boxUnderline != null) {
// Draw using the current boxStrokeWidth.
Rect underlineBounds = boxUnderline.getBounds();
underlineBounds.top = underlineBounds.bottom - boxStrokeWidthPx;
boxUnderline.draw(canvas);
if (boxUnderlineFocused != null && boxUnderlineDefault != null) {
// Always draw boxUnderlineDefault, because it's either the only underline that should be
// drawn or the backdrop for the focused underline expansion.
boxUnderlineDefault.draw(canvas);

if (editText.isFocused()) {
Rect focusedUnderlineBounds = boxUnderlineFocused.getBounds();
Rect defaultUnderlineBounds = boxUnderlineDefault.getBounds();

// Calculate the expansion fraction bounds based on the CollapsingTextHelper's hint text
// expansion fraction.
float hintExpansionFraction = collapsingTextHelper.getExpansionFraction();
int midpointX = defaultUnderlineBounds.centerX();

focusedUnderlineBounds.left =
AnimationUtils.lerp(midpointX, defaultUnderlineBounds.left, hintExpansionFraction);
focusedUnderlineBounds.right =
AnimationUtils.lerp(midpointX, defaultUnderlineBounds.right, hintExpansionFraction);

boxUnderlineFocused.draw(canvas);
}
}
}

Expand Down Expand Up @@ -4365,18 +4396,21 @@ void updateTextInputBoxState() {
tintEndIconOnError(indicatorViewController.errorShouldBeShown());
}

int originalBoxStrokeWidthPx = boxStrokeWidthPx;
// Update the text box's stroke width based on the current state.
if (hasFocus && isEnabled()) {
boxStrokeWidthPx = boxStrokeWidthFocusedPx;
} else {
boxStrokeWidthPx = boxStrokeWidthDefaultPx;
}

if (boxStrokeWidthPx != originalBoxStrokeWidthPx
&& boxBackgroundMode == BOX_BACKGROUND_OUTLINE) {
// If stroke width changes, cutout bounds need to be recalculated.
recalculateCutout();
// Update the outlined text box's stroke width based on the current state. The filled stroke
// width does not need to be updated based on state, because the filled stroke is handled as
// separate drawables for default and focused states, with constant stroke widths; only the
// stroke visibility changes based on state.
if (boxBackgroundMode == BOX_BACKGROUND_OUTLINE) {
int originalBoxStrokeWidthPx = boxStrokeWidthPx;
if (hasFocus && isEnabled()) {
boxStrokeWidthPx = boxStrokeWidthFocusedPx;
} else {
boxStrokeWidthPx = boxStrokeWidthDefaultPx;
}
if (boxStrokeWidthPx != originalBoxStrokeWidthPx) {
// If stroke width changes, cutout bounds need to be recalculated.
recalculateCutout();
}
}

// Update the text box's background color based on the current state.
Expand Down

0 comments on commit 2463946

Please sign in to comment.