diff --git a/docs/components/Menu.md b/docs/components/Menu.md index e6200f1f66a..a4143449eb0 100644 --- a/docs/components/Menu.md +++ b/docs/components/Menu.md @@ -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`
`getText` | `@null` -**Typography** | `android:textAppearance` | `setTextAppearance` | `?attr/textAppearanceBodyLarge` -**Input accepted** | `android:inputType` | `N/A` | framework's default -**Input text color** | `android:textColor` | `setTextColor`
`getTextColors`
`getCurrentTextColor` | `?android:textColorPrimary` -**Cursor color** | N/A (color comes from the theme attr `?attr/colorControlActivated`) | N/A | `?attr/colorPrimary` -**Dropdown menu
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`
`getText` | `@null` +**Typography** | `android:textAppearance` | `setTextAppearance` | `?attr/textAppearanceBodyLarge` +**Input accepted** | `android:inputType` | `N/A` | framework's default +**Input text color** | `android:textColor` | `setTextColor`
`getTextColors`
`getCurrentTextColor` | `?android:textColorPrimary` +**Cursor color** | N/A (color comes from the theme attr `?attr/colorControlActivated`) | N/A | `?attr/colorPrimary` +**Dropdown menu
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`
`getSimpleItemSelectedColor` | `?attr/colorSurfaceVariant` +**Selected simple item
ripple color** | `app:simpleItemSelectedRippleColor` | `setSimpleItemSelectedRippleColor`
`getSimpleItemSelectedRippleColor` | `@color/m3_simple_item_ripple_color` #### Styles diff --git a/docs/components/TextField.md b/docs/components/TextField.md index 43b047097ae..ac1e0f28f65 100644 --- a/docs/components/TextField.md +++ b/docs/components/TextField.md @@ -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: diff --git a/docs/components/assets/menu/menus_exposed_dropdown_filled.png b/docs/components/assets/menu/menus_exposed_dropdown_filled.png index 7a267d7ba8f..9a5ca8e8528 100644 Binary files a/docs/components/assets/menu/menus_exposed_dropdown_filled.png and b/docs/components/assets/menu/menus_exposed_dropdown_filled.png differ diff --git a/docs/components/assets/menu/menus_exposed_dropdown_outlined.png b/docs/components/assets/menu/menus_exposed_dropdown_outlined.png index 1eb025978c2..6c3bb53728a 100644 Binary files a/docs/components/assets/menu/menus_exposed_dropdown_outlined.png and b/docs/components/assets/menu/menus_exposed_dropdown_outlined.png differ diff --git a/docs/components/assets/textfields/textfields_exposed_dropdown_menu.png b/docs/components/assets/textfields/textfields_exposed_dropdown_menu.png deleted file mode 100644 index 342cfeb14e0..00000000000 Binary files a/docs/components/assets/textfields/textfields_exposed_dropdown_menu.png and /dev/null differ diff --git a/lib/java/com/google/android/material/textfield/MaterialAutoCompleteTextView.java b/lib/java/com/google/android/material/textfield/MaterialAutoCompleteTextView.java index 01ca9650df3..0166a3b7282 100644 --- a/lib/java/com/google/android/material/textfield/MaterialAutoCompleteTextView.java +++ b/lib/java/com/google/android/material/textfield/MaterialAutoCompleteTextView.java @@ -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; @@ -38,12 +44,17 @@ 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 @@ -51,10 +62,10 @@ * interacted through a screen reader. * *

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 { @@ -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); @@ -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); @@ -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; } /** @@ -325,4 +405,110 @@ private void updateText(Object selectedItem setAdapter((T) adapter); } } + + /** ArrayAdapter for the {@link MaterialAutoCompleteTextView}. */ + private class MaterialArrayAdapter extends ArrayAdapter { + + @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; + } + } } diff --git a/lib/java/com/google/android/material/textfield/res-public/values/public.xml b/lib/java/com/google/android/material/textfield/res-public/values/public.xml index e940423331f..4ba84f441ff 100644 --- a/lib/java/com/google/android/material/textfield/res-public/values/public.xml +++ b/lib/java/com/google/android/material/textfield/res-public/values/public.xml @@ -100,6 +100,8 @@ + + diff --git a/lib/java/com/google/android/material/textfield/res/color/m3_simple_item_ripple_color.xml b/lib/java/com/google/android/material/textfield/res/color/m3_simple_item_ripple_color.xml new file mode 100644 index 00000000000..e350a012166 --- /dev/null +++ b/lib/java/com/google/android/material/textfield/res/color/m3_simple_item_ripple_color.xml @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/lib/java/com/google/android/material/textfield/res/values/attrs.xml b/lib/java/com/google/android/material/textfield/res/values/attrs.xml index b680c9e754a..a8cb46a612f 100644 --- a/lib/java/com/google/android/material/textfield/res/values/attrs.xml +++ b/lib/java/com/google/android/material/textfield/res/values/attrs.xml @@ -307,6 +307,10 @@ + + + + diff --git a/lib/java/com/google/android/material/textfield/res/values/dimens.xml b/lib/java/com/google/android/material/textfield/res/values/dimens.xml index 0c3e1df6d0f..dd4fa763fe9 100644 --- a/lib/java/com/google/android/material/textfield/res/values/dimens.xml +++ b/lib/java/com/google/android/material/textfield/res/values/dimens.xml @@ -64,4 +64,11 @@ -8dp @dimen/m3_sys_elevation_level2 + + + + 0.12 + + 0.08 diff --git a/lib/java/com/google/android/material/textfield/res/values/styles.xml b/lib/java/com/google/android/material/textfield/res/values/styles.xml index 1c2ff61255e..5a79df99a01 100644 --- a/lib/java/com/google/android/material/textfield/res/values/styles.xml +++ b/lib/java/com/google/android/material/textfield/res/values/styles.xml @@ -447,24 +447,32 @@ diff --git a/tests/javatests/com/google/android/material/textfield/ExposedDropdownMenuTest.java b/tests/javatests/com/google/android/material/textfield/ExposedDropdownMenuTest.java index c1fdcf66a86..bd3473436c6 100644 --- a/tests/javatests/com/google/android/material/textfield/ExposedDropdownMenuTest.java +++ b/tests/javatests/com/google/android/material/textfield/ExposedDropdownMenuTest.java @@ -40,6 +40,8 @@ import static org.junit.Assert.assertNull; import android.app.Activity; +import android.content.res.ColorStateList; +import android.graphics.Color; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.text.InputType; @@ -123,6 +125,27 @@ public void testSwitchingRawInputType_updatesBackground() { assertThat(editText.getBackground(), instanceOf(LayerDrawable.class)); } + @Test + public void testSetSimpleItemSelectedColor_succeeds() { + Activity activity = activityTestRule.getActivity(); + MaterialAutoCompleteTextView editText = activity.findViewById(R.id.edittext_filled); + + editText.setSimpleItemSelectedColor(Color.BLUE); + + assertThat(editText.getSimpleItemSelectedColor(), is(Color.BLUE)); + } + + @Test + public void testSetSimpleItemSelectedRippleColor_succeeds() { + Activity activity = activityTestRule.getActivity(); + MaterialAutoCompleteTextView editText = activity.findViewById(R.id.edittext_filled); + + editText.setSimpleItemSelectedRippleColor(ColorStateList.valueOf(Color.BLUE)); + + assertThat( + editText.getSimpleItemSelectedRippleColor(), is(ColorStateList.valueOf(Color.BLUE))); + } + @Test public void testEndIconClickShowsDropdownPopup() { final Activity activity = activityTestRule.getActivity();