2121import static com .google .android .material .theme .overlay .MaterialThemeOverlay .wrap ;
2222
2323import android .content .Context ;
24+ import android .content .res .ColorStateList ;
2425import android .content .res .TypedArray ;
26+ import android .graphics .Color ;
2527import android .graphics .Rect ;
28+ import android .graphics .drawable .ColorDrawable ;
2629import android .graphics .drawable .Drawable ;
30+ import android .graphics .drawable .RippleDrawable ;
2731import android .os .Build .VERSION ;
32+ import android .os .Build .VERSION_CODES ;
2833import androidx .appcompat .widget .AppCompatAutoCompleteTextView ;
2934import androidx .appcompat .widget .ListPopupWindow ;
3035import android .text .InputType ;
3136import android .util .AttributeSet ;
3237import android .view .View ;
38+ import android .view .ViewGroup ;
3339import android .view .ViewGroup .LayoutParams ;
3440import android .view .ViewParent ;
3541import android .view .accessibility .AccessibilityManager ;
3844import android .widget .ArrayAdapter ;
3945import android .widget .Filterable ;
4046import android .widget .ListAdapter ;
47+ import android .widget .TextView ;
4148import androidx .annotation .ArrayRes ;
4249import androidx .annotation .LayoutRes ;
4350import androidx .annotation .NonNull ;
4451import androidx .annotation .Nullable ;
52+ import androidx .core .graphics .drawable .DrawableCompat ;
53+ import androidx .core .view .ViewCompat ;
54+ import com .google .android .material .color .MaterialColors ;
4555import com .google .android .material .internal .ManufacturerUtils ;
4656import 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 */
5970public 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}
0 commit comments