mirror of
https://github.com/material-components/material-components-android.git
synced 2026-01-17 10:21:51 +08:00
On API 21 & 22, the borderless ripple will go behind the container view under certain conditions. Setting a mask when creating the ripple drawable to restrain the ripple inside view bounds somehow solves the issue. PiperOrigin-RevId: 445920587
308 lines
11 KiB
Java
308 lines
11 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.refreshIconDrawableState;
|
|
import static com.google.android.material.textfield.IconHelper.setCompatRippleBackgroundIfNeeded;
|
|
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);
|
|
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));
|
|
}
|
|
}
|
|
|
|
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 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();
|
|
}
|
|
}
|