Skip to content

Commit

Permalink
[ExposedDropdownMenu] Added support for default/ripple background col…
Browse files Browse the repository at this point in the history
…ors for the selected item of the exposed dropdown menu when the default MaterialAutoCompleteArrayAdapter is being used.

PiperOrigin-RevId: 447531152
  • Loading branch information
leticiarossi committed May 9, 2022
1 parent 3fc53ac commit 6206ff5
Show file tree
Hide file tree
Showing 12 changed files with 278 additions and 23 deletions.
28 changes: 15 additions & 13 deletions docs/components/Menu.md
Expand Up @@ -562,19 +562,21 @@ The exposed dropdown menu is an `AutoCompleteTextView` within a
For all attributes that apply to the `TextInputLayout`, see the
[TextInputLayout documentation](TextField.md).

#### `AutoCompleteTextView` attributes (input text, dropdown menu)

Element | Attribute | Related method(s) | Default value
------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------ | -------------
**Input text** | `android:text` | `setText`<br/>`getText` | `@null`
**Typography** | `android:textAppearance` | `setTextAppearance` | `?attr/textAppearanceBodyLarge`
**Input accepted** | `android:inputType` | `N/A` | framework's default
**Input text color** | `android:textColor` | `setTextColor`<br/>`getTextColors`<br/>`getCurrentTextColor` | `?android:textColorPrimary`
**Cursor color** | N/A (color comes from the theme attr `?attr/colorControlActivated`) | N/A | `?attr/colorPrimary`
**Dropdown menu<br/>container color** | N/A | N/A | `?attr/colorSurface`
**Dropdown menu elevation** | `android:popupElevation` | `getPopupElevation` | `3dp`
**Simple items** | `app:simpleItems` | `setSimpleItems` | `null`
**Simple item layout** | `app:simpleItemLayout` | N/A | `@layout/m3_auto_complete_simple_item`
#### `MaterialAutoCompleteTextView` attributes (input text, dropdown menu)

Element | Attribute | Related method(s) | Default value
----------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------- | -------------
**Input text** | `android:text` | `setText`<br/>`getText` | `@null`
**Typography** | `android:textAppearance` | `setTextAppearance` | `?attr/textAppearanceBodyLarge`
**Input accepted** | `android:inputType` | `N/A` | framework's default
**Input text color** | `android:textColor` | `setTextColor`<br/>`getTextColors`<br/>`getCurrentTextColor` | `?android:textColorPrimary`
**Cursor color** | N/A (color comes from the theme attr `?attr/colorControlActivated`) | N/A | `?attr/colorPrimary`
**Dropdown menu<br/>container color** | N/A | N/A | `?attr/colorSurface`
**Dropdown menu elevation** | `android:popupElevation` | `getPopupElevation` | `3dp`
**Simple items** | `app:simpleItems` | `setSimpleItems` | `null`
**Simple item layout** | `app:simpleItemLayout` | N/A | `@layout/m3_auto_complete_simple_item`
**Selected simple item color** | `app:simpleItemSelectedColor` | `setSimpleItemSelectedColor`<br/>`getSimpleItemSelectedColor` | `?attr/colorSurfaceVariant`
**Selected simple item<br/>ripple color** | `app:simpleItemSelectedRippleColor` | `setSimpleItemSelectedRippleColor`<br/>`getSimpleItemSelectedRippleColor` | `@color/m3_simple_item_ripple_color`

#### Styles

Expand Down
2 changes: 1 addition & 1 deletion docs/components/TextField.md
Expand Up @@ -206,7 +206,7 @@ See the full list of

### Implementing an exposed dropdown menu

!["Text field with an exposed dropdown menu."](assets/textfields/textfields_exposed_dropdown_menu.png)
!["Text field with an exposed dropdown menu."](assets/menu/menus_exposed_dropdown_outlined.png)

In the layout:

Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/components/assets/menu/menus_exposed_dropdown_outlined.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Expand Up @@ -21,15 +21,21 @@
import static com.google.android.material.theme.overlay.MaterialThemeOverlay.wrap;

import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.RippleDrawable;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import androidx.appcompat.widget.AppCompatAutoCompleteTextView;
import androidx.appcompat.widget.ListPopupWindow;
import android.text.InputType;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityManager;
Expand All @@ -38,23 +44,28 @@
import android.widget.ArrayAdapter;
import android.widget.Filterable;
import android.widget.ListAdapter;
import android.widget.TextView;
import androidx.annotation.ArrayRes;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.core.view.ViewCompat;
import com.google.android.material.color.MaterialColors;
import com.google.android.material.internal.ManufacturerUtils;
import com.google.android.material.internal.ThemeEnforcement;
import com.google.android.material.resources.MaterialResources;

/**
* A special sub-class of {@link android.widget.AutoCompleteTextView} that is auto-inflated so that
* auto-complete text fields (e.g., for an Exposed Dropdown Menu) are accessible when being
* interacted through a screen reader.
*
* <p>The {@link ListPopupWindow} of the {@link android.widget.AutoCompleteTextView} is not modal,
* so it does not grab accessibility focus. The {@link MaterialAutoCompleteTextView} changes that
* by having a modal {@link ListPopupWindow} that is displayed instead of the non-modal one, so that
* the first item of the popup is automatically focused. This simulates the behavior of the
* {@link android.widget.Spinner}.
* so it does not grab accessibility focus. The {@link MaterialAutoCompleteTextView} changes that by
* having a modal {@link ListPopupWindow} that is displayed instead of the non-modal one, so that
* the first item of the popup is automatically focused. This simulates the behavior of the {@link
* android.widget.Spinner}.
*/
public class MaterialAutoCompleteTextView extends AppCompatAutoCompleteTextView {

Expand All @@ -65,6 +76,8 @@ public class MaterialAutoCompleteTextView extends AppCompatAutoCompleteTextView
@NonNull private final Rect tempRect = new Rect();
@LayoutRes private final int simpleItemLayout;
private final float popupElevation;
private int simpleItemSelectedColor;
@Nullable private ColorStateList simpleItemSelectedRippleColor;

public MaterialAutoCompleteTextView(@NonNull Context context) {
this(context, null);
Expand Down Expand Up @@ -100,15 +113,24 @@ public MaterialAutoCompleteTextView(
}
}

simpleItemLayout = attributes.getResourceId(
R.styleable.MaterialAutoCompleteTextView_simpleItemLayout,
R.layout.mtrl_auto_complete_simple_item);

simpleItemLayout =
attributes.getResourceId(
R.styleable.MaterialAutoCompleteTextView_simpleItemLayout,
R.layout.mtrl_auto_complete_simple_item);
popupElevation =
attributes.getDimensionPixelOffset(
R.styleable.MaterialAutoCompleteTextView_android_popupElevation,
R.dimen.mtrl_exposed_dropdown_menu_popup_elevation);

simpleItemSelectedColor =
attributes.getColor(
R.styleable.MaterialAutoCompleteTextView_simpleItemSelectedColor, Color.TRANSPARENT);
simpleItemSelectedRippleColor =
MaterialResources.getColorStateList(
context,
attributes,
R.styleable.MaterialAutoCompleteTextView_simpleItemSelectedRippleColor);

accessibilityManager =
(AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);

Expand Down Expand Up @@ -191,7 +213,65 @@ public void setSimpleItems(@ArrayRes int stringArrayResId) {
* @see #setAdapter(ListAdapter)
*/
public void setSimpleItems(@NonNull String[] stringArray) {
setAdapter(new ArrayAdapter<>(getContext(), simpleItemLayout, stringArray));
setAdapter(new MaterialArrayAdapter<>(getContext(), simpleItemLayout, stringArray));
}

/**
* Sets the color of the default selected popup dropdown item to be used along with
* {@code R.attr.simpleItemLayout}.
*
* @param simpleItemSelectedColor the selected item color
* @see #getSimpleItemSelectedColor()
* @see #setSimpleItems(int)
* @attr ref
* com.google.android.material.R.styleable#MaterialAutoCompleteTextView_simpleItemSelectedColor
*/
public void setSimpleItemSelectedColor(int simpleItemSelectedColor) {
this.simpleItemSelectedColor = simpleItemSelectedColor;
if (getAdapter() instanceof MaterialArrayAdapter) {
((MaterialArrayAdapter) getAdapter()).updateSelectedItemColorStateList();
}
}

/**
* Returns the color of the default selected popup dropdown item.
*
* @see #setSimpleItemSelectedColor(int)
* @attr ref
* com.google.android.material.R.styleable#MaterialAutoCompleteTextView_simpleItemSelectedColor
*/
public int getSimpleItemSelectedColor() {
return simpleItemSelectedColor;
}

/**
* Sets the ripple color of the selected popup dropdown item to be used along with
* {@code R.attr.simpleItemLayout}.
*
* @param simpleItemSelectedRippleColor the ripple color state list
* @see #getSimpleItemSelectedRippleColor()
* @see #setSimpleItems(int)
* @attr ref
* com.google.android.material.R.styleable#MaterialAutoCompleteTextView_simpleItemSelectedRippleColor
*/
public void setSimpleItemSelectedRippleColor(
@Nullable ColorStateList simpleItemSelectedRippleColor) {
this.simpleItemSelectedRippleColor = simpleItemSelectedRippleColor;
if (getAdapter() instanceof MaterialArrayAdapter) {
((MaterialArrayAdapter) getAdapter()).updateSelectedItemColorStateList();
}
}

/**
* Returns the ripple color of the default selected popup dropdown item, or null if not set.
*
* @see #setSimpleItemSelectedRippleColor(ColorStateList)
* @attr ref
* com.google.android.material.R.styleable#MaterialAutoCompleteTextView_simpleItemSelectedRippleColor
*/
@Nullable
public ColorStateList getSimpleItemSelectedRippleColor() {
return simpleItemSelectedRippleColor;
}

/**
Expand Down Expand Up @@ -325,4 +405,110 @@ private <T extends ListAdapter & Filterable> void updateText(Object selectedItem
setAdapter((T) adapter);
}
}

/** ArrayAdapter for the {@link MaterialAutoCompleteTextView}. */
private class MaterialArrayAdapter<T> extends ArrayAdapter<String> {

@Nullable private ColorStateList selectedItemRippleOverlaidColor;
@Nullable private ColorStateList pressedRippleColor;

MaterialArrayAdapter(
@NonNull Context context, int resource, @NonNull String[] objects) {
super(context, resource, objects);
updateSelectedItemColorStateList();
}

void updateSelectedItemColorStateList() {
pressedRippleColor = sanitizeDropdownItemSelectedRippleColor();
selectedItemRippleOverlaidColor = createItemSelectedColorStateList();
}

@Override
public View getView(int position, @Nullable View convertView, ViewGroup parent) {
View view = super.getView(position, convertView, parent);

if (view instanceof TextView) {
TextView textView = (TextView) view;
boolean isSelectedItem = getText().toString().contentEquals(textView.getText());
ViewCompat.setBackground(textView, isSelectedItem ? getSelectedItemDrawable() : null);
}

return view;
}

@Nullable
private Drawable getSelectedItemDrawable() {
if (!hasSelectedColor() || VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) {
return null;
}

// The adapter calls getView with the same position multiple times with different views,
// meaning we can't know which view is actually being used to show the list item. We need to
// create the drawable on every call, otherwise there can be a race condition causing the
// background color to not be updated to the right state.
Drawable colorDrawable = new ColorDrawable(simpleItemSelectedColor);
if (pressedRippleColor != null) {
// The ListPopupWindow takes over the states of its list items in order to implement its
// own ripple. That makes the RippleDrawable not work as expected, i.e. it will respond to
// pressed states, but not to other states like focused and hovered. To solve that, we
// create the selectedItemRippleOverlaidColor that will work in those missing states, making
// the selected list item stateful as expected.
DrawableCompat.setTintList(colorDrawable, selectedItemRippleOverlaidColor);
return new RippleDrawable(pressedRippleColor, colorDrawable, null);
} else {
return colorDrawable;
}
}

@Nullable
private ColorStateList createItemSelectedColorStateList() {
if (!hasSelectedColor()
|| !hasSelectedRippleColor()
|| VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) {
return null;
}
int[] stateHovered = new int[] {android.R.attr.state_hovered, -android.R.attr.state_pressed};
int[] stateSelected =
new int[] {android.R.attr.state_selected, -android.R.attr.state_pressed};
int colorSelected =
simpleItemSelectedRippleColor.getColorForState(stateSelected, Color.TRANSPARENT);
int colorHovered =
simpleItemSelectedRippleColor.getColorForState(stateHovered, Color.TRANSPARENT);
// Use ripple colors overlaid over selected color.
int[] colors =
new int[] {
MaterialColors.layer(simpleItemSelectedColor, colorSelected),
MaterialColors.layer(simpleItemSelectedColor, colorHovered),
simpleItemSelectedColor
};
int[][] states = new int[][] {stateSelected, stateHovered, new int[] {}};

return new ColorStateList(states, colors);
}

private ColorStateList sanitizeDropdownItemSelectedRippleColor() {
if (!hasSelectedRippleColor()) {
return null;
}

// We need to ensure that the ripple drawable we create will show a color only for the pressed
// state so that the final ripple over the item view will be the correct color.
int[] statePressed = new int[] {android.R.attr.state_pressed};
int[] colors =
new int[] {
simpleItemSelectedRippleColor.getColorForState(statePressed, Color.TRANSPARENT),
Color.TRANSPARENT
};
int[][] states = new int[][] {statePressed, new int[] {}};
return new ColorStateList(states, colors);
}

private boolean hasSelectedColor() {
return simpleItemSelectedColor != Color.TRANSPARENT;
}

private boolean hasSelectedRippleColor() {
return simpleItemSelectedRippleColor != null;
}
}
}
Expand Up @@ -100,6 +100,8 @@
<public name="textInputLayoutFocusedRectEnabled" type="attr"/>
<public name="simpleItemLayout" type="attr"/>
<public name="simpleItems" type="attr"/>
<public name="simpleItemSelectedColor" type="attr"/>
<public name="simpleItemSelectedRippleColor" type="attr"/>

<public name="Widget.Design.TextInputLayout" type="style"/>
<public name="Widget.MaterialComponents.TextInputLayout.FilledBox" type="style"/>
Expand Down
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:alpha="@dimen/m3_ripple_pressed_alpha" android:color="?attr/colorOnSurface" android:state_pressed="true"/>
<!-- The selected and hovered colors should also specify that they are for android:state_pressed="false". -->
<!-- When focused, the dropdown's item text view doesn't respond to state_focused, but to state_selected instead. -->
<item android:alpha="@dimen/m3_simple_item_color_selected_alpha" android:color="?attr/colorOnSurface" android:state_selected="true" android:state_pressed="false"/>
<item android:alpha="@dimen/m3_simple_item_color_hovered_alpha" android:color="?attr/colorOnSurface" android:state_hovered="true" android:state_pressed="false"/>
</selector>
Expand Up @@ -307,6 +307,10 @@
<attr name="simpleItemLayout" format="reference"/>
<!-- The default auto-completion items in a string array -->
<attr name="simpleItems" format="reference"/>
<!-- The color of the default selected item of the dropdown list. -->
<attr name="simpleItemSelectedColor" format="color"/>
<!-- The ripple color of the default selected item of the dropdown list. -->
<attr name="simpleItemSelectedRippleColor" format="color"/>
</declare-styleable>

</resources>
Expand Up @@ -64,4 +64,11 @@
<dimen name="mtrl_exposed_dropdown_menu_popup_vertical_offset">-8dp</dimen>

<dimen name="m3_exposed_dropdown_menu_popup_elevation">@dimen/m3_sys_elevation_level2</dimen>

<!-- Alphas for state colors of the dropdown menu items. Same as the ripple
alpha values but without the changes applied for different APIs. -->
<!-- Same as @dimen/m3_ripple_focused_alpha. -->
<item name="m3_simple_item_color_selected_alpha" format="float" type="dimen">0.12</item>
<!-- Same as @dimen/m3_ripple_hovered_alpha. -->
<item name="m3_simple_item_color_hovered_alpha" format="float" type="dimen">0.08</item>
</resources>

0 comments on commit 6206ff5

Please sign in to comment.