mirror of
https://github.com/material-components/material-components-android.git
synced 2026-01-16 18:01:42 +08:00
- The icon's `contentDescription` is used as the tooltip text. - Tooltips are only shown for icons that are interactive (focusable). Decorative-only icons will not display a tooltip. - `CheckableImageButton` is updated to provide a callback for when its focusable state changes, which is used to trigger tooltip updates. - API-level differences are handled to ensure that custom `OnLongClickListeners` are not overridden by the tooltip's long-press listener on older platforms (pre-API 26). PiperOrigin-RevId: 794951524
859 lines
31 KiB
Java
859 lines
31 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 static com.google.android.material.textfield.IconHelper.updateIconTooltip;
|
|
import static com.google.android.material.textfield.TextInputLayout.END_ICON_CLEAR_TEXT;
|
|
import static com.google.android.material.textfield.TextInputLayout.END_ICON_CUSTOM;
|
|
import static com.google.android.material.textfield.TextInputLayout.END_ICON_DROPDOWN_MENU;
|
|
import static com.google.android.material.textfield.TextInputLayout.END_ICON_NONE;
|
|
import static com.google.android.material.textfield.TextInputLayout.END_ICON_PASSWORD_TOGGLE;
|
|
|
|
import android.annotation.SuppressLint;
|
|
import android.content.Context;
|
|
import android.content.res.ColorStateList;
|
|
import android.graphics.PorterDuff;
|
|
import android.graphics.drawable.Drawable;
|
|
import androidx.appcompat.content.res.AppCompatResources;
|
|
import androidx.appcompat.widget.AppCompatTextView;
|
|
import androidx.appcompat.widget.TintTypedArray;
|
|
import android.text.Editable;
|
|
import android.text.TextUtils;
|
|
import android.text.TextWatcher;
|
|
import android.util.SparseArray;
|
|
import android.view.Gravity;
|
|
import android.view.LayoutInflater;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.view.accessibility.AccessibilityManager;
|
|
import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener;
|
|
import android.widget.EditText;
|
|
import android.widget.FrameLayout;
|
|
import android.widget.ImageView.ScaleType;
|
|
import android.widget.LinearLayout;
|
|
import android.widget.TextView;
|
|
import androidx.annotation.DrawableRes;
|
|
import androidx.annotation.IdRes;
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.annotation.Px;
|
|
import androidx.annotation.StringRes;
|
|
import androidx.annotation.StyleRes;
|
|
import androidx.core.graphics.drawable.DrawableCompat;
|
|
import androidx.core.widget.TextViewCompat;
|
|
import com.google.android.material.internal.CheckableImageButton;
|
|
import com.google.android.material.internal.TextWatcherAdapter;
|
|
import com.google.android.material.internal.ViewUtils;
|
|
import com.google.android.material.resources.MaterialResources;
|
|
import com.google.android.material.textfield.TextInputLayout.EndIconMode;
|
|
import com.google.android.material.textfield.TextInputLayout.OnEditTextAttachedListener;
|
|
import com.google.android.material.textfield.TextInputLayout.OnEndIconChangedListener;
|
|
import java.util.LinkedHashSet;
|
|
|
|
/**
|
|
* A compound layout that includes views that will be shown at the end of {@link TextInputLayout}
|
|
* and their relevant rendering and presenting logic.
|
|
*/
|
|
@SuppressLint("ViewConstructor")
|
|
class EndCompoundLayout extends LinearLayout {
|
|
final TextInputLayout textInputLayout;
|
|
|
|
@NonNull private final FrameLayout endIconFrame;
|
|
|
|
@NonNull private final CheckableImageButton errorIconView;
|
|
private ColorStateList errorIconTintList;
|
|
private PorterDuff.Mode errorIconTintMode;
|
|
private OnLongClickListener errorIconOnLongClickListener;
|
|
|
|
@NonNull private final CheckableImageButton endIconView;
|
|
private final EndIconDelegates endIconDelegates;
|
|
@EndIconMode private int endIconMode = END_ICON_NONE;
|
|
private final LinkedHashSet<OnEndIconChangedListener> endIconChangedListeners =
|
|
new LinkedHashSet<>();
|
|
private ColorStateList endIconTintList;
|
|
private PorterDuff.Mode endIconTintMode;
|
|
private int endIconMinSize;
|
|
@NonNull private ScaleType endIconScaleType;
|
|
private OnLongClickListener endIconOnLongClickListener;
|
|
|
|
@Nullable private CharSequence suffixText;
|
|
@NonNull private final TextView suffixTextView;
|
|
|
|
private boolean hintExpanded;
|
|
|
|
private EditText editText;
|
|
@Nullable private final AccessibilityManager accessibilityManager;
|
|
@Nullable private TouchExplorationStateChangeListener touchExplorationStateChangeListener;
|
|
|
|
private final TextWatcher editTextWatcher =
|
|
new TextWatcherAdapter() {
|
|
@Override
|
|
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
|
getEndIconDelegate().beforeEditTextChanged(s, start, count, after);
|
|
}
|
|
|
|
@Override
|
|
public void afterTextChanged(Editable s) {
|
|
getEndIconDelegate().afterEditTextChanged(s);
|
|
}
|
|
};
|
|
|
|
private final OnEditTextAttachedListener onEditTextAttachedListener =
|
|
new OnEditTextAttachedListener() {
|
|
@Override
|
|
public void onEditTextAttached(@NonNull TextInputLayout textInputLayout) {
|
|
if (editText == textInputLayout.getEditText()) {
|
|
return;
|
|
}
|
|
if (editText != null) {
|
|
editText.removeTextChangedListener(editTextWatcher);
|
|
if (editText.getOnFocusChangeListener()
|
|
== getEndIconDelegate().getOnEditTextFocusChangeListener()) {
|
|
editText.setOnFocusChangeListener(null);
|
|
}
|
|
}
|
|
editText = textInputLayout.getEditText();
|
|
if (editText != null) {
|
|
editText.addTextChangedListener(editTextWatcher);
|
|
}
|
|
getEndIconDelegate().onEditTextAttached(editText);
|
|
setOnFocusChangeListenersIfNeeded(getEndIconDelegate());
|
|
}
|
|
};
|
|
|
|
EndCompoundLayout(TextInputLayout textInputLayout, TintTypedArray a) {
|
|
super(textInputLayout.getContext());
|
|
|
|
accessibilityManager =
|
|
(AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
|
|
|
|
this.textInputLayout = textInputLayout;
|
|
|
|
setVisibility(GONE);
|
|
setOrientation(HORIZONTAL);
|
|
setLayoutParams(
|
|
new FrameLayout.LayoutParams(
|
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
Gravity.END | Gravity.RIGHT));
|
|
|
|
endIconFrame = new FrameLayout(getContext());
|
|
endIconFrame.setVisibility(GONE);
|
|
endIconFrame.setLayoutParams(
|
|
new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
|
|
|
LayoutInflater layoutInflater = LayoutInflater.from(getContext());
|
|
errorIconView = createIconView(this, layoutInflater, R.id.text_input_error_icon);
|
|
endIconView = createIconView(endIconFrame, layoutInflater, R.id.text_input_end_icon);
|
|
endIconDelegates = new EndIconDelegates(this, a);
|
|
|
|
suffixTextView = new AppCompatTextView(getContext());
|
|
|
|
initErrorIconView(a);
|
|
initEndIconView(a);
|
|
initSuffixTextView(a);
|
|
|
|
endIconFrame.addView(endIconView);
|
|
|
|
addView(suffixTextView);
|
|
addView(endIconFrame);
|
|
addView(errorIconView);
|
|
|
|
errorIconView.setOnFocusableChangedListener(
|
|
(v, focusable) ->
|
|
updateIconTooltip(
|
|
errorIconView,
|
|
errorIconOnLongClickListener,
|
|
errorIconView.getContentDescription()));
|
|
endIconView.setOnFocusableChangedListener(
|
|
(v, focusable) ->
|
|
updateIconTooltip(
|
|
endIconView, endIconOnLongClickListener, getEndIconContentDescription()));
|
|
|
|
textInputLayout.addOnEditTextAttachedListener(onEditTextAttachedListener);
|
|
addOnAttachStateChangeListener(
|
|
new OnAttachStateChangeListener() {
|
|
@Override
|
|
public void onViewAttachedToWindow(View ignored) {
|
|
addTouchExplorationStateChangeListenerIfNeeded();
|
|
}
|
|
|
|
@Override
|
|
public void onViewDetachedFromWindow(View ignored) {
|
|
removeTouchExplorationStateChangeListenerIfNeeded();
|
|
}
|
|
});
|
|
}
|
|
|
|
private CheckableImageButton createIconView(
|
|
ViewGroup root, LayoutInflater inflater, @IdRes int id) {
|
|
CheckableImageButton iconView =
|
|
(CheckableImageButton) inflater.inflate(
|
|
R.layout.design_text_input_end_icon, root, false);
|
|
iconView.setId(id);
|
|
setCompatRippleBackgroundIfNeeded(iconView);
|
|
if (MaterialResources.isFontScaleAtLeast1_3(getContext())) {
|
|
ViewGroup.MarginLayoutParams lp =
|
|
(ViewGroup.MarginLayoutParams) iconView.getLayoutParams();
|
|
lp.setMarginStart(0);
|
|
}
|
|
return iconView;
|
|
}
|
|
|
|
private void initErrorIconView(TintTypedArray a) {
|
|
if (a.hasValue(R.styleable.TextInputLayout_errorIconTint)) {
|
|
errorIconTintList =
|
|
MaterialResources.getColorStateList(
|
|
getContext(), a, R.styleable.TextInputLayout_errorIconTint);
|
|
}
|
|
if (a.hasValue(R.styleable.TextInputLayout_errorIconTintMode)) {
|
|
errorIconTintMode =
|
|
ViewUtils.parseTintMode(
|
|
a.getInt(R.styleable.TextInputLayout_errorIconTintMode, -1), null);
|
|
}
|
|
if (a.hasValue(R.styleable.TextInputLayout_errorIconDrawable)) {
|
|
setErrorIconDrawable(a.getDrawable(R.styleable.TextInputLayout_errorIconDrawable));
|
|
}
|
|
errorIconView.setContentDescription(
|
|
getResources().getText(R.string.error_icon_content_description));
|
|
errorIconView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
|
errorIconView.setClickable(false);
|
|
errorIconView.setPressable(false);
|
|
errorIconView.setCheckable(false);
|
|
errorIconView.setFocusable(false);
|
|
}
|
|
|
|
private void initEndIconView(TintTypedArray a) {
|
|
// Set up the end icon if any.
|
|
if (!a.hasValue(R.styleable.TextInputLayout_passwordToggleEnabled)) {
|
|
// Default tint for any end icon or value specified by user
|
|
if (a.hasValue(R.styleable.TextInputLayout_endIconTint)) {
|
|
endIconTintList =
|
|
MaterialResources.getColorStateList(
|
|
getContext(), a, R.styleable.TextInputLayout_endIconTint);
|
|
}
|
|
// Default tint mode for any end icon or value specified by user
|
|
if (a.hasValue(R.styleable.TextInputLayout_endIconTintMode)) {
|
|
endIconTintMode =
|
|
ViewUtils.parseTintMode(
|
|
a.getInt(R.styleable.TextInputLayout_endIconTintMode, -1), null);
|
|
}
|
|
}
|
|
if (a.hasValue(R.styleable.TextInputLayout_endIconMode)) {
|
|
// Specific defaults depending on which end icon mode is set
|
|
setEndIconMode(a.getInt(R.styleable.TextInputLayout_endIconMode, END_ICON_NONE));
|
|
if (a.hasValue(R.styleable.TextInputLayout_endIconContentDescription)) {
|
|
setEndIconContentDescription(
|
|
a.getText(R.styleable.TextInputLayout_endIconContentDescription));
|
|
}
|
|
setEndIconCheckable(a.getBoolean(R.styleable.TextInputLayout_endIconCheckable, true));
|
|
} else if (a.hasValue(R.styleable.TextInputLayout_passwordToggleEnabled)) {
|
|
// Support for deprecated attributes related to the password toggle end icon
|
|
if (a.hasValue(R.styleable.TextInputLayout_passwordToggleTint)) {
|
|
endIconTintList =
|
|
MaterialResources.getColorStateList(
|
|
getContext(), a, R.styleable.TextInputLayout_passwordToggleTint);
|
|
}
|
|
if (a.hasValue(R.styleable.TextInputLayout_passwordToggleTintMode)) {
|
|
endIconTintMode =
|
|
ViewUtils.parseTintMode(
|
|
a.getInt(R.styleable.TextInputLayout_passwordToggleTintMode, -1), null);
|
|
}
|
|
boolean passwordToggleEnabled =
|
|
a.getBoolean(R.styleable.TextInputLayout_passwordToggleEnabled, false);
|
|
setEndIconMode(passwordToggleEnabled ? END_ICON_PASSWORD_TOGGLE : END_ICON_NONE);
|
|
setEndIconContentDescription(
|
|
a.getText(R.styleable.TextInputLayout_passwordToggleContentDescription));
|
|
}
|
|
setEndIconMinSize(
|
|
a.getDimensionPixelSize(
|
|
R.styleable.TextInputLayout_endIconMinSize,
|
|
getResources().getDimensionPixelSize(R.dimen.mtrl_min_touch_target_size)));
|
|
if (a.hasValue(R.styleable.TextInputLayout_endIconScaleType)) {
|
|
setEndIconScaleType(
|
|
convertScaleType(a.getInt(R.styleable.TextInputLayout_endIconScaleType, -1)));
|
|
}
|
|
}
|
|
|
|
private void initSuffixTextView(TintTypedArray a) {
|
|
// Set up suffix view.
|
|
suffixTextView.setVisibility(GONE);
|
|
suffixTextView.setId(R.id.textinput_suffix_text);
|
|
suffixTextView.setLayoutParams(
|
|
new LayoutParams(
|
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
Gravity.BOTTOM));
|
|
suffixTextView.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
|
|
|
|
setSuffixTextAppearance(
|
|
a.getResourceId(R.styleable.TextInputLayout_suffixTextAppearance, 0));
|
|
if (a.hasValue(R.styleable.TextInputLayout_suffixTextColor)) {
|
|
setSuffixTextColor(a.getColorStateList(R.styleable.TextInputLayout_suffixTextColor));
|
|
}
|
|
setSuffixText(a.getText(R.styleable.TextInputLayout_suffixText));
|
|
}
|
|
|
|
void setErrorIconDrawable(@DrawableRes int resId) {
|
|
setErrorIconDrawable(resId != 0 ? AppCompatResources.getDrawable(getContext(), resId) : null);
|
|
refreshErrorIconDrawableState();
|
|
}
|
|
|
|
void setErrorIconDrawable(@Nullable Drawable errorIconDrawable) {
|
|
errorIconView.setImageDrawable(errorIconDrawable);
|
|
updateErrorIconVisibility();
|
|
applyIconTint(textInputLayout, errorIconView, errorIconTintList, errorIconTintMode);
|
|
}
|
|
|
|
Drawable getErrorIconDrawable() {
|
|
return errorIconView.getDrawable();
|
|
}
|
|
|
|
void setErrorIconTintList(@Nullable ColorStateList errorIconTintList) {
|
|
if (this.errorIconTintList != errorIconTintList) {
|
|
this.errorIconTintList = errorIconTintList;
|
|
applyIconTint(textInputLayout, errorIconView, errorIconTintList, errorIconTintMode);
|
|
}
|
|
}
|
|
|
|
void setErrorIconTintMode(@Nullable PorterDuff.Mode errorIconTintMode) {
|
|
if (this.errorIconTintMode != errorIconTintMode) {
|
|
this.errorIconTintMode = errorIconTintMode;
|
|
applyIconTint(textInputLayout, errorIconView, errorIconTintList, this.errorIconTintMode);
|
|
}
|
|
}
|
|
|
|
void setErrorIconOnClickListener(@Nullable OnClickListener errorIconOnClickListener) {
|
|
setIconOnClickListener(errorIconView, errorIconOnClickListener, errorIconOnLongClickListener);
|
|
}
|
|
|
|
CheckableImageButton getEndIconView() {
|
|
return endIconView;
|
|
}
|
|
|
|
EndIconDelegate getEndIconDelegate() {
|
|
return endIconDelegates.get(endIconMode);
|
|
}
|
|
|
|
@EndIconMode
|
|
int getEndIconMode() {
|
|
return endIconMode;
|
|
}
|
|
|
|
void setEndIconMode(@EndIconMode int endIconMode) {
|
|
if (this.endIconMode == endIconMode) {
|
|
return;
|
|
}
|
|
tearDownDelegate(getEndIconDelegate());
|
|
int previousEndIconMode = this.endIconMode;
|
|
this.endIconMode = endIconMode;
|
|
dispatchOnEndIconChanged(previousEndIconMode);
|
|
setEndIconVisible(endIconMode != END_ICON_NONE);
|
|
EndIconDelegate delegate = getEndIconDelegate();
|
|
setEndIconDrawable(getIconResId(delegate));
|
|
setEndIconCheckable(delegate.isIconCheckable());
|
|
if (delegate.isBoxBackgroundModeSupported(textInputLayout.getBoxBackgroundMode())) {
|
|
setUpDelegate(delegate);
|
|
} else {
|
|
throw new IllegalStateException(
|
|
"The current box background mode "
|
|
+ textInputLayout.getBoxBackgroundMode()
|
|
+ " is not supported by the end icon mode "
|
|
+ endIconMode);
|
|
}
|
|
setEndIconOnClickListener(delegate.getOnIconClickListener());
|
|
setEndIconContentDescription(delegate.getIconContentDescriptionResId());
|
|
if (editText != null) {
|
|
delegate.onEditTextAttached(editText);
|
|
setOnFocusChangeListenersIfNeeded(delegate);
|
|
}
|
|
applyIconTint(textInputLayout, endIconView, endIconTintList, endIconTintMode);
|
|
refreshIconState(/* force= */ true);
|
|
}
|
|
|
|
void refreshIconState(boolean force) {
|
|
boolean stateChanged = false;
|
|
EndIconDelegate delegate = getEndIconDelegate();
|
|
if (delegate.isIconCheckable()) {
|
|
boolean wasChecked = endIconView.isChecked();
|
|
if (wasChecked != delegate.isIconChecked()) {
|
|
endIconView.setChecked(!wasChecked);
|
|
stateChanged = true;
|
|
}
|
|
}
|
|
if (delegate.isIconActivable()) {
|
|
boolean wasActivated = endIconView.isActivated();
|
|
if (wasActivated != delegate.isIconActivated()) {
|
|
setEndIconActivated(!wasActivated);
|
|
stateChanged = true;
|
|
}
|
|
}
|
|
if (force || stateChanged) {
|
|
refreshEndIconDrawableState();
|
|
}
|
|
}
|
|
|
|
private void setUpDelegate(@NonNull EndIconDelegate delegate) {
|
|
delegate.setUp();
|
|
|
|
touchExplorationStateChangeListener = delegate.getTouchExplorationStateChangeListener();
|
|
addTouchExplorationStateChangeListenerIfNeeded();
|
|
}
|
|
|
|
private void tearDownDelegate(@NonNull EndIconDelegate delegate) {
|
|
removeTouchExplorationStateChangeListenerIfNeeded();
|
|
touchExplorationStateChangeListener = null;
|
|
delegate.tearDown();
|
|
}
|
|
|
|
private void addTouchExplorationStateChangeListenerIfNeeded() {
|
|
if (touchExplorationStateChangeListener != null
|
|
&& accessibilityManager != null
|
|
&& isAttachedToWindow()) {
|
|
accessibilityManager.addTouchExplorationStateChangeListener(
|
|
touchExplorationStateChangeListener);
|
|
}
|
|
}
|
|
|
|
private void removeTouchExplorationStateChangeListenerIfNeeded() {
|
|
if (touchExplorationStateChangeListener != null && accessibilityManager != null) {
|
|
accessibilityManager.removeTouchExplorationStateChangeListener(
|
|
touchExplorationStateChangeListener);
|
|
}
|
|
}
|
|
|
|
private int getIconResId(EndIconDelegate delegate) {
|
|
int customIconResId = endIconDelegates.customEndIconDrawableId;
|
|
return customIconResId == 0 ? delegate.getIconDrawableResId() : customIconResId;
|
|
}
|
|
|
|
void setEndIconOnClickListener(@Nullable OnClickListener endIconOnClickListener) {
|
|
setIconOnClickListener(endIconView, endIconOnClickListener, endIconOnLongClickListener);
|
|
}
|
|
|
|
void setEndIconOnLongClickListener(
|
|
@Nullable OnLongClickListener endIconOnLongClickListener) {
|
|
this.endIconOnLongClickListener = endIconOnLongClickListener;
|
|
setIconOnLongClickListener(endIconView, endIconOnLongClickListener);
|
|
}
|
|
|
|
void setErrorIconOnLongClickListener(
|
|
@Nullable OnLongClickListener errorIconOnLongClickListener) {
|
|
this.errorIconOnLongClickListener = errorIconOnLongClickListener;
|
|
setIconOnLongClickListener(errorIconView, errorIconOnLongClickListener);
|
|
}
|
|
|
|
private void setOnFocusChangeListenersIfNeeded(EndIconDelegate delegate) {
|
|
if (editText == null) {
|
|
return;
|
|
}
|
|
if (delegate.getOnEditTextFocusChangeListener() != null) {
|
|
editText.setOnFocusChangeListener(delegate.getOnEditTextFocusChangeListener());
|
|
}
|
|
if (delegate.getOnIconViewFocusChangeListener() != null) {
|
|
endIconView.setOnFocusChangeListener(delegate.getOnIconViewFocusChangeListener());
|
|
}
|
|
}
|
|
|
|
void refreshErrorIconDrawableState() {
|
|
refreshIconDrawableState(textInputLayout, errorIconView, errorIconTintList);
|
|
}
|
|
|
|
void setEndIconVisible(boolean visible) {
|
|
if (isEndIconVisible() != visible) {
|
|
endIconView.setVisibility(visible ? View.VISIBLE : View.GONE);
|
|
updateEndLayoutVisibility();
|
|
updateSuffixTextViewPadding();
|
|
textInputLayout.updateDummyDrawables();
|
|
}
|
|
}
|
|
|
|
boolean isEndIconVisible() {
|
|
return endIconFrame.getVisibility() == VISIBLE && endIconView.getVisibility() == VISIBLE;
|
|
}
|
|
|
|
void setEndIconActivated(boolean endIconActivated) {
|
|
endIconView.setActivated(endIconActivated);
|
|
}
|
|
|
|
void refreshEndIconDrawableState() {
|
|
refreshIconDrawableState(textInputLayout, endIconView, endIconTintList);
|
|
}
|
|
|
|
void setEndIconCheckable(boolean endIconCheckable) {
|
|
endIconView.setCheckable(endIconCheckable);
|
|
}
|
|
|
|
boolean isEndIconCheckable() {
|
|
return endIconView.isCheckable();
|
|
}
|
|
|
|
boolean isEndIconChecked() {
|
|
return hasEndIcon() && endIconView.isChecked();
|
|
}
|
|
|
|
void checkEndIcon() {
|
|
endIconView.performClick();
|
|
// Skip animation
|
|
endIconView.jumpDrawablesToCurrentState();
|
|
}
|
|
|
|
void setEndIconDrawable(@DrawableRes int resId) {
|
|
setEndIconDrawable(resId != 0 ? AppCompatResources.getDrawable(getContext(), resId) : null);
|
|
}
|
|
|
|
void setEndIconDrawable(@Nullable Drawable endIconDrawable) {
|
|
endIconView.setImageDrawable(endIconDrawable);
|
|
if (endIconDrawable != null) {
|
|
applyIconTint(textInputLayout, endIconView, endIconTintList, endIconTintMode);
|
|
refreshEndIconDrawableState();
|
|
}
|
|
}
|
|
|
|
@Nullable
|
|
Drawable getEndIconDrawable() {
|
|
return endIconView.getDrawable();
|
|
}
|
|
|
|
void setEndIconContentDescription(@StringRes int resId) {
|
|
setEndIconContentDescription(resId != 0 ? getResources().getText(resId) : null);
|
|
}
|
|
|
|
void setEndIconContentDescription(@Nullable CharSequence endIconContentDescription) {
|
|
if (getEndIconContentDescription() != endIconContentDescription) {
|
|
endIconView.setContentDescription(endIconContentDescription);
|
|
updateIconTooltip(endIconView, endIconOnLongClickListener, endIconContentDescription);
|
|
}
|
|
}
|
|
|
|
@Nullable
|
|
CharSequence getEndIconContentDescription() {
|
|
return endIconView.getContentDescription();
|
|
}
|
|
|
|
void setEndIconTintList(@Nullable ColorStateList endIconTintList) {
|
|
if (this.endIconTintList != endIconTintList) {
|
|
this.endIconTintList = endIconTintList;
|
|
applyIconTint(textInputLayout, endIconView, this.endIconTintList, endIconTintMode);
|
|
}
|
|
}
|
|
|
|
void setEndIconTintMode(@Nullable PorterDuff.Mode endIconTintMode) {
|
|
if (this.endIconTintMode != endIconTintMode) {
|
|
this.endIconTintMode = endIconTintMode;
|
|
applyIconTint(textInputLayout, endIconView, endIconTintList, this.endIconTintMode);
|
|
}
|
|
}
|
|
|
|
void setEndIconMinSize(@Px int iconSize) {
|
|
if (iconSize < 0) {
|
|
throw new IllegalArgumentException("endIconSize cannot be less than 0");
|
|
}
|
|
if (iconSize != endIconMinSize) {
|
|
endIconMinSize = iconSize;
|
|
setIconMinSize(endIconView, iconSize);
|
|
setIconMinSize(errorIconView, iconSize);
|
|
}
|
|
}
|
|
|
|
int getEndIconMinSize() {
|
|
return endIconMinSize;
|
|
}
|
|
|
|
void setEndIconScaleType(@NonNull ScaleType endIconScaleType) {
|
|
this.endIconScaleType = endIconScaleType;
|
|
setIconScaleType(endIconView, endIconScaleType);
|
|
setIconScaleType(errorIconView, endIconScaleType);
|
|
}
|
|
|
|
@NonNull ScaleType getEndIconScaleType() {
|
|
return endIconScaleType;
|
|
}
|
|
|
|
void addOnEndIconChangedListener(@NonNull OnEndIconChangedListener listener) {
|
|
endIconChangedListeners.add(listener);
|
|
}
|
|
|
|
void removeOnEndIconChangedListener(@NonNull OnEndIconChangedListener listener) {
|
|
endIconChangedListeners.remove(listener);
|
|
}
|
|
|
|
void clearOnEndIconChangedListeners() {
|
|
endIconChangedListeners.clear();
|
|
}
|
|
|
|
boolean hasEndIcon() {
|
|
return endIconMode != END_ICON_NONE;
|
|
}
|
|
|
|
TextView getSuffixTextView() {
|
|
return suffixTextView;
|
|
}
|
|
|
|
void setSuffixText(@Nullable CharSequence suffixText) {
|
|
this.suffixText = TextUtils.isEmpty(suffixText) ? null : suffixText;
|
|
suffixTextView.setText(suffixText);
|
|
updateSuffixTextVisibility();
|
|
}
|
|
|
|
@Nullable
|
|
CharSequence getSuffixText() {
|
|
return suffixText;
|
|
}
|
|
|
|
void setSuffixTextAppearance(@StyleRes int suffixTextAppearance) {
|
|
TextViewCompat.setTextAppearance(suffixTextView, suffixTextAppearance);
|
|
}
|
|
|
|
void setSuffixTextColor(@NonNull ColorStateList suffixTextColor) {
|
|
suffixTextView.setTextColor(suffixTextColor);
|
|
}
|
|
|
|
@Nullable
|
|
ColorStateList getSuffixTextColor() {
|
|
return suffixTextView.getTextColors();
|
|
}
|
|
|
|
void setPasswordVisibilityToggleDrawable(@DrawableRes int resId) {
|
|
setPasswordVisibilityToggleDrawable(
|
|
resId != 0 ? AppCompatResources.getDrawable(getContext(), resId) : null);
|
|
}
|
|
|
|
void setPasswordVisibilityToggleDrawable(@Nullable Drawable icon) {
|
|
endIconView.setImageDrawable(icon);
|
|
}
|
|
|
|
void setPasswordVisibilityToggleContentDescription(@StringRes int resId) {
|
|
setPasswordVisibilityToggleContentDescription(
|
|
resId != 0 ? getResources().getText(resId) : null);
|
|
}
|
|
|
|
void setPasswordVisibilityToggleContentDescription(@Nullable CharSequence description) {
|
|
endIconView.setContentDescription(description);
|
|
}
|
|
|
|
@Nullable
|
|
Drawable getPasswordVisibilityToggleDrawable() {
|
|
return endIconView.getDrawable();
|
|
}
|
|
|
|
@Nullable
|
|
CharSequence getPasswordVisibilityToggleContentDescription() {
|
|
return endIconView.getContentDescription();
|
|
}
|
|
|
|
boolean isPasswordVisibilityToggleEnabled() {
|
|
return endIconMode == END_ICON_PASSWORD_TOGGLE;
|
|
}
|
|
|
|
void setPasswordVisibilityToggleEnabled(boolean enabled) {
|
|
if (enabled && endIconMode != END_ICON_PASSWORD_TOGGLE) {
|
|
// Set password toggle end icon if it's not already set
|
|
setEndIconMode(END_ICON_PASSWORD_TOGGLE);
|
|
} else if (!enabled) {
|
|
// Set end icon to null
|
|
setEndIconMode(END_ICON_NONE);
|
|
}
|
|
}
|
|
|
|
void setPasswordVisibilityToggleTintList(@Nullable ColorStateList tintList) {
|
|
endIconTintList = tintList;
|
|
applyIconTint(textInputLayout, endIconView, endIconTintList, endIconTintMode);
|
|
}
|
|
|
|
void setPasswordVisibilityToggleTintMode(@Nullable PorterDuff.Mode mode) {
|
|
endIconTintMode = mode;
|
|
applyIconTint(textInputLayout, endIconView, endIconTintList, endIconTintMode);
|
|
}
|
|
|
|
void togglePasswordVisibilityToggle(boolean shouldSkipAnimations) {
|
|
if (endIconMode == END_ICON_PASSWORD_TOGGLE) {
|
|
endIconView.performClick();
|
|
if (shouldSkipAnimations) {
|
|
endIconView.jumpDrawablesToCurrentState();
|
|
}
|
|
}
|
|
}
|
|
|
|
void onHintStateChanged(boolean hintExpanded) {
|
|
this.hintExpanded = hintExpanded;
|
|
updateSuffixTextVisibility();
|
|
}
|
|
|
|
void onTextInputBoxStateUpdated() {
|
|
updateErrorIconVisibility();
|
|
|
|
// Update icons tints
|
|
refreshErrorIconDrawableState();
|
|
refreshEndIconDrawableState();
|
|
|
|
if (getEndIconDelegate().shouldTintIconOnError()) {
|
|
tintEndIconOnError(textInputLayout.shouldShowError());
|
|
}
|
|
}
|
|
|
|
private void updateSuffixTextVisibility() {
|
|
int oldVisibility = suffixTextView.getVisibility();
|
|
int newVisibility = (suffixText != null && !hintExpanded) ? VISIBLE : GONE;
|
|
if (oldVisibility != newVisibility) {
|
|
getEndIconDelegate().onSuffixVisibilityChanged(/* visible= */ newVisibility == VISIBLE);
|
|
}
|
|
updateEndLayoutVisibility();
|
|
// Set visibility after updating end layout's visibility so screen readers correctly announce
|
|
// when suffix text appears.
|
|
suffixTextView.setVisibility(newVisibility);
|
|
textInputLayout.updateDummyDrawables();
|
|
}
|
|
|
|
void updateSuffixTextViewPadding() {
|
|
if (textInputLayout.editText == null) {
|
|
return;
|
|
}
|
|
int endPadding =
|
|
(isEndIconVisible() || isErrorIconVisible()) ? 0 : textInputLayout.editText.getPaddingEnd();
|
|
suffixTextView.setPaddingRelative(
|
|
getContext()
|
|
.getResources()
|
|
.getDimensionPixelSize(R.dimen.material_input_text_to_prefix_suffix_padding),
|
|
textInputLayout.editText.getPaddingTop(),
|
|
endPadding,
|
|
textInputLayout.editText.getPaddingBottom());
|
|
}
|
|
|
|
int getSuffixTextEndOffset() {
|
|
int endIconOffset;
|
|
if (isEndIconVisible() || isErrorIconVisible()) {
|
|
endIconOffset =
|
|
endIconView.getMeasuredWidth()
|
|
+ ((MarginLayoutParams) endIconView.getLayoutParams()).getMarginStart();
|
|
} else {
|
|
endIconOffset = 0;
|
|
}
|
|
return getPaddingEnd()
|
|
+ suffixTextView.getPaddingEnd()
|
|
+ endIconOffset;
|
|
}
|
|
|
|
@Nullable
|
|
CheckableImageButton getCurrentEndIconView() {
|
|
if (isErrorIconVisible()) {
|
|
return errorIconView;
|
|
} else if (hasEndIcon() && isEndIconVisible()) {
|
|
return endIconView;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
boolean isErrorIconVisible() {
|
|
return errorIconView.getVisibility() == VISIBLE;
|
|
}
|
|
|
|
private void updateErrorIconVisibility() {
|
|
boolean visible =
|
|
getErrorIconDrawable() != null
|
|
&& textInputLayout.isErrorEnabled()
|
|
&& textInputLayout.shouldShowError();
|
|
errorIconView.setVisibility(visible ? VISIBLE : GONE);
|
|
updateEndLayoutVisibility();
|
|
updateSuffixTextViewPadding();
|
|
if (!hasEndIcon()) {
|
|
textInputLayout.updateDummyDrawables();
|
|
}
|
|
}
|
|
|
|
private void updateEndLayoutVisibility() {
|
|
// Sync endIconFrame's visibility with the endIconView's.
|
|
endIconFrame.setVisibility(
|
|
(endIconView.getVisibility() == VISIBLE && !isErrorIconVisible()) ? VISIBLE : GONE);
|
|
|
|
int suffixTextVisibility = (suffixText != null && !hintExpanded) ? VISIBLE : GONE;
|
|
boolean shouldBeVisible =
|
|
isEndIconVisible() || isErrorIconVisible() || suffixTextVisibility == VISIBLE;
|
|
setVisibility(shouldBeVisible ? VISIBLE : GONE);
|
|
}
|
|
|
|
private void dispatchOnEndIconChanged(@EndIconMode int previousIcon) {
|
|
for (OnEndIconChangedListener listener : endIconChangedListeners) {
|
|
listener.onEndIconChanged(textInputLayout, previousIcon);
|
|
}
|
|
}
|
|
|
|
private void tintEndIconOnError(boolean tintEndIconOnError) {
|
|
if (tintEndIconOnError && getEndIconDrawable() != null) {
|
|
// Setting the tint here instead of calling setEndIconTintList() in order to preserve and
|
|
// restore the icon's original tint.
|
|
Drawable endIconDrawable = DrawableCompat.wrap(getEndIconDrawable()).mutate();
|
|
endIconDrawable.setTint(textInputLayout.getErrorCurrentTextColors());
|
|
endIconView.setImageDrawable(endIconDrawable);
|
|
} else {
|
|
applyIconTint(textInputLayout, endIconView, endIconTintList, endIconTintMode);
|
|
}
|
|
}
|
|
|
|
private static class EndIconDelegates {
|
|
private final SparseArray<EndIconDelegate> delegates = new SparseArray<>();
|
|
|
|
private final EndCompoundLayout endLayout;
|
|
private final int customEndIconDrawableId;
|
|
private final int passwordIconDrawableId;
|
|
|
|
EndIconDelegates(EndCompoundLayout endLayout, TintTypedArray a) {
|
|
this.endLayout = endLayout;
|
|
customEndIconDrawableId = a.getResourceId(R.styleable.TextInputLayout_endIconDrawable, 0);
|
|
passwordIconDrawableId =
|
|
a.getResourceId(R.styleable.TextInputLayout_passwordToggleDrawable, 0);
|
|
}
|
|
|
|
EndIconDelegate get(@EndIconMode int endIconMode) {
|
|
EndIconDelegate delegate = delegates.get(endIconMode);
|
|
if (delegate == null) {
|
|
delegate = create(endIconMode);
|
|
delegates.append(endIconMode, delegate);
|
|
}
|
|
return delegate;
|
|
}
|
|
|
|
private EndIconDelegate create(@EndIconMode int endIconMode) {
|
|
switch (endIconMode) {
|
|
case END_ICON_PASSWORD_TOGGLE:
|
|
return new PasswordToggleEndIconDelegate(endLayout, passwordIconDrawableId);
|
|
case END_ICON_CLEAR_TEXT:
|
|
return new ClearTextEndIconDelegate(endLayout);
|
|
case END_ICON_DROPDOWN_MENU:
|
|
return new DropdownMenuEndIconDelegate(endLayout);
|
|
case END_ICON_CUSTOM:
|
|
return new CustomEndIconDelegate(endLayout);
|
|
case END_ICON_NONE:
|
|
return new NoEndIconDelegate(endLayout);
|
|
default:
|
|
throw new IllegalArgumentException("Invalid end icon mode: " + endIconMode);
|
|
}
|
|
}
|
|
}
|
|
}
|