conradchen d9b97b42ff [TextField] Make collapsed hint aligned with prefix and suffix text
The original logic prevents displaying the collapsed hint on top of suffix text. The behavior looks not consistent with the case of prefix text or outline variants of text fields.

This CL also fixes the issue that when drawable paddings are set on EditText, the collapsed hint is not aligned with the prefix anymore.

Resolves https://github.com/material-components/material-components-android/issues/2800

PiperOrigin-RevId: 511390069
2023-02-23 03:45:40 -08:00

363 lines
13 KiB
Java

/*
* 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.convertScaleType;
import static com.google.android.material.textfield.IconHelper.refreshIconDrawableState;
import static com.google.android.material.textfield.IconHelper.setCompatRippleBackgroundIfNeeded;
import static com.google.android.material.textfield.IconHelper.setIconMinSize;
import static com.google.android.material.textfield.IconHelper.setIconOnClickListener;
import static com.google.android.material.textfield.IconHelper.setIconOnLongClickListener;
import static com.google.android.material.textfield.IconHelper.setIconScaleType;
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.ImageView.ScaleType;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
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 int startIconMinSize;
@NonNull private ScaleType startIconScaleType;
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);
setCompatRippleBackgroundIfNeeded(startIconView);
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));
}
setStartIconMinSize(
a.getDimensionPixelSize(
R.styleable.TextInputLayout_startIconMinSize,
getResources().getDimensionPixelSize(R.dimen.mtrl_min_touch_target_size)));
if (a.hasValue(R.styleable.TextInputLayout_startIconScaleType)) {
setStartIconScaleType(
convertScaleType(a.getInt(R.styleable.TextInputLayout_startIconScaleType, -1)));
}
}
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 <code>null</code> 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 setStartIconMinSize(@Px int iconSize) {
if (iconSize < 0) {
throw new IllegalArgumentException("startIconSize cannot be less than 0");
}
if (iconSize != startIconMinSize) {
startIconMinSize = iconSize;
setIconMinSize(startIconView, iconSize);
}
}
int getStartIconMinSize() {
return startIconMinSize;
}
void setStartIconScaleType(@NonNull ScaleType startIconScaleType) {
this.startIconScaleType = startIconScaleType;
setIconScaleType(startIconView, startIconScaleType);
}
@NonNull
ScaleType getStartIconScaleType() {
return startIconScaleType;
}
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());
}
int getPrefixTextStartOffset() {
int startIconOffset;
if (isStartIconVisible()) {
startIconOffset =
startIconView.getMeasuredWidth()
+ MarginLayoutParamsCompat.getMarginEnd(
(MarginLayoutParams) startIconView.getLayoutParams());
} else {
startIconOffset = 0;
}
return ViewCompat.getPaddingStart(this)
+ ViewCompat.getPaddingStart(prefixTextView)
+ startIconOffset;
}
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();
}
}