mirror of
https://github.com/material-components/material-components-android.git
synced 2026-01-16 01:42:16 +08:00
Resolves https://github.com/material-components/material-components-android/pull/4289 GIT_ORIGIN_REV_ID=6686c2a3101c235c1fec087a0f77acbfecd23540 PiperOrigin-RevId: 718390534
337 lines
11 KiB
Java
337 lines
11 KiB
Java
/*
|
|
* Copyright (C) 2019 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 android.view.accessibility.AccessibilityEvent.TYPE_VIEW_CLICKED;
|
|
import static com.google.android.material.textfield.EditTextUtils.isEditable;
|
|
|
|
import android.animation.Animator;
|
|
import android.animation.AnimatorListenerAdapter;
|
|
import android.animation.TimeInterpolator;
|
|
import android.animation.ValueAnimator;
|
|
import android.annotation.SuppressLint;
|
|
import android.content.Context;
|
|
import android.os.SystemClock;
|
|
import android.text.Editable;
|
|
import android.view.MotionEvent;
|
|
import android.view.View;
|
|
import android.view.View.OnClickListener;
|
|
import android.view.View.OnFocusChangeListener;
|
|
import android.view.accessibility.AccessibilityEvent;
|
|
import android.view.accessibility.AccessibilityManager;
|
|
import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener;
|
|
import android.widget.AutoCompleteTextView;
|
|
import android.widget.EditText;
|
|
import android.widget.Spinner;
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.core.view.accessibility.AccessibilityEventCompat;
|
|
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
|
|
import com.google.android.material.animation.AnimationUtils;
|
|
import com.google.android.material.motion.MotionUtils;
|
|
import com.google.android.material.textfield.TextInputLayout.BoxBackgroundMode;
|
|
|
|
/** Default initialization of the exposed dropdown menu {@link TextInputLayout.EndIconMode}. */
|
|
class DropdownMenuEndIconDelegate extends EndIconDelegate {
|
|
|
|
private static final int DEFAULT_ANIMATION_FADE_OUT_DURATION = 50;
|
|
private static final int DEFAULT_ANIMATION_FADE_IN_DURATION = 67;
|
|
private final int animationFadeOutDuration;
|
|
private final int animationFadeInDuration;
|
|
@NonNull private final TimeInterpolator animationFadeInterpolator;
|
|
|
|
@Nullable
|
|
private AutoCompleteTextView autoCompleteTextView;
|
|
|
|
private final OnClickListener onIconClickListener = view -> showHideDropdown();
|
|
|
|
private final OnFocusChangeListener onEditTextFocusChangeListener = (view, hasFocus) -> {
|
|
editTextHasFocus = hasFocus;
|
|
refreshIconState();
|
|
if (!hasFocus) {
|
|
setEndIconChecked(false);
|
|
dropdownPopupDirty = false;
|
|
}
|
|
};
|
|
|
|
private final TouchExplorationStateChangeListener touchExplorationStateChangeListener =
|
|
(boolean enabled) -> {
|
|
if (autoCompleteTextView != null && !isEditable(autoCompleteTextView)) {
|
|
endIconView.setImportantForAccessibility(
|
|
enabled ? View.IMPORTANT_FOR_ACCESSIBILITY_NO : View.IMPORTANT_FOR_ACCESSIBILITY_YES);
|
|
}
|
|
};
|
|
|
|
private boolean editTextHasFocus;
|
|
private boolean dropdownPopupDirty;
|
|
private boolean isEndIconChecked;
|
|
private long dropdownPopupActivatedAt = Long.MAX_VALUE;
|
|
@Nullable private AccessibilityManager accessibilityManager;
|
|
private ValueAnimator fadeOutAnim;
|
|
private ValueAnimator fadeInAnim;
|
|
|
|
DropdownMenuEndIconDelegate(@NonNull EndCompoundLayout endLayout) {
|
|
super(endLayout);
|
|
animationFadeInDuration =
|
|
MotionUtils.resolveThemeDuration(
|
|
endLayout.getContext(),
|
|
R.attr.motionDurationShort3,
|
|
DEFAULT_ANIMATION_FADE_IN_DURATION);
|
|
animationFadeOutDuration =
|
|
MotionUtils.resolveThemeDuration(
|
|
endLayout.getContext(),
|
|
R.attr.motionDurationShort3,
|
|
DEFAULT_ANIMATION_FADE_OUT_DURATION);
|
|
animationFadeInterpolator =
|
|
MotionUtils.resolveThemeInterpolator(
|
|
endLayout.getContext(),
|
|
R.attr.motionEasingLinearInterpolator,
|
|
AnimationUtils.LINEAR_INTERPOLATOR);
|
|
}
|
|
|
|
@Override
|
|
void setUp() {
|
|
initAnimators();
|
|
accessibilityManager =
|
|
(AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
|
|
}
|
|
|
|
@SuppressLint("ClickableViewAccessibility") // There's an accessibility delegate that handles
|
|
// interactions with the dropdown menu.
|
|
@Override
|
|
void tearDown() {
|
|
if (autoCompleteTextView != null) {
|
|
// Remove any listeners set on the edit text.
|
|
autoCompleteTextView.setOnTouchListener(null);
|
|
autoCompleteTextView.setOnDismissListener(null);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public TouchExplorationStateChangeListener getTouchExplorationStateChangeListener() {
|
|
return touchExplorationStateChangeListener;
|
|
}
|
|
|
|
@Override
|
|
int getIconDrawableResId() {
|
|
return R.drawable.mtrl_dropdown_arrow;
|
|
}
|
|
|
|
@Override
|
|
int getIconContentDescriptionResId() {
|
|
return R.string.exposed_dropdown_menu_content_description;
|
|
}
|
|
|
|
@Override
|
|
boolean isIconCheckable() {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
boolean isIconChecked() {
|
|
return isEndIconChecked;
|
|
}
|
|
|
|
@Override
|
|
boolean isIconActivable() {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
boolean isIconActivated() {
|
|
return editTextHasFocus;
|
|
}
|
|
|
|
@Override
|
|
boolean shouldTintIconOnError() {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
boolean isBoxBackgroundModeSupported(@BoxBackgroundMode int boxBackgroundMode) {
|
|
return boxBackgroundMode != TextInputLayout.BOX_BACKGROUND_NONE;
|
|
}
|
|
|
|
@Override
|
|
OnClickListener getOnIconClickListener() {
|
|
return onIconClickListener;
|
|
}
|
|
|
|
@Override
|
|
public void onEditTextAttached(@Nullable EditText editText) {
|
|
this.autoCompleteTextView = castAutoCompleteTextViewOrThrow(editText);
|
|
setUpDropdownShowHideBehavior();
|
|
textInputLayout.setErrorIconDrawable(null);
|
|
if (!isEditable(editText) && accessibilityManager.isTouchExplorationEnabled()) {
|
|
endIconView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
|
}
|
|
textInputLayout.setEndIconVisible(true);
|
|
}
|
|
|
|
@Override
|
|
public void afterEditTextChanged(Editable s) {
|
|
// Don't show dropdown list if we're in a11y mode and the menu is editable.
|
|
if (accessibilityManager.isTouchExplorationEnabled()
|
|
&& isEditable(autoCompleteTextView)
|
|
&& !endIconView.hasFocus()) {
|
|
autoCompleteTextView.dismissDropDown();
|
|
}
|
|
autoCompleteTextView.post(() -> {
|
|
boolean isPopupShowing = autoCompleteTextView.isPopupShowing();
|
|
setEndIconChecked(isPopupShowing);
|
|
dropdownPopupDirty = isPopupShowing;
|
|
});
|
|
}
|
|
|
|
@Override
|
|
OnFocusChangeListener getOnEditTextFocusChangeListener() {
|
|
return onEditTextFocusChangeListener;
|
|
}
|
|
|
|
@Override
|
|
public void onInitializeAccessibilityNodeInfo(
|
|
View host, @NonNull AccessibilityNodeInfoCompat info) {
|
|
// The non-editable exposed dropdown menu behaves like a Spinner.
|
|
if (!isEditable(autoCompleteTextView)) {
|
|
info.setClassName(Spinner.class.getName());
|
|
}
|
|
if (info.isShowingHintText()) {
|
|
// Set hint text to null so TalkBack doesn't announce the label twice when there is no
|
|
// item selected.
|
|
info.setHintText(null);
|
|
}
|
|
}
|
|
|
|
@SuppressLint("WrongConstant")
|
|
@Override
|
|
public void onPopulateAccessibilityEvent(View host, @NonNull AccessibilityEvent event) {
|
|
if (!accessibilityManager.isEnabled() || isEditable(autoCompleteTextView)) {
|
|
return;
|
|
}
|
|
// TODO(b/256138189): Find better workaround, back gesture should call
|
|
// AutoCompleteTextView.OnDismissListener.
|
|
boolean invalidState =
|
|
(event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED
|
|
|| event.getEventType() == AccessibilityEventCompat.CONTENT_CHANGE_TYPE_PANE_TITLE)
|
|
&& isEndIconChecked
|
|
&& !autoCompleteTextView.isPopupShowing();
|
|
// If dropdown is non editable, layout click is what triggers showing/hiding the popup
|
|
// list. Otherwise, arrow icon alone is what triggers it.
|
|
if (event.getEventType() == TYPE_VIEW_CLICKED || invalidState) {
|
|
showHideDropdown();
|
|
updateDropdownPopupDirty();
|
|
}
|
|
}
|
|
|
|
private void showHideDropdown() {
|
|
if (autoCompleteTextView == null) {
|
|
return;
|
|
}
|
|
if (isDropdownPopupActive()) {
|
|
dropdownPopupDirty = false;
|
|
}
|
|
if (!dropdownPopupDirty) {
|
|
setEndIconChecked(!isEndIconChecked);
|
|
if (isEndIconChecked) {
|
|
autoCompleteTextView.requestFocus();
|
|
autoCompleteTextView.showDropDown();
|
|
} else {
|
|
autoCompleteTextView.dismissDropDown();
|
|
}
|
|
} else {
|
|
dropdownPopupDirty = false;
|
|
}
|
|
}
|
|
|
|
@SuppressLint("ClickableViewAccessibility") // There's an accessibility delegate that handles
|
|
// interactions with the dropdown menu.
|
|
private void setUpDropdownShowHideBehavior() {
|
|
// Set whole layout clickable.
|
|
autoCompleteTextView.setOnTouchListener((view, event) -> {
|
|
if (event.getAction() == MotionEvent.ACTION_UP) {
|
|
if (isDropdownPopupActive()) {
|
|
dropdownPopupDirty = false;
|
|
}
|
|
showHideDropdown();
|
|
updateDropdownPopupDirty();
|
|
}
|
|
return false;
|
|
});
|
|
autoCompleteTextView.setOnDismissListener(() -> {
|
|
updateDropdownPopupDirty();
|
|
setEndIconChecked(false);
|
|
});
|
|
autoCompleteTextView.setThreshold(0);
|
|
}
|
|
|
|
private boolean isDropdownPopupActive() {
|
|
long activeFor = SystemClock.uptimeMillis() - dropdownPopupActivatedAt;
|
|
return activeFor < 0 || activeFor > 300;
|
|
}
|
|
|
|
@NonNull
|
|
private static AutoCompleteTextView castAutoCompleteTextViewOrThrow(EditText editText) {
|
|
if (!(editText instanceof AutoCompleteTextView)) {
|
|
throw new RuntimeException(
|
|
"EditText needs to be an AutoCompleteTextView if an Exposed Dropdown Menu is being"
|
|
+ " used.");
|
|
}
|
|
|
|
return (AutoCompleteTextView) editText;
|
|
}
|
|
|
|
private void updateDropdownPopupDirty() {
|
|
dropdownPopupDirty = true;
|
|
dropdownPopupActivatedAt = SystemClock.uptimeMillis();
|
|
}
|
|
|
|
private void setEndIconChecked(boolean checked) {
|
|
if (isEndIconChecked != checked) {
|
|
isEndIconChecked = checked;
|
|
fadeInAnim.cancel();
|
|
fadeOutAnim.start();
|
|
}
|
|
}
|
|
|
|
private void initAnimators() {
|
|
fadeInAnim = getAlphaAnimator(animationFadeInDuration, 0, 1);
|
|
fadeOutAnim = getAlphaAnimator(animationFadeOutDuration, 1, 0);
|
|
fadeOutAnim.addListener(
|
|
new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
refreshIconState();
|
|
fadeInAnim.start();
|
|
}
|
|
});
|
|
}
|
|
|
|
private ValueAnimator getAlphaAnimator(int duration, float... values) {
|
|
ValueAnimator animator = ValueAnimator.ofFloat(values);
|
|
animator.setInterpolator(animationFadeInterpolator);
|
|
animator.setDuration(duration);
|
|
animator.addUpdateListener(animation -> {
|
|
float alpha = (float) animation.getAnimatedValue();
|
|
endIconView.setAlpha(alpha);
|
|
});
|
|
|
|
return animator;
|
|
}
|
|
}
|