From 9a46af2af2a7faa99ed287e9f1ff89e2a05e3238 Mon Sep 17 00:00:00 2001 From: conradchen Date: Mon, 7 Mar 2022 16:37:05 +0000 Subject: [PATCH] [CleanUp][TextField] Extract start components from TextInputLayout PiperOrigin-RevId: 432953110 --- .../{IconTintHelper.java => IconHelper.java} | 38 ++- .../textfield/StartCompoundLayout.java | 305 ++++++++++++++++++ .../material/textfield/TextInputLayout.java | 200 ++---------- 3 files changed, 374 insertions(+), 169 deletions(-) rename lib/java/com/google/android/material/textfield/{IconTintHelper.java => IconHelper.java} (68%) create mode 100644 lib/java/com/google/android/material/textfield/StartCompoundLayout.java diff --git a/lib/java/com/google/android/material/textfield/IconTintHelper.java b/lib/java/com/google/android/material/textfield/IconHelper.java similarity index 68% rename from lib/java/com/google/android/material/textfield/IconTintHelper.java rename to lib/java/com/google/android/material/textfield/IconHelper.java index 4ecd42a82fe..23f3bb68e79 100644 --- a/lib/java/com/google/android/material/textfield/IconTintHelper.java +++ b/lib/java/com/google/android/material/textfield/IconHelper.java @@ -19,13 +19,47 @@ import android.content.res.ColorStateList; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; +import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.graphics.drawable.DrawableCompat; +import androidx.core.view.ViewCompat; import com.google.android.material.internal.CheckableImageButton; import java.util.Arrays; -class IconTintHelper { - private IconTintHelper() {} +class IconHelper { + private IconHelper() {} + + static void setIconOnClickListener( + @NonNull CheckableImageButton iconView, + @Nullable OnClickListener onClickListener, + @Nullable OnLongClickListener onLongClickListener) { + iconView.setOnClickListener(onClickListener); + setIconClickable(iconView, onLongClickListener); + } + + static void setIconOnLongClickListener( + @NonNull CheckableImageButton iconView, @Nullable OnLongClickListener onLongClickListener) { + iconView.setOnLongClickListener(onLongClickListener); + setIconClickable(iconView, onLongClickListener); + } + + private static void setIconClickable( + @NonNull CheckableImageButton iconView, @Nullable OnLongClickListener onLongClickListener) { + boolean iconClickable = ViewCompat.hasOnClickListeners(iconView); + boolean iconLongClickable = onLongClickListener != null; + boolean iconFocusable = iconClickable || iconLongClickable; + iconView.setFocusable(iconFocusable); + iconView.setClickable(iconClickable); + iconView.setPressable(iconClickable); + iconView.setLongClickable(iconLongClickable); + ViewCompat.setImportantForAccessibility( + iconView, + iconFocusable + ? ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES + : ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO); + } /** * Applies the given icon tint according to the merged view state of the host text input layout diff --git a/lib/java/com/google/android/material/textfield/StartCompoundLayout.java b/lib/java/com/google/android/material/textfield/StartCompoundLayout.java new file mode 100644 index 00000000000..8a35edf8b5f --- /dev/null +++ b/lib/java/com/google/android/material/textfield/StartCompoundLayout.java @@ -0,0 +1,305 @@ +/* + * Copyright (C) 2022 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. + */ + +package com.google.android.material.textfield; + +import com.google.android.material.R; + +import static com.google.android.material.textfield.IconHelper.applyIconTint; +import static com.google.android.material.textfield.IconHelper.refreshIconDrawableState; +import static com.google.android.material.textfield.IconHelper.setIconOnClickListener; +import static com.google.android.material.textfield.IconHelper.setIconOnLongClickListener; + +import android.annotation.SuppressLint; +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import androidx.appcompat.widget.AppCompatTextView; +import androidx.appcompat.widget.TintTypedArray; +import android.text.TextUtils; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StyleRes; +import androidx.core.view.MarginLayoutParamsCompat; +import androidx.core.view.ViewCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; +import androidx.core.widget.TextViewCompat; +import com.google.android.material.internal.CheckableImageButton; +import com.google.android.material.internal.ViewUtils; +import com.google.android.material.resources.MaterialResources; + +/** + * A compound layout that includes views that will be shown at the start of {@link TextInputLayout} + * and their relevant rendering and presenting logic. + */ +@SuppressLint("ViewConstructor") +class StartCompoundLayout extends LinearLayout { + private final TextInputLayout textInputLayout; + + private final TextView prefixTextView; + @Nullable private CharSequence prefixText; + + private final CheckableImageButton startIconView; + private ColorStateList startIconTintList; + private PorterDuff.Mode startIconTintMode; + private OnLongClickListener startIconOnLongClickListener; + + private boolean hintExpanded; + + StartCompoundLayout(TextInputLayout textInputLayout, TintTypedArray a) { + super(textInputLayout.getContext()); + + this.textInputLayout = textInputLayout; + + setVisibility(GONE); + setOrientation(HORIZONTAL); + setLayoutParams( + new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.MATCH_PARENT, + Gravity.START | Gravity.LEFT)); + + final LayoutInflater layoutInflater = LayoutInflater.from(getContext()); + startIconView = + (CheckableImageButton) + layoutInflater.inflate(R.layout.design_text_input_start_icon, this, false); + + prefixTextView = new AppCompatTextView(getContext()); + + initStartIconView(a); + initPrefixTextView(a); + + addView(startIconView); + addView(prefixTextView); + } + + private void initStartIconView(TintTypedArray a) { + + if (MaterialResources.isFontScaleAtLeast1_3(getContext())) { + ViewGroup.MarginLayoutParams lp = + (ViewGroup.MarginLayoutParams) startIconView.getLayoutParams(); + MarginLayoutParamsCompat.setMarginEnd(lp, 0); + } + setStartIconOnClickListener(null); + setStartIconOnLongClickListener(null); + // Default tint for a start icon or value specified by user. + if (a.hasValue(R.styleable.TextInputLayout_startIconTint)) { + startIconTintList = + MaterialResources.getColorStateList( + getContext(), a, R.styleable.TextInputLayout_startIconTint); + } + // Default tint mode for a start icon or value specified by user. + if (a.hasValue(R.styleable.TextInputLayout_startIconTintMode)) { + startIconTintMode = + ViewUtils.parseTintMode( + a.getInt(R.styleable.TextInputLayout_startIconTintMode, -1), null); + } + // Set up start icon if any. + if (a.hasValue(R.styleable.TextInputLayout_startIconDrawable)) { + setStartIconDrawable(a.getDrawable(R.styleable.TextInputLayout_startIconDrawable)); + if (a.hasValue(R.styleable.TextInputLayout_startIconContentDescription)) { + setStartIconContentDescription( + a.getText(R.styleable.TextInputLayout_startIconContentDescription)); + } + setStartIconCheckable(a.getBoolean(R.styleable.TextInputLayout_startIconCheckable, true)); + } + } + + private void initPrefixTextView(TintTypedArray a) { + prefixTextView.setVisibility(GONE); + // Set up prefix view. + prefixTextView.setId(R.id.textinput_prefix_text); + prefixTextView.setLayoutParams( + new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + ViewCompat.setAccessibilityLiveRegion( + prefixTextView, ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE); + + setPrefixTextAppearance(a.getResourceId(R.styleable.TextInputLayout_prefixTextAppearance, 0)); + if (a.hasValue(R.styleable.TextInputLayout_prefixTextColor)) { + setPrefixTextColor(a.getColorStateList(R.styleable.TextInputLayout_prefixTextColor)); + } + setPrefixText(a.getText(R.styleable.TextInputLayout_prefixText)); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + updatePrefixTextViewPadding(); + } + + @NonNull + TextView getPrefixTextView() { + return prefixTextView; + } + + void setPrefixText(@Nullable final CharSequence prefixText) { + this.prefixText = TextUtils.isEmpty(prefixText) ? null : prefixText; + prefixTextView.setText(prefixText); + updateVisibility(); + } + + /** + * Returns the prefix text that was set to be displayed with {@link #setPrefixText(CharSequence)}, + * or null if there is no prefix text. + * + * @see #setPrefixText(CharSequence) + */ + @Nullable + CharSequence getPrefixText() { + return prefixText; + } + + void setPrefixTextColor(@NonNull ColorStateList prefixTextColor) { + prefixTextView.setTextColor(prefixTextColor); + } + + @Nullable + ColorStateList getPrefixTextColor() { + return prefixTextView.getTextColors(); + } + + void setPrefixTextAppearance(@StyleRes int prefixTextAppearance) { + TextViewCompat.setTextAppearance(prefixTextView, prefixTextAppearance); + } + + void setStartIconDrawable(@Nullable Drawable startIconDrawable) { + startIconView.setImageDrawable(startIconDrawable); + if (startIconDrawable != null) { + applyIconTint(textInputLayout, startIconView, startIconTintList, startIconTintMode); + setStartIconVisible(true); + refreshStartIconDrawableState(); + } else { + setStartIconVisible(false); + setStartIconOnClickListener(null); + setStartIconOnLongClickListener(null); + setStartIconContentDescription(null); + } + } + + @Nullable + Drawable getStartIconDrawable() { + return startIconView.getDrawable(); + } + + void setStartIconOnClickListener(@Nullable OnClickListener startIconOnClickListener) { + setIconOnClickListener(startIconView, startIconOnClickListener, startIconOnLongClickListener); + } + + void setStartIconOnLongClickListener( + @Nullable OnLongClickListener startIconOnLongClickListener) { + this.startIconOnLongClickListener = startIconOnLongClickListener; + setIconOnLongClickListener(startIconView, startIconOnLongClickListener); + } + + void setStartIconVisible(boolean visible) { + if (isStartIconVisible() != visible) { + startIconView.setVisibility(visible ? View.VISIBLE : View.GONE); + updatePrefixTextViewPadding(); + updateVisibility(); + } + } + + boolean isStartIconVisible() { + return startIconView.getVisibility() == View.VISIBLE; + } + + void refreshStartIconDrawableState() { + refreshIconDrawableState(textInputLayout, startIconView, startIconTintList); + } + + void setStartIconCheckable(boolean startIconCheckable) { + startIconView.setCheckable(startIconCheckable); + } + + boolean isStartIconCheckable() { + return startIconView.isCheckable(); + } + + void setStartIconContentDescription(@Nullable CharSequence startIconContentDescription) { + if (getStartIconContentDescription() != startIconContentDescription) { + startIconView.setContentDescription(startIconContentDescription); + } + } + + @Nullable + CharSequence getStartIconContentDescription() { + return startIconView.getContentDescription(); + } + + void setStartIconTintList(@Nullable ColorStateList startIconTintList) { + if (this.startIconTintList != startIconTintList) { + this.startIconTintList = startIconTintList; + applyIconTint(textInputLayout, startIconView, startIconTintList, startIconTintMode); + } + } + + void setStartIconTintMode(@Nullable PorterDuff.Mode startIconTintMode) { + if (this.startIconTintMode != startIconTintMode) { + this.startIconTintMode = startIconTintMode; + applyIconTint(textInputLayout, startIconView, startIconTintList, this.startIconTintMode); + } + } + + void setupAccessibilityNodeInfo(@NonNull AccessibilityNodeInfoCompat info) { + if (prefixTextView.getVisibility() == VISIBLE) { + info.setLabelFor(prefixTextView); + info.setTraversalAfter(prefixTextView); + } else { + info.setTraversalAfter(startIconView); + } + } + + void updatePrefixTextViewPadding() { + EditText editText = textInputLayout.editText; + if (editText == null) { + return; + } + int startPadding = isStartIconVisible() ? 0 : ViewCompat.getPaddingStart(editText); + ViewCompat.setPaddingRelative( + prefixTextView, + startPadding, + editText.getCompoundPaddingTop(), + getContext() + .getResources() + .getDimensionPixelSize(R.dimen.material_input_text_to_prefix_suffix_padding), + editText.getCompoundPaddingBottom()); + } + + void onHintStateChanged(boolean hintExpanded) { + this.hintExpanded = hintExpanded; + updateVisibility(); + } + + private void updateVisibility() { + // Set startLayout to visible if start icon or prefix text is present. + int prefixTextVisibility = (prefixText != null && !hintExpanded) ? VISIBLE : GONE; + boolean shouldBeVisible = + startIconView.getVisibility() == VISIBLE || prefixTextVisibility == VISIBLE; + setVisibility(shouldBeVisible ? VISIBLE : GONE); + // Set prefix visibility after updating layout's visibility so screen readers correctly announce + // when prefix text appears. + prefixTextView.setVisibility(prefixTextVisibility); + textInputLayout.updateDummyDrawables(); + } +} diff --git a/lib/java/com/google/android/material/textfield/TextInputLayout.java b/lib/java/com/google/android/material/textfield/TextInputLayout.java index 4e6c94fbbdc..b5490e8e254 100644 --- a/lib/java/com/google/android/material/textfield/TextInputLayout.java +++ b/lib/java/com/google/android/material/textfield/TextInputLayout.java @@ -19,8 +19,8 @@ import com.google.android.material.R; import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; -import static com.google.android.material.textfield.IconTintHelper.applyIconTint; -import static com.google.android.material.textfield.IconTintHelper.refreshIconDrawableState; +import static com.google.android.material.textfield.IconHelper.applyIconTint; +import static com.google.android.material.textfield.IconHelper.refreshIconDrawableState; import static com.google.android.material.textfield.IndicatorViewController.COUNTER_INDEX; import static com.google.android.material.theme.overlay.MaterialThemeOverlay.wrap; @@ -201,7 +201,7 @@ public class TextInputLayout extends LinearLayout { private static final String LOG_TAG = "TextInputLayout"; @NonNull private final FrameLayout inputFrame; - @NonNull private final LinearLayout startLayout; + @NonNull private final StartCompoundLayout startLayout; @NonNull private final LinearLayout endLayout; @NonNull private final FrameLayout endIconFrame; EditText editText; @@ -232,8 +232,6 @@ public class TextInputLayout extends LinearLayout { @Nullable private ColorStateList counterTextColor; @Nullable private ColorStateList counterOverflowTextColor; - @Nullable private CharSequence prefixText; - @NonNull private final TextView prefixTextView; @Nullable private CharSequence suffixText; @NonNull private final TextView suffixTextView; @@ -279,12 +277,8 @@ public class TextInputLayout extends LinearLayout { private final RectF tmpRectF = new RectF(); private Typeface typeface; - @NonNull private final CheckableImageButton startIconView; - private ColorStateList startIconTintList; - private PorterDuff.Mode startIconTintMode; @Nullable private Drawable startDummyDrawable; private int startDummyDrawableWidth; - private OnLongClickListener startIconOnLongClickListener; /** * Values for the end icon mode. @@ -454,22 +448,15 @@ public TextInputLayout(@NonNull Context context, @Nullable AttributeSet attrs, i inputFrame = new FrameLayout(context); endIconFrame = new FrameLayout(context); - startLayout = new LinearLayout(context); endLayout = new LinearLayout(context); - prefixTextView = new AppCompatTextView(context); suffixTextView = new AppCompatTextView(context); - startLayout.setVisibility(GONE); endLayout.setVisibility(GONE); endIconFrame.setVisibility(GONE); - prefixTextView.setVisibility(GONE); suffixTextView.setVisibility(GONE); final LayoutInflater layoutInflater = LayoutInflater.from(context); - startIconView = - (CheckableImageButton) - layoutInflater.inflate(R.layout.design_text_input_start_icon, startLayout, false); errorIconView = (CheckableImageButton) layoutInflater.inflate(R.layout.design_text_input_end_icon, endLayout, false); @@ -479,12 +466,6 @@ public TextInputLayout(@NonNull Context context, @Nullable AttributeSet attrs, i inputFrame.setAddStatesFromChildren(true); - startLayout.setOrientation(HORIZONTAL); - startLayout.setLayoutParams( - new FrameLayout.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.MATCH_PARENT, - Gravity.START | Gravity.LEFT)); endLayout.setOrientation(HORIZONTAL); endLayout.setLayoutParams( new FrameLayout.LayoutParams( @@ -512,6 +493,8 @@ public TextInputLayout(@NonNull Context context, @Nullable AttributeSet attrs, i R.styleable.TextInputLayout_helperTextTextAppearance, R.styleable.TextInputLayout_hintTextAppearance); + startLayout = new StartCompoundLayout(this, a); + hintEnabled = a.getBoolean(R.styleable.TextInputLayout_hintEnabled, true); setHint(a.getText(R.styleable.TextInputLayout_android_hint)); hintAnimationEnabled = a.getBoolean(R.styleable.TextInputLayout_hintAnimationEnabled, true); @@ -683,10 +666,6 @@ public TextInputLayout(@NonNull Context context, @Nullable AttributeSet attrs, i a.getResourceId(R.styleable.TextInputLayout_placeholderTextAppearance, 0); final CharSequence placeholderText = a.getText(R.styleable.TextInputLayout_placeholderText); - final int prefixTextAppearance = - a.getResourceId(R.styleable.TextInputLayout_prefixTextAppearance, 0); - final CharSequence prefixText = a.getText(R.styleable.TextInputLayout_prefixText); - final int suffixTextAppearance = a.getResourceId(R.styleable.TextInputLayout_suffixTextAppearance, 0); final CharSequence suffixText = a.getText(R.styleable.TextInputLayout_suffixText); @@ -697,36 +676,6 @@ public TextInputLayout(@NonNull Context context, @Nullable AttributeSet attrs, i counterOverflowTextAppearance = a.getResourceId(R.styleable.TextInputLayout_counterOverflowTextAppearance, 0); - // Initialize start icon view. - if (MaterialResources.isFontScaleAtLeast1_3(context)) { - ViewGroup.MarginLayoutParams lp = - (ViewGroup.MarginLayoutParams) startIconView.getLayoutParams(); - MarginLayoutParamsCompat.setMarginEnd(lp, 0); - } - setStartIconOnClickListener(null); - setStartIconOnLongClickListener(null); - // Default tint for a start icon or value specified by user. - if (a.hasValue(R.styleable.TextInputLayout_startIconTint)) { - startIconTintList = - MaterialResources.getColorStateList( - context, a, R.styleable.TextInputLayout_startIconTint); - } - // Default tint mode for a start icon or value specified by user. - if (a.hasValue(R.styleable.TextInputLayout_startIconTintMode)) { - startIconTintMode = - ViewUtils.parseTintMode( - a.getInt(R.styleable.TextInputLayout_startIconTintMode, -1), null); - } - // Set up start icon if any. - if (a.hasValue(R.styleable.TextInputLayout_startIconDrawable)) { - setStartIconDrawable(a.getDrawable(R.styleable.TextInputLayout_startIconDrawable)); - if (a.hasValue(R.styleable.TextInputLayout_startIconContentDescription)) { - setStartIconContentDescription( - a.getText(R.styleable.TextInputLayout_startIconContentDescription)); - } - setStartIconCheckable(a.getBoolean(R.styleable.TextInputLayout_startIconCheckable, true)); - } - setBoxBackgroundMode( a.getInt(R.styleable.TextInputLayout_boxBackgroundMode, BOX_BACKGROUND_NONE)); @@ -792,14 +741,6 @@ public TextInputLayout(@NonNull Context context, @Nullable AttributeSet attrs, i a.getText(R.styleable.TextInputLayout_passwordToggleContentDescription)); } - // Set up prefix view. - prefixTextView.setId(R.id.textinput_prefix_text); - prefixTextView.setLayoutParams( - new FrameLayout.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); - ViewCompat.setAccessibilityLiveRegion( - prefixTextView, ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE); - // Set up suffix view. suffixTextView.setId(R.id.textinput_suffix_text); suffixTextView.setLayoutParams( @@ -818,7 +759,6 @@ public TextInputLayout(@NonNull Context context, @Nullable AttributeSet attrs, i setCounterTextAppearance(counterTextAppearance); setPlaceholderText(placeholderText); setPlaceholderTextAppearance(placeholderTextAppearance); - setPrefixTextAppearance(prefixTextAppearance); setSuffixTextAppearance(suffixTextAppearance); if (a.hasValue(R.styleable.TextInputLayout_errorTextColor)) { @@ -841,9 +781,6 @@ public TextInputLayout(@NonNull Context context, @Nullable AttributeSet attrs, i setPlaceholderTextColor( a.getColorStateList(R.styleable.TextInputLayout_placeholderTextColor)); } - if (a.hasValue(R.styleable.TextInputLayout_prefixTextColor)) { - setPrefixTextColor(a.getColorStateList(R.styleable.TextInputLayout_prefixTextColor)); - } if (a.hasValue(R.styleable.TextInputLayout_suffixTextColor)) { setSuffixTextColor(a.getColorStateList(R.styleable.TextInputLayout_suffixTextColor)); } @@ -862,9 +799,6 @@ public TextInputLayout(@NonNull Context context, @Nullable AttributeSet attrs, i ViewCompat.setImportantForAutofill(this, View.IMPORTANT_FOR_AUTOFILL_YES); } - startLayout.addView(startIconView); - startLayout.addView(prefixTextView); - endIconFrame.addView(endIconView); endLayout.addView(suffixTextView); @@ -881,7 +815,6 @@ public TextInputLayout(@NonNull Context context, @Nullable AttributeSet attrs, i setCounterEnabled(counterEnabled); setHelperText(helperText); - setPrefixText(prefixText); setSuffixText(suffixText); } @@ -1551,7 +1484,6 @@ public void onTextChanged(CharSequence s, int start, int before, int count) {} endIconFrame.bringToFront(); errorIconView.bringToFront(); dispatchOnEditTextAttached(); - updatePrefixTextViewPadding(); updateSuffixTextViewPadding(); // Only call setEnabled on the edit text if the layout is disabled, to prevent reenabling an @@ -2525,9 +2457,7 @@ public int getPlaceholderTextAppearance() { * @see #getPrefixText() */ public void setPrefixText(@Nullable final CharSequence prefixText) { - this.prefixText = TextUtils.isEmpty(prefixText) ? null : prefixText; - prefixTextView.setText(prefixText); - updatePrefixTextVisibility(); + startLayout.setPrefixText(prefixText); } /** @@ -2538,7 +2468,7 @@ public void setPrefixText(@Nullable final CharSequence prefixText) { */ @Nullable public CharSequence getPrefixText() { - return prefixText; + return startLayout.getPrefixText(); } /** @@ -2551,16 +2481,7 @@ public CharSequence getPrefixText() { */ @NonNull public TextView getPrefixTextView() { - return prefixTextView; - } - - private void updatePrefixTextVisibility() { - int visibility = (prefixText != null && !isHintExpanded()) ? VISIBLE : GONE; - updateStartLayoutVisibility(); - // Set visibility after updating start layout's visibility so screen readers correctly announce - // when prefix text appears. - prefixTextView.setVisibility(visibility); - updateDummyDrawables(); + return startLayout.getPrefixTextView(); } /** @@ -2569,7 +2490,7 @@ private void updatePrefixTextVisibility() { * @attr ref com.google.android.material.R.styleable#TextInputLayout_prefixTextColor */ public void setPrefixTextColor(@NonNull ColorStateList prefixTextColor) { - prefixTextView.setTextColor(prefixTextColor); + startLayout.setPrefixTextColor(prefixTextColor); } /** @@ -2579,7 +2500,7 @@ public void setPrefixTextColor(@NonNull ColorStateList prefixTextColor) { */ @Nullable public ColorStateList getPrefixTextColor() { - return prefixTextView.getTextColors(); + return startLayout.getPrefixTextColor(); } /** @@ -2588,22 +2509,7 @@ public ColorStateList getPrefixTextColor() { * @attr ref com.google.android.material.R.styleable#TextInputLayout_prefixTextAppearance */ public void setPrefixTextAppearance(@StyleRes int prefixTextAppearance) { - TextViewCompat.setTextAppearance(prefixTextView, prefixTextAppearance); - } - - private void updatePrefixTextViewPadding() { - if (editText == null) { - return; - } - int startPadding = isStartIconVisible() ? 0 : ViewCompat.getPaddingStart(editText); - ViewCompat.setPaddingRelative( - prefixTextView, - startPadding, - editText.getCompoundPaddingTop(), - getContext() - .getResources() - .getDimensionPixelSize(R.dimen.material_input_text_to_prefix_suffix_padding), - editText.getCompoundPaddingBottom()); + startLayout.setPrefixTextAppearance(prefixTextAppearance); } /** @@ -2827,18 +2733,18 @@ private Rect calculateCollapsedTextBounds(@NonNull Rect rect) { private int getLabelLeftBoundAlightWithPrefix(int rectLeft, boolean isRtl) { int left = rectLeft + editText.getCompoundPaddingLeft(); - if (prefixText != null && !isRtl) { + if (getPrefixText() != null && !isRtl) { // Label should be vertically aligned with prefix - left = left - prefixTextView.getMeasuredWidth() + prefixTextView.getPaddingLeft(); + left = left - getPrefixTextView().getMeasuredWidth() + getPrefixTextView().getPaddingLeft(); } return left; } private int getLabelRightBoundAlignedWithSuffix(int rectRight, boolean isRtl) { int right = rectRight - editText.getCompoundPaddingRight(); - if (prefixText != null && isRtl) { + if (getPrefixText() != null && isRtl) { // Label should be vertically aligned with prefix if in RTL - right += prefixTextView.getMeasuredWidth() - prefixTextView.getPaddingRight(); + right += getPrefixTextView().getMeasuredWidth() - getPrefixTextView().getPaddingRight(); } return right; } @@ -3222,7 +3128,6 @@ public void run() { }); } updatePlaceholderMeasurementsBasedOnEditText(); - updatePrefixTextViewPadding(); updateSuffixTextViewPadding(); } @@ -3280,17 +3185,7 @@ public void setStartIconDrawable(@DrawableRes int resId) { * @attr ref com.google.android.material.R.styleable#TextInputLayout_startIconDrawable */ public void setStartIconDrawable(@Nullable Drawable startIconDrawable) { - startIconView.setImageDrawable(startIconDrawable); - if (startIconDrawable != null) { - applyIconTint(this, startIconView, startIconTintList, startIconTintMode); - setStartIconVisible(true); - refreshStartIconDrawableState(); - } else { - setStartIconVisible(false); - setStartIconOnClickListener(null); - setStartIconOnLongClickListener(null); - setStartIconContentDescription(null); - } + startLayout.setStartIconDrawable(startIconDrawable); } /** @@ -3302,7 +3197,7 @@ public void setStartIconDrawable(@Nullable Drawable startIconDrawable) { */ @Nullable public Drawable getStartIconDrawable() { - return startIconView.getDrawable(); + return startLayout.getStartIconDrawable(); } /** @@ -3313,7 +3208,7 @@ public Drawable getStartIconDrawable() { * view will have, or null to clear it. */ public void setStartIconOnClickListener(@Nullable OnClickListener startIconOnClickListener) { - setIconOnClickListener(startIconView, startIconOnClickListener, startIconOnLongClickListener); + startLayout.setStartIconOnClickListener(startIconOnClickListener); } /** @@ -3325,8 +3220,7 @@ public void setStartIconOnClickListener(@Nullable OnClickListener startIconOnCli */ public void setStartIconOnLongClickListener( @Nullable OnLongClickListener startIconOnLongClickListener) { - this.startIconOnLongClickListener = startIconOnLongClickListener; - setIconOnLongClickListener(startIconView, startIconOnLongClickListener); + startLayout.setStartIconOnLongClickListener(startIconOnLongClickListener); } /** @@ -3335,12 +3229,7 @@ public void setStartIconOnLongClickListener( * @param visible whether the icon should be set to visible */ public void setStartIconVisible(boolean visible) { - if (isStartIconVisible() != visible) { - startIconView.setVisibility(visible ? View.VISIBLE : View.GONE); - updateStartLayoutVisibility(); - updatePrefixTextViewPadding(); - updateDummyDrawables(); - } + startLayout.setStartIconVisible(visible); } /** @@ -3349,7 +3238,7 @@ public void setStartIconVisible(boolean visible) { * @see #setStartIconVisible(boolean) */ public boolean isStartIconVisible() { - return startIconView.getVisibility() == View.VISIBLE; + return startLayout.isStartIconVisible(); } /** @@ -3357,7 +3246,7 @@ public boolean isStartIconVisible() { * has a color for a state that depends on a click (such as checked state). */ public void refreshStartIconDrawableState() { - refreshIconDrawableState(this, startIconView, startIconTintList); + startLayout.refreshStartIconDrawableState(); } /** @@ -3371,7 +3260,7 @@ public void refreshStartIconDrawableState() { * @attr ref com.google.android.material.R.styleable#TextInputLayout_startIconCheckable */ public void setStartIconCheckable(boolean startIconCheckable) { - startIconView.setCheckable(startIconCheckable); + startLayout.setStartIconCheckable(startIconCheckable); } /** @@ -3380,7 +3269,7 @@ public void setStartIconCheckable(boolean startIconCheckable) { * @see #setStartIconCheckable(boolean) */ public boolean isStartIconCheckable() { - return startIconView.isCheckable(); + return startLayout.isStartIconCheckable(); } /** @@ -3407,9 +3296,7 @@ public void setStartIconContentDescription(@StringRes int resId) { * @attr ref com.google.android.material.R.styleable#TextInputLayout_startIconContentDescription */ public void setStartIconContentDescription(@Nullable CharSequence startIconContentDescription) { - if (getStartIconContentDescription() != startIconContentDescription) { - startIconView.setContentDescription(startIconContentDescription); - } + startLayout.setStartIconContentDescription(startIconContentDescription); } /** @@ -3420,7 +3307,7 @@ public void setStartIconContentDescription(@Nullable CharSequence startIconConte */ @Nullable public CharSequence getStartIconContentDescription() { - return startIconView.getContentDescription(); + return startLayout.getStartIconContentDescription(); } /** @@ -3435,10 +3322,7 @@ public CharSequence getStartIconContentDescription() { * @attr ref com.google.android.material.R.styleable#TextInputLayout_startIconTint */ public void setStartIconTintList(@Nullable ColorStateList startIconTintList) { - if (this.startIconTintList != startIconTintList) { - this.startIconTintList = startIconTintList; - applyIconTint(this, startIconView, this.startIconTintList, startIconTintMode); - } + startLayout.setStartIconTintList(startIconTintList); } /** @@ -3450,10 +3334,7 @@ public void setStartIconTintList(@Nullable ColorStateList startIconTintList) { * @attr ref com.google.android.material.R.styleable#TextInputLayout_startIconTintMode */ public void setStartIconTintMode(@Nullable PorterDuff.Mode startIconTintMode) { - if (this.startIconTintMode != startIconTintMode) { - this.startIconTintMode = startIconTintMode; - applyIconTint(this, startIconView, startIconTintList, this.startIconTintMode); - } + startLayout.setStartIconTintMode(startIconTintMode); } /** @@ -3993,14 +3874,6 @@ private void updateEndLayoutVisibility() { endLayout.setVisibility(shouldBeVisible ? VISIBLE : GONE); } - private void updateStartLayoutVisibility() { - // Set startLayout to visible if start icon or prefix text is present. - int prefixTextVisibility = (prefixText != null && !isHintExpanded()) ? VISIBLE : GONE; - boolean shouldBeVisible = - startIconView.getVisibility() == VISIBLE || prefixTextVisibility == VISIBLE; - startLayout.setVisibility(shouldBeVisible ? VISIBLE : GONE); - } - @NonNull CheckableImageButton getEndIconView() { return endIconView; @@ -4044,7 +3917,7 @@ private void tintEndIconOnError(boolean tintEndIconOnError) { * We need to add a dummy drawable as the start and/or end compound drawables so that the text is * indented and doesn't display below the icon or suffix/prefix views. */ - private boolean updateDummyDrawables() { + boolean updateDummyDrawables() { if (editText == null) { return false; } @@ -4122,7 +3995,7 @@ private boolean updateDummyDrawables() { } private boolean shouldUpdateStartDummyDrawable() { - return (getStartIconDrawable() != null || prefixText != null) + return (getStartIconDrawable() != null || getPrefixText() != null) && (startLayout.getMeasuredWidth() > 0); } @@ -4272,7 +4145,7 @@ private void collapseHint(boolean animate) { } updatePlaceholderText(); - updatePrefixTextVisibility(); + startLayout.onHintStateChanged(false); updateSuffixTextVisibility(); } @@ -4480,7 +4353,7 @@ private void expandHint(boolean animate) { hintExpanded = true; hidePlaceholderText(); - updatePrefixTextVisibility(); + startLayout.onHintStateChanged(true); updateSuffixTextVisibility(); } @@ -4505,7 +4378,6 @@ public void onAnimationUpdate(@NonNull ValueAnimator animator) { this.animator.start(); } - @VisibleForTesting final boolean isHintExpanded() { return hintExpanded; } @@ -4559,16 +4431,10 @@ public void onInitializeAccessibilityNodeInfo( boolean isHintCollapsed = !layout.isHintExpanded(); boolean showingError = !TextUtils.isEmpty(errorText); boolean contentInvalid = showingError || !TextUtils.isEmpty(counterOverflowDesc); - boolean isShowingPrefixText = layout.getPrefixTextView().getVisibility() == VISIBLE; String hint = hasHint ? hintText.toString() : ""; // Screen readers should follow visual order of the elements of the text field. - if (isShowingPrefixText) { - info.setLabelFor(layout.getPrefixTextView()); - info.setTraversalAfter(layout.getPrefixTextView()); - } else { - info.setTraversalAfter(layout.startIconView); - } + layout.startLayout.setupAccessibilityNodeInfo(info); // Make sure text field has the appropriate announcements. if (showingText) {