Skip to content

Commit 6206ff5

Browse files
committed
[ExposedDropdownMenu] Added support for default/ripple background colors for the selected item of the exposed dropdown menu when the default MaterialAutoCompleteArrayAdapter is being used.
PiperOrigin-RevId: 447531152
1 parent 3fc53ac commit 6206ff5

File tree

12 files changed

+278
-23
lines changed

12 files changed

+278
-23
lines changed

docs/components/Menu.md

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -562,19 +562,21 @@ The exposed dropdown menu is an `AutoCompleteTextView` within a
562562
For all attributes that apply to the `TextInputLayout`, see the
563563
[TextInputLayout documentation](TextField.md).
564564

565-
#### `AutoCompleteTextView` attributes (input text, dropdown menu)
566-
567-
Element | Attribute | Related method(s) | Default value
568-
------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------ | -------------
569-
**Input text** | `android:text` | `setText`<br/>`getText` | `@null`
570-
**Typography** | `android:textAppearance` | `setTextAppearance` | `?attr/textAppearanceBodyLarge`
571-
**Input accepted** | `android:inputType` | `N/A` | framework's default
572-
**Input text color** | `android:textColor` | `setTextColor`<br/>`getTextColors`<br/>`getCurrentTextColor` | `?android:textColorPrimary`
573-
**Cursor color** | N/A (color comes from the theme attr `?attr/colorControlActivated`) | N/A | `?attr/colorPrimary`
574-
**Dropdown menu<br/>container color** | N/A | N/A | `?attr/colorSurface`
575-
**Dropdown menu elevation** | `android:popupElevation` | `getPopupElevation` | `3dp`
576-
**Simple items** | `app:simpleItems` | `setSimpleItems` | `null`
577-
**Simple item layout** | `app:simpleItemLayout` | N/A | `@layout/m3_auto_complete_simple_item`
565+
#### `MaterialAutoCompleteTextView` attributes (input text, dropdown menu)
566+
567+
Element | Attribute | Related method(s) | Default value
568+
----------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------- | -------------
569+
**Input text** | `android:text` | `setText`<br/>`getText` | `@null`
570+
**Typography** | `android:textAppearance` | `setTextAppearance` | `?attr/textAppearanceBodyLarge`
571+
**Input accepted** | `android:inputType` | `N/A` | framework's default
572+
**Input text color** | `android:textColor` | `setTextColor`<br/>`getTextColors`<br/>`getCurrentTextColor` | `?android:textColorPrimary`
573+
**Cursor color** | N/A (color comes from the theme attr `?attr/colorControlActivated`) | N/A | `?attr/colorPrimary`
574+
**Dropdown menu<br/>container color** | N/A | N/A | `?attr/colorSurface`
575+
**Dropdown menu elevation** | `android:popupElevation` | `getPopupElevation` | `3dp`
576+
**Simple items** | `app:simpleItems` | `setSimpleItems` | `null`
577+
**Simple item layout** | `app:simpleItemLayout` | N/A | `@layout/m3_auto_complete_simple_item`
578+
**Selected simple item color** | `app:simpleItemSelectedColor` | `setSimpleItemSelectedColor`<br/>`getSimpleItemSelectedColor` | `?attr/colorSurfaceVariant`
579+
**Selected simple item<br/>ripple color** | `app:simpleItemSelectedRippleColor` | `setSimpleItemSelectedRippleColor`<br/>`getSimpleItemSelectedRippleColor` | `@color/m3_simple_item_ripple_color`
578580

579581
#### Styles
580582

docs/components/TextField.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ See the full list of
206206

207207
### Implementing an exposed dropdown menu
208208

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

211211
In the layout:
212212

-35 KB
Loading
-25.4 KB
Loading
Binary file not shown.

lib/java/com/google/android/material/textfield/MaterialAutoCompleteTextView.java

Lines changed: 195 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,21 @@
2121
import static com.google.android.material.theme.overlay.MaterialThemeOverlay.wrap;
2222

2323
import android.content.Context;
24+
import android.content.res.ColorStateList;
2425
import android.content.res.TypedArray;
26+
import android.graphics.Color;
2527
import android.graphics.Rect;
28+
import android.graphics.drawable.ColorDrawable;
2629
import android.graphics.drawable.Drawable;
30+
import android.graphics.drawable.RippleDrawable;
2731
import android.os.Build.VERSION;
32+
import android.os.Build.VERSION_CODES;
2833
import androidx.appcompat.widget.AppCompatAutoCompleteTextView;
2934
import androidx.appcompat.widget.ListPopupWindow;
3035
import android.text.InputType;
3136
import android.util.AttributeSet;
3237
import android.view.View;
38+
import android.view.ViewGroup;
3339
import android.view.ViewGroup.LayoutParams;
3440
import android.view.ViewParent;
3541
import android.view.accessibility.AccessibilityManager;
@@ -38,23 +44,28 @@
3844
import android.widget.ArrayAdapter;
3945
import android.widget.Filterable;
4046
import android.widget.ListAdapter;
47+
import android.widget.TextView;
4148
import androidx.annotation.ArrayRes;
4249
import androidx.annotation.LayoutRes;
4350
import androidx.annotation.NonNull;
4451
import androidx.annotation.Nullable;
52+
import androidx.core.graphics.drawable.DrawableCompat;
53+
import androidx.core.view.ViewCompat;
54+
import com.google.android.material.color.MaterialColors;
4555
import com.google.android.material.internal.ManufacturerUtils;
4656
import com.google.android.material.internal.ThemeEnforcement;
57+
import com.google.android.material.resources.MaterialResources;
4758

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

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

6982
public MaterialAutoCompleteTextView(@NonNull Context context) {
7083
this(context, null);
@@ -100,15 +113,24 @@ public MaterialAutoCompleteTextView(
100113
}
101114
}
102115

103-
simpleItemLayout = attributes.getResourceId(
104-
R.styleable.MaterialAutoCompleteTextView_simpleItemLayout,
105-
R.layout.mtrl_auto_complete_simple_item);
106-
116+
simpleItemLayout =
117+
attributes.getResourceId(
118+
R.styleable.MaterialAutoCompleteTextView_simpleItemLayout,
119+
R.layout.mtrl_auto_complete_simple_item);
107120
popupElevation =
108121
attributes.getDimensionPixelOffset(
109122
R.styleable.MaterialAutoCompleteTextView_android_popupElevation,
110123
R.dimen.mtrl_exposed_dropdown_menu_popup_elevation);
111124

125+
simpleItemSelectedColor =
126+
attributes.getColor(
127+
R.styleable.MaterialAutoCompleteTextView_simpleItemSelectedColor, Color.TRANSPARENT);
128+
simpleItemSelectedRippleColor =
129+
MaterialResources.getColorStateList(
130+
context,
131+
attributes,
132+
R.styleable.MaterialAutoCompleteTextView_simpleItemSelectedRippleColor);
133+
112134
accessibilityManager =
113135
(AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
114136

@@ -191,7 +213,65 @@ public void setSimpleItems(@ArrayRes int stringArrayResId) {
191213
* @see #setAdapter(ListAdapter)
192214
*/
193215
public void setSimpleItems(@NonNull String[] stringArray) {
194-
setAdapter(new ArrayAdapter<>(getContext(), simpleItemLayout, stringArray));
216+
setAdapter(new MaterialArrayAdapter<>(getContext(), simpleItemLayout, stringArray));
217+
}
218+
219+
/**
220+
* Sets the color of the default selected popup dropdown item to be used along with
221+
* {@code R.attr.simpleItemLayout}.
222+
*
223+
* @param simpleItemSelectedColor the selected item color
224+
* @see #getSimpleItemSelectedColor()
225+
* @see #setSimpleItems(int)
226+
* @attr ref
227+
* com.google.android.material.R.styleable#MaterialAutoCompleteTextView_simpleItemSelectedColor
228+
*/
229+
public void setSimpleItemSelectedColor(int simpleItemSelectedColor) {
230+
this.simpleItemSelectedColor = simpleItemSelectedColor;
231+
if (getAdapter() instanceof MaterialArrayAdapter) {
232+
((MaterialArrayAdapter) getAdapter()).updateSelectedItemColorStateList();
233+
}
234+
}
235+
236+
/**
237+
* Returns the color of the default selected popup dropdown item.
238+
*
239+
* @see #setSimpleItemSelectedColor(int)
240+
* @attr ref
241+
* com.google.android.material.R.styleable#MaterialAutoCompleteTextView_simpleItemSelectedColor
242+
*/
243+
public int getSimpleItemSelectedColor() {
244+
return simpleItemSelectedColor;
245+
}
246+
247+
/**
248+
* Sets the ripple color of the selected popup dropdown item to be used along with
249+
* {@code R.attr.simpleItemLayout}.
250+
*
251+
* @param simpleItemSelectedRippleColor the ripple color state list
252+
* @see #getSimpleItemSelectedRippleColor()
253+
* @see #setSimpleItems(int)
254+
* @attr ref
255+
* com.google.android.material.R.styleable#MaterialAutoCompleteTextView_simpleItemSelectedRippleColor
256+
*/
257+
public void setSimpleItemSelectedRippleColor(
258+
@Nullable ColorStateList simpleItemSelectedRippleColor) {
259+
this.simpleItemSelectedRippleColor = simpleItemSelectedRippleColor;
260+
if (getAdapter() instanceof MaterialArrayAdapter) {
261+
((MaterialArrayAdapter) getAdapter()).updateSelectedItemColorStateList();
262+
}
263+
}
264+
265+
/**
266+
* Returns the ripple color of the default selected popup dropdown item, or null if not set.
267+
*
268+
* @see #setSimpleItemSelectedRippleColor(ColorStateList)
269+
* @attr ref
270+
* com.google.android.material.R.styleable#MaterialAutoCompleteTextView_simpleItemSelectedRippleColor
271+
*/
272+
@Nullable
273+
public ColorStateList getSimpleItemSelectedRippleColor() {
274+
return simpleItemSelectedRippleColor;
195275
}
196276

197277
/**
@@ -325,4 +405,110 @@ private <T extends ListAdapter & Filterable> void updateText(Object selectedItem
325405
setAdapter((T) adapter);
326406
}
327407
}
408+
409+
/** ArrayAdapter for the {@link MaterialAutoCompleteTextView}. */
410+
private class MaterialArrayAdapter<T> extends ArrayAdapter<String> {
411+
412+
@Nullable private ColorStateList selectedItemRippleOverlaidColor;
413+
@Nullable private ColorStateList pressedRippleColor;
414+
415+
MaterialArrayAdapter(
416+
@NonNull Context context, int resource, @NonNull String[] objects) {
417+
super(context, resource, objects);
418+
updateSelectedItemColorStateList();
419+
}
420+
421+
void updateSelectedItemColorStateList() {
422+
pressedRippleColor = sanitizeDropdownItemSelectedRippleColor();
423+
selectedItemRippleOverlaidColor = createItemSelectedColorStateList();
424+
}
425+
426+
@Override
427+
public View getView(int position, @Nullable View convertView, ViewGroup parent) {
428+
View view = super.getView(position, convertView, parent);
429+
430+
if (view instanceof TextView) {
431+
TextView textView = (TextView) view;
432+
boolean isSelectedItem = getText().toString().contentEquals(textView.getText());
433+
ViewCompat.setBackground(textView, isSelectedItem ? getSelectedItemDrawable() : null);
434+
}
435+
436+
return view;
437+
}
438+
439+
@Nullable
440+
private Drawable getSelectedItemDrawable() {
441+
if (!hasSelectedColor() || VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) {
442+
return null;
443+
}
444+
445+
// The adapter calls getView with the same position multiple times with different views,
446+
// meaning we can't know which view is actually being used to show the list item. We need to
447+
// create the drawable on every call, otherwise there can be a race condition causing the
448+
// background color to not be updated to the right state.
449+
Drawable colorDrawable = new ColorDrawable(simpleItemSelectedColor);
450+
if (pressedRippleColor != null) {
451+
// The ListPopupWindow takes over the states of its list items in order to implement its
452+
// own ripple. That makes the RippleDrawable not work as expected, i.e. it will respond to
453+
// pressed states, but not to other states like focused and hovered. To solve that, we
454+
// create the selectedItemRippleOverlaidColor that will work in those missing states, making
455+
// the selected list item stateful as expected.
456+
DrawableCompat.setTintList(colorDrawable, selectedItemRippleOverlaidColor);
457+
return new RippleDrawable(pressedRippleColor, colorDrawable, null);
458+
} else {
459+
return colorDrawable;
460+
}
461+
}
462+
463+
@Nullable
464+
private ColorStateList createItemSelectedColorStateList() {
465+
if (!hasSelectedColor()
466+
|| !hasSelectedRippleColor()
467+
|| VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) {
468+
return null;
469+
}
470+
int[] stateHovered = new int[] {android.R.attr.state_hovered, -android.R.attr.state_pressed};
471+
int[] stateSelected =
472+
new int[] {android.R.attr.state_selected, -android.R.attr.state_pressed};
473+
int colorSelected =
474+
simpleItemSelectedRippleColor.getColorForState(stateSelected, Color.TRANSPARENT);
475+
int colorHovered =
476+
simpleItemSelectedRippleColor.getColorForState(stateHovered, Color.TRANSPARENT);
477+
// Use ripple colors overlaid over selected color.
478+
int[] colors =
479+
new int[] {
480+
MaterialColors.layer(simpleItemSelectedColor, colorSelected),
481+
MaterialColors.layer(simpleItemSelectedColor, colorHovered),
482+
simpleItemSelectedColor
483+
};
484+
int[][] states = new int[][] {stateSelected, stateHovered, new int[] {}};
485+
486+
return new ColorStateList(states, colors);
487+
}
488+
489+
private ColorStateList sanitizeDropdownItemSelectedRippleColor() {
490+
if (!hasSelectedRippleColor()) {
491+
return null;
492+
}
493+
494+
// We need to ensure that the ripple drawable we create will show a color only for the pressed
495+
// state so that the final ripple over the item view will be the correct color.
496+
int[] statePressed = new int[] {android.R.attr.state_pressed};
497+
int[] colors =
498+
new int[] {
499+
simpleItemSelectedRippleColor.getColorForState(statePressed, Color.TRANSPARENT),
500+
Color.TRANSPARENT
501+
};
502+
int[][] states = new int[][] {statePressed, new int[] {}};
503+
return new ColorStateList(states, colors);
504+
}
505+
506+
private boolean hasSelectedColor() {
507+
return simpleItemSelectedColor != Color.TRANSPARENT;
508+
}
509+
510+
private boolean hasSelectedRippleColor() {
511+
return simpleItemSelectedRippleColor != null;
512+
}
513+
}
328514
}

lib/java/com/google/android/material/textfield/res-public/values/public.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@
100100
<public name="textInputLayoutFocusedRectEnabled" type="attr"/>
101101
<public name="simpleItemLayout" type="attr"/>
102102
<public name="simpleItems" type="attr"/>
103+
<public name="simpleItemSelectedColor" type="attr"/>
104+
<public name="simpleItemSelectedRippleColor" type="attr"/>
103105

104106
<public name="Widget.Design.TextInputLayout" type="style"/>
105107
<public name="Widget.MaterialComponents.TextInputLayout.FilledBox" type="style"/>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
Copyright (C) 2022 The Android Open Source Project
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
-->
17+
<selector xmlns:android="http://schemas.android.com/apk/res/android">
18+
<item android:alpha="@dimen/m3_ripple_pressed_alpha" android:color="?attr/colorOnSurface" android:state_pressed="true"/>
19+
<!-- The selected and hovered colors should also specify that they are for android:state_pressed="false". -->
20+
<!-- When focused, the dropdown's item text view doesn't respond to state_focused, but to state_selected instead. -->
21+
<item android:alpha="@dimen/m3_simple_item_color_selected_alpha" android:color="?attr/colorOnSurface" android:state_selected="true" android:state_pressed="false"/>
22+
<item android:alpha="@dimen/m3_simple_item_color_hovered_alpha" android:color="?attr/colorOnSurface" android:state_hovered="true" android:state_pressed="false"/>
23+
</selector>

lib/java/com/google/android/material/textfield/res/values/attrs.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,10 @@
307307
<attr name="simpleItemLayout" format="reference"/>
308308
<!-- The default auto-completion items in a string array -->
309309
<attr name="simpleItems" format="reference"/>
310+
<!-- The color of the default selected item of the dropdown list. -->
311+
<attr name="simpleItemSelectedColor" format="color"/>
312+
<!-- The ripple color of the default selected item of the dropdown list. -->
313+
<attr name="simpleItemSelectedRippleColor" format="color"/>
310314
</declare-styleable>
311315

312316
</resources>

lib/java/com/google/android/material/textfield/res/values/dimens.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,11 @@
6464
<dimen name="mtrl_exposed_dropdown_menu_popup_vertical_offset">-8dp</dimen>
6565

6666
<dimen name="m3_exposed_dropdown_menu_popup_elevation">@dimen/m3_sys_elevation_level2</dimen>
67+
68+
<!-- Alphas for state colors of the dropdown menu items. Same as the ripple
69+
alpha values but without the changes applied for different APIs. -->
70+
<!-- Same as @dimen/m3_ripple_focused_alpha. -->
71+
<item name="m3_simple_item_color_selected_alpha" format="float" type="dimen">0.12</item>
72+
<!-- Same as @dimen/m3_ripple_hovered_alpha. -->
73+
<item name="m3_simple_item_color_hovered_alpha" format="float" type="dimen">0.08</item>
6774
</resources>

0 commit comments

Comments
 (0)