1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.appcompat.widget;
18 
19 import static android.os.Build.VERSION.SDK_INT;
20 
21 import android.annotation.SuppressLint;
22 import android.content.Context;
23 import android.graphics.Canvas;
24 import android.graphics.Rect;
25 import android.graphics.drawable.Drawable;
26 import android.os.Build;
27 import android.view.MotionEvent;
28 import android.view.View;
29 import android.view.ViewGroup;
30 import android.widget.AbsListView;
31 import android.widget.AdapterView;
32 import android.widget.ListAdapter;
33 import android.widget.ListView;
34 
35 import androidx.annotation.RequiresApi;
36 import androidx.appcompat.R;
37 import androidx.appcompat.graphics.drawable.DrawableWrapperCompat;
38 import androidx.core.graphics.drawable.DrawableCompat;
39 import androidx.core.view.ViewPropertyAnimatorCompat;
40 import androidx.core.widget.ListViewAutoScrollHelper;
41 
42 import org.jspecify.annotations.NonNull;
43 
44 import java.lang.reflect.Field;
45 import java.lang.reflect.InvocationTargetException;
46 import java.lang.reflect.Method;
47 
48 /**
49  * <p>Wrapper class for a ListView. This wrapper can hijack the focus to
50  * make sure the list uses the appropriate drawables and states when
51  * displayed on screen within a drop down. The focus is never actually
52  * passed to the drop down in this mode; the list only looks focused.</p>
53  */
54 class DropDownListView extends ListView {
55     public static final int INVALID_POSITION = -1;
56     public static final int NO_POSITION = -1;
57 
58     private final Rect mSelectorRect = new Rect();
59     private int mSelectionLeftPadding = 0;
60     private int mSelectionTopPadding = 0;
61     private int mSelectionRightPadding = 0;
62     private int mSelectionBottomPadding = 0;
63 
64     private int mMotionPosition;
65 
66     private GateKeeperDrawable mSelector;
67 
68     /*
69     * WARNING: This is a workaround for a touch mode issue.
70     *
71     * Touch mode is propagated lazily to windows. This causes problems in
72     * the following scenario:
73     * - Type something in the AutoCompleteTextView and get some results
74     * - Move down with the d-pad to select an item in the list
75     * - Move up with the d-pad until the selection disappears
76     * - Type more text in the AutoCompleteTextView *using the soft keyboard*
77     *   and get new results; you are now in touch mode
78     * - The selection comes back on the first item in the list, even though
79     *   the list is supposed to be in touch mode
80     *
81     * Using the soft keyboard triggers the touch mode change but that change
82     * is propagated to our window only after the first list layout, therefore
83     * after the list attempts to resurrect the selection.
84     *
85     * The trick to work around this issue is to pretend the list is in touch
86     * mode when we know that the selection should not appear, that is when
87     * we know the user moved the selection away from the list.
88     *
89     * This boolean is set to true whenever we explicitly hide the list's
90     * selection and reset to false whenever we know the user moved the
91     * selection back to the list.
92     *
93     * When this boolean is true, isInTouchMode() returns true, otherwise it
94     * returns super.isInTouchMode().
95     */
96     private boolean mListSelectionHidden;
97 
98     /**
99      * True if this wrapper should fake focus.
100      */
101     private boolean mHijackFocus;
102 
103     /** Whether to force drawing of the pressed state selector. */
104     private boolean mDrawsInPressedState;
105 
106     /** Current drag-to-open click animation, if any. */
107     private ViewPropertyAnimatorCompat mClickAnimation;
108 
109     /** Helper for drag-to-open auto scrolling. */
110     private ListViewAutoScrollHelper mScrollHelper;
111 
112     /**
113      * Runnable posted when we are awaiting hover event resolution. When set,
114      * drawable state changes are postponed.
115      */
116     ResolveHoverRunnable mResolveHoverRunnable;
117 
118     /**
119      * <p>Creates a new list view wrapper.</p>
120      *
121      * @param context this view's context
122      */
123     @SuppressWarnings("CatchAndPrintStackTrace")
DropDownListView(@onNull Context context, boolean hijackFocus)124     DropDownListView(@NonNull Context context, boolean hijackFocus) {
125         super(context, null, R.attr.dropDownListViewStyle);
126         mHijackFocus = hijackFocus;
127         setCacheColorHint(0); // Transparent, since the background drawable could be anything.
128     }
superIsSelectedChildViewEnabled()129     private boolean superIsSelectedChildViewEnabled() {
130         if (Build.VERSION.SDK_INT >= 33) {
131             return Api33Impl.isSelectedChildViewEnabled(this);
132         } else {
133             return PreApi33Impl.isSelectedChildViewEnabled(this);
134         }
135     }
superSetSelectedChildViewEnabled(boolean enabled)136     private void superSetSelectedChildViewEnabled(boolean enabled) {
137         if (Build.VERSION.SDK_INT >= 33) {
138             Api33Impl.setSelectedChildViewEnabled(this, enabled);
139         } else {
140             PreApi33Impl.setSelectedChildViewEnabled(this, enabled);
141         }
142     }
143 
144     @Override
isInTouchMode()145     public boolean isInTouchMode() {
146         // WARNING: Please read the comment where mListSelectionHidden is declared
147         return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode();
148     }
149 
150     /**
151      * <p>Returns the focus state in the drop down.</p>
152      *
153      * @return true always if hijacking focus
154      */
155     @Override
hasWindowFocus()156     public boolean hasWindowFocus() {
157         return mHijackFocus || super.hasWindowFocus();
158     }
159 
160     /**
161      * <p>Returns the focus state in the drop down.</p>
162      *
163      * @return true always if hijacking focus
164      */
165     @Override
isFocused()166     public boolean isFocused() {
167         return mHijackFocus || super.isFocused();
168     }
169 
170     /**
171      * <p>Returns the focus state in the drop down.</p>
172      *
173      * @return true always if hijacking focus
174      */
175     @Override
hasFocus()176     public boolean hasFocus() {
177         return mHijackFocus || super.hasFocus();
178     }
179 
180     @Override
setSelector(Drawable sel)181     public void setSelector(Drawable sel) {
182         mSelector = sel != null ? new GateKeeperDrawable(sel) : null;
183         super.setSelector(mSelector);
184 
185         final Rect padding = new Rect();
186         if (sel != null) {
187             sel.getPadding(padding);
188         }
189 
190         mSelectionLeftPadding = padding.left;
191         mSelectionTopPadding = padding.top;
192         mSelectionRightPadding = padding.right;
193         mSelectionBottomPadding = padding.bottom;
194     }
195 
196     @Override
drawableStateChanged()197     protected void drawableStateChanged() {
198         //postpone drawableStateChanged until pending hover to pressed transition finishes.
199         if (mResolveHoverRunnable != null) {
200             return;
201         }
202 
203         super.drawableStateChanged();
204 
205         setSelectorEnabled(true);
206         updateSelectorStateCompat();
207     }
208 
209     @Override
dispatchDraw(Canvas canvas)210     protected void dispatchDraw(Canvas canvas) {
211         final boolean drawSelectorOnTop = false;
212         if (!drawSelectorOnTop) {
213             drawSelectorCompat(canvas);
214         }
215 
216         super.dispatchDraw(canvas);
217     }
218 
219     @Override
onTouchEvent(MotionEvent ev)220     public boolean onTouchEvent(MotionEvent ev) {
221         switch (ev.getAction()) {
222             case MotionEvent.ACTION_DOWN:
223                 mMotionPosition = pointToPosition((int) ev.getX(), (int) ev.getY());
224                 break;
225         }
226         if (mResolveHoverRunnable != null) {
227             // Resolved hover event as hover => touch transition.
228             mResolveHoverRunnable.cancel();
229         }
230         return super.onTouchEvent(ev);
231     }
232 
233     /**
234      * Find a position that can be selected (i.e., is not a separator).
235      *
236      * @param position The starting position to look at.
237      * @param lookDown Whether to look down for other positions.
238      * @return The next selectable position starting at position and then searching either up or
239      *         down. Returns {@link #INVALID_POSITION} if nothing can be found.
240      */
lookForSelectablePosition(int position, boolean lookDown)241     public int lookForSelectablePosition(int position, boolean lookDown) {
242         final ListAdapter adapter = getAdapter();
243         if (adapter == null || isInTouchMode()) {
244             return INVALID_POSITION;
245         }
246 
247         final int count = adapter.getCount();
248         if (!getAdapter().areAllItemsEnabled()) {
249             if (lookDown) {
250                 position = Math.max(0, position);
251                 while (position < count && !adapter.isEnabled(position)) {
252                     position++;
253                 }
254             } else {
255                 position = Math.min(position, count - 1);
256                 while (position >= 0 && !adapter.isEnabled(position)) {
257                     position--;
258                 }
259             }
260 
261             if (position < 0 || position >= count) {
262                 return INVALID_POSITION;
263             }
264             return position;
265         } else {
266             if (position < 0 || position >= count) {
267                 return INVALID_POSITION;
268             }
269             return position;
270         }
271     }
272 
273     /**
274      * Measures the height of the given range of children (inclusive) and returns the height
275      * with this ListView's padding and divider heights included. If maxHeight is provided, the
276      * measuring will stop when the current height reaches maxHeight.
277      *
278      * @param widthMeasureSpec             The width measure spec to be given to a child's
279      *                                     {@link View#measure(int, int)}.
280      * @param startPosition                The position of the first child to be shown.
281      * @param endPosition                  The (inclusive) position of the last child to be
282      *                                     shown. Specify {@link #NO_POSITION} if the last child
283      *                                     should be the last available child from the adapter.
284      * @param maxHeight                    The maximum height that will be returned (if all the
285      *                                     children don't fit in this value, this value will be
286      *                                     returned).
287      * @param disallowPartialChildPosition In general, whether the returned height should only
288      *                                     contain entire children. This is more powerful--it is
289      *                                     the first inclusive position at which partial
290      *                                     children will not be allowed. Example: it looks nice
291      *                                     to have at least 3 completely visible children, and
292      *                                     in portrait this will most likely fit; but in
293      *                                     landscape there could be times when even 2 children
294      *                                     can not be completely shown, so a value of 2
295      *                                     (remember, inclusive) would be good (assuming
296      *                                     startPosition is 0).
297      * @return The height of this ListView with the given children.
298      */
measureHeightOfChildrenCompat(int widthMeasureSpec, int startPosition, int endPosition, final int maxHeight, int disallowPartialChildPosition)299     public int measureHeightOfChildrenCompat(int widthMeasureSpec, int startPosition,
300             int endPosition, final int maxHeight,
301             int disallowPartialChildPosition) {
302 
303         final int paddingTop = getListPaddingTop();
304         final int paddingBottom = getListPaddingBottom();
305         final int reportedDividerHeight = getDividerHeight();
306         final Drawable divider = getDivider();
307 
308         final ListAdapter adapter = getAdapter();
309 
310         if (adapter == null) {
311             return paddingTop + paddingBottom;
312         }
313 
314         // Include the padding of the list
315         int returnedHeight = paddingTop + paddingBottom;
316         final int dividerHeight = ((reportedDividerHeight > 0) && divider != null)
317                 ? reportedDividerHeight : 0;
318 
319         // The previous height value that was less than maxHeight and contained
320         // no partial children
321         int prevHeightWithoutPartialChild = 0;
322 
323         View child = null;
324         int viewType = 0;
325         int count = adapter.getCount();
326         for (int i = 0; i < count; i++) {
327             int newType = adapter.getItemViewType(i);
328             if (newType != viewType) {
329                 child = null;
330                 viewType = newType;
331             }
332             child = adapter.getView(i, child, this);
333 
334             // Compute child height spec
335             int heightMeasureSpec;
336             ViewGroup.LayoutParams childLp = child.getLayoutParams();
337 
338             if (childLp == null) {
339                 childLp = generateDefaultLayoutParams();
340                 child.setLayoutParams(childLp);
341             }
342 
343             if (childLp.height > 0) {
344                 heightMeasureSpec = MeasureSpec.makeMeasureSpec(childLp.height,
345                         MeasureSpec.EXACTLY);
346             } else {
347                 heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
348             }
349             child.measure(widthMeasureSpec, heightMeasureSpec);
350 
351             // Since this view was measured directly against the parent measure
352             // spec, we must measure it again before reuse.
353             child.forceLayout();
354 
355             if (i > 0) {
356                 // Count the divider for all but one child
357                 returnedHeight += dividerHeight;
358             }
359 
360             returnedHeight += child.getMeasuredHeight();
361 
362             if (returnedHeight >= maxHeight) {
363                 // We went over, figure out which height to return.  If returnedHeight >
364                 // maxHeight, then the i'th position did not fit completely.
365                 return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
366                         && (i > disallowPartialChildPosition) // We've past the min pos
367                         && (prevHeightWithoutPartialChild > 0) // We have a prev height
368                         && (returnedHeight != maxHeight) // i'th child did not fit completely
369                         ? prevHeightWithoutPartialChild
370                         : maxHeight;
371             }
372 
373             if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
374                 prevHeightWithoutPartialChild = returnedHeight;
375             }
376         }
377 
378         // At this point, we went through the range of children, and they each
379         // completely fit, so return the returnedHeight
380         return returnedHeight;
381     }
382 
setSelectorEnabled(boolean enabled)383     private void setSelectorEnabled(boolean enabled) {
384         if (mSelector != null) {
385             mSelector.setEnabled(enabled);
386         }
387     }
388 
389     private static class GateKeeperDrawable extends DrawableWrapperCompat {
390         private boolean mEnabled;
391 
GateKeeperDrawable(Drawable drawable)392         GateKeeperDrawable(Drawable drawable) {
393             super(drawable);
394             mEnabled = true;
395         }
396 
setEnabled(boolean enabled)397         void setEnabled(boolean enabled) {
398             mEnabled = enabled;
399         }
400 
401         @Override
setState(int[] stateSet)402         public boolean setState(int[] stateSet) {
403             if (mEnabled) {
404                 return super.setState(stateSet);
405             }
406             return false;
407         }
408 
409         @Override
draw(@onNull Canvas canvas)410         public void draw(@NonNull Canvas canvas) {
411             if (mEnabled) {
412                 super.draw(canvas);
413             }
414         }
415 
416         @Override
setHotspot(float x, float y)417         public void setHotspot(float x, float y) {
418             if (mEnabled) {
419                 super.setHotspot(x, y);
420             }
421         }
422 
423         @Override
setHotspotBounds(int left, int top, int right, int bottom)424         public void setHotspotBounds(int left, int top, int right, int bottom) {
425             if (mEnabled) {
426                 super.setHotspotBounds(left, top, right, bottom);
427             }
428         }
429 
430         @Override
setVisible(boolean visible, boolean restart)431         public boolean setVisible(boolean visible, boolean restart) {
432             if (mEnabled) {
433                 return super.setVisible(visible, restart);
434             }
435             return false;
436         }
437     }
438 
439     @Override
onHoverEvent(@onNull MotionEvent ev)440     public boolean onHoverEvent(@NonNull MotionEvent ev) {
441         if (SDK_INT < 26) {
442             // On SDK 26 and below, hover events force the UI into touch mode which does not show
443             // the selector. Don't bother trying to move selection.
444             return super.onHoverEvent(ev);
445         }
446 
447         final int action = ev.getActionMasked();
448         if (action == MotionEvent.ACTION_HOVER_EXIT && mResolveHoverRunnable == null) {
449             // This may be transitioning to TOUCH_DOWN. Postpone drawable state
450             // updates until either the next frame or the next touch event.
451             mResolveHoverRunnable = new ResolveHoverRunnable();
452             mResolveHoverRunnable.post();
453         }
454 
455         // Allow the super class to handle hover state management first.
456         final boolean handled = super.onHoverEvent(ev);
457         if (action == MotionEvent.ACTION_HOVER_ENTER
458                 || action == MotionEvent.ACTION_HOVER_MOVE) {
459             final int position = pointToPosition((int) ev.getX(), (int) ev.getY());
460 
461             if (position != INVALID_POSITION && position != getSelectedItemPosition()) {
462                 final View hoveredItem = getChildAt(position - getFirstVisiblePosition());
463                 if (hoveredItem.isEnabled()) {
464                     // Force a focus so that the proper selector state gets
465                     // used when we update.
466                     requestFocus();
467 
468                     if (SDK_INT >= 30 && Api30Impl.canPositionSelectorForHoveredItem()) {
469                         // Starting in SDK 30, setSelectionFromTop does not move selection. Instead,
470                         // we'll reflect on the methods used by the platform DropDownListView.
471                         Api30Impl.positionSelectorForHoveredItem(this, position, hoveredItem);
472                     } else {
473                         setSelectionFromTop(position, hoveredItem.getTop() - this.getTop());
474                     }
475                 }
476                 updateSelectorStateCompat();
477             }
478         } else {
479             // Do not cancel the selected position if the selection is visible
480             // by other means.
481             setSelection(INVALID_POSITION);
482         }
483 
484         return handled;
485     }
486 
487     @Override
onDetachedFromWindow()488     protected void onDetachedFromWindow() {
489         mResolveHoverRunnable = null;
490         super.onDetachedFromWindow();
491     }
492 
493     /**
494      * Handles forwarded events.
495      *
496      * @param activePointerId id of the pointer that activated forwarding
497      * @return whether the event was handled
498      */
onForwardedEvent(MotionEvent event, int activePointerId)499     public boolean onForwardedEvent(MotionEvent event, int activePointerId) {
500         boolean handledEvent = true;
501         boolean clearPressedItem = false;
502 
503         final int actionMasked = event.getActionMasked();
504         switch (actionMasked) {
505             case MotionEvent.ACTION_CANCEL:
506                 handledEvent = false;
507                 break;
508             case MotionEvent.ACTION_UP:
509                 handledEvent = false;
510                 // $FALL-THROUGH$
511             case MotionEvent.ACTION_MOVE:
512                 final int activeIndex = event.findPointerIndex(activePointerId);
513                 if (activeIndex < 0) {
514                     handledEvent = false;
515                     break;
516                 }
517 
518                 final int x = (int) event.getX(activeIndex);
519                 final int y = (int) event.getY(activeIndex);
520                 final int position = pointToPosition(x, y);
521                 if (position == INVALID_POSITION) {
522                     clearPressedItem = true;
523                     break;
524                 }
525 
526                 final View child = getChildAt(position - getFirstVisiblePosition());
527                 setPressedItem(child, position, x, y);
528                 handledEvent = true;
529 
530                 if (actionMasked == MotionEvent.ACTION_UP) {
531                     clickPressedItem(child, position);
532                 }
533                 break;
534         }
535 
536         // Failure to handle the event cancels forwarding.
537         if (!handledEvent || clearPressedItem) {
538             clearPressedItem();
539         }
540 
541         // Manage automatic scrolling.
542         if (handledEvent) {
543             if (mScrollHelper == null) {
544                 mScrollHelper = new ListViewAutoScrollHelper(this);
545             }
546             mScrollHelper.setEnabled(true);
547             mScrollHelper.onTouch(this, event);
548         } else if (mScrollHelper != null) {
549             mScrollHelper.setEnabled(false);
550         }
551 
552         return handledEvent;
553     }
554 
555     /**
556      * Starts an alpha animation on the selector. When the animation ends,
557      * the list performs a click on the item.
558      */
clickPressedItem(final View child, final int position)559     private void clickPressedItem(final View child, final int position) {
560         final long id = getItemIdAtPosition(position);
561         performItemClick(child, position, id);
562     }
563 
564     /**
565      * Sets whether the list selection is hidden, as part of a workaround for a
566      * touch mode issue (see the declaration for mListSelectionHidden).
567      *
568      * @param hideListSelection {@code true} to hide list selection,
569      *                          {@code false} to show
570      */
setListSelectionHidden(boolean hideListSelection)571     void setListSelectionHidden(boolean hideListSelection) {
572         mListSelectionHidden = hideListSelection;
573     }
574 
updateSelectorStateCompat()575     private void updateSelectorStateCompat() {
576         Drawable selector = getSelector();
577         if (selector != null && touchModeDrawsInPressedStateCompat() && isPressed()) {
578             selector.setState(getDrawableState());
579         }
580     }
581 
drawSelectorCompat(Canvas canvas)582     private void drawSelectorCompat(Canvas canvas) {
583         if (!mSelectorRect.isEmpty()) {
584             final Drawable selector = getSelector();
585             if (selector != null) {
586                 selector.setBounds(mSelectorRect);
587                 selector.draw(canvas);
588             }
589         }
590     }
591 
positionSelectorLikeTouchCompat(int position, View sel, float x, float y)592     private void positionSelectorLikeTouchCompat(int position, View sel, float x, float y) {
593         positionSelectorLikeFocusCompat(position, sel);
594 
595         Drawable selector = getSelector();
596         if (selector != null && position != INVALID_POSITION) {
597             DrawableCompat.setHotspot(selector, x, y);
598         }
599     }
600 
positionSelectorLikeFocusCompat(int position, View sel)601     private void positionSelectorLikeFocusCompat(int position, View sel) {
602         // If we're changing position, update the visibility since the selector
603         // is technically being detached from the previous selection.
604         final Drawable selector = getSelector();
605         final boolean manageState = selector != null && position != INVALID_POSITION;
606         if (manageState) {
607             selector.setVisible(false, false);
608         }
609 
610         positionSelectorCompat(position, sel);
611 
612         if (manageState) {
613             final Rect bounds = mSelectorRect;
614             final float x = bounds.exactCenterX();
615             final float y = bounds.exactCenterY();
616             selector.setVisible(getVisibility() == VISIBLE, false);
617             DrawableCompat.setHotspot(selector, x, y);
618         }
619     }
620 
621     @SuppressWarnings("CatchAndPrintStackTrace")
positionSelectorCompat(int position, View sel)622     private void positionSelectorCompat(int position, View sel) {
623         final Rect selectorRect = mSelectorRect;
624         selectorRect.set(sel.getLeft(), sel.getTop(), sel.getRight(), sel.getBottom());
625 
626         // Adjust for selection padding.
627         selectorRect.left -= mSelectionLeftPadding;
628         selectorRect.top -= mSelectionTopPadding;
629         selectorRect.right += mSelectionRightPadding;
630         selectorRect.bottom += mSelectionBottomPadding;
631 
632         // AbsListView.mIsChildViewEnabled controls the selector's state so we need to
633         // modify its value
634         final boolean isChildViewEnabled = superIsSelectedChildViewEnabled();
635         if (sel.isEnabled() != isChildViewEnabled) {
636             superSetSelectedChildViewEnabled(!isChildViewEnabled);
637             if (position != INVALID_POSITION) {
638                 refreshDrawableState();
639             }
640         }
641     }
642 
clearPressedItem()643     private void clearPressedItem() {
644         mDrawsInPressedState = false;
645         setPressed(false);
646         // This will call through to updateSelectorState()
647         drawableStateChanged();
648 
649         final View motionView = getChildAt(mMotionPosition - getFirstVisiblePosition());
650         if (motionView != null) {
651             motionView.setPressed(false);
652         }
653 
654         if (mClickAnimation != null) {
655             mClickAnimation.cancel();
656             mClickAnimation = null;
657         }
658     }
659 
setPressedItem(View child, int position, float x, float y)660     private void setPressedItem(View child, int position, float x, float y) {
661         mDrawsInPressedState = true;
662 
663         // Ordering is essential. First, update the container's pressed state.
664         if (SDK_INT >= 21) {
665             Api21Impl.drawableHotspotChanged(this, x, y);
666         }
667         if (!isPressed()) {
668             setPressed(true);
669         }
670 
671         // Next, run layout to stabilize child positions.
672         layoutChildren();
673 
674         // Manage the pressed view based on motion position. This allows us to
675         // play nicely with actual touch and scroll events.
676         if (mMotionPosition != INVALID_POSITION) {
677             final View motionView = getChildAt(mMotionPosition - getFirstVisiblePosition());
678             if (motionView != null && motionView != child && motionView.isPressed()) {
679                 motionView.setPressed(false);
680             }
681         }
682         mMotionPosition = position;
683 
684         // Offset for child coordinates.
685         final float childX = x - child.getLeft();
686         final float childY = y - child.getTop();
687         if (SDK_INT >= 21) {
688             Api21Impl.drawableHotspotChanged(child, childX, childY);
689         }
690         if (!child.isPressed()) {
691             child.setPressed(true);
692         }
693 
694         // Ensure that keyboard focus starts from the last touched position.
695         positionSelectorLikeTouchCompat(position, child, x, y);
696 
697         // This needs some explanation. We need to disable the selector for this next call
698         // due to the way that ListViewCompat works. Otherwise both ListView and ListViewCompat
699         // will draw the selector and bad things happen.
700         setSelectorEnabled(false);
701 
702         // Refresh the drawable state to reflect the new pressed state,
703         // which will also update the selector state.
704         refreshDrawableState();
705     }
706 
touchModeDrawsInPressedStateCompat()707     private boolean touchModeDrawsInPressedStateCompat() {
708         return mDrawsInPressedState;
709     }
710 
711     /**
712      * Runnable that forces hover event resolution and updates drawable state.
713      */
714     private class ResolveHoverRunnable implements Runnable {
ResolveHoverRunnable()715         ResolveHoverRunnable() {
716         }
717 
718         @Override
run()719         public void run() {
720             // Resolved hover event as standard hover exit.
721             mResolveHoverRunnable = null;
722             drawableStateChanged();
723         }
724 
cancel()725         public void cancel() {
726             mResolveHoverRunnable = null;
727             removeCallbacks(this);
728         }
729 
post()730         public void post() {
731             DropDownListView.this.post(this);
732         }
733     }
734 
735     @SuppressWarnings("CatchAndPrintStackTrace")
736     @RequiresApi(30)
737     static class Api30Impl {
738         private static Method sPositionSelector;
739         private static Method sSetSelectedPositionInt;
740         private static Method sSetNextSelectedPositionInt;
741         private static boolean sHasMethods;
742 
743         static {
744             try {
745                 sPositionSelector = AbsListView.class.getDeclaredMethod(
746                         "positionSelector", int.class, View.class,
747                         boolean.class, float.class, float.class);
748                 sPositionSelector.setAccessible(true);
749                 sSetSelectedPositionInt  = AdapterView.class.getDeclaredMethod(
750                         "setSelectedPositionInt", int.class);
751                 sSetSelectedPositionInt.setAccessible(true);
752                 sSetNextSelectedPositionInt = AdapterView.class.getDeclaredMethod(
753                         "setNextSelectedPositionInt", int.class);
754                 sSetNextSelectedPositionInt.setAccessible(true);
755                 sHasMethods = true;
756             } catch (NoSuchMethodException e) {
757                 e.printStackTrace();
758             }
759         }
760 
Api30Impl()761         private Api30Impl() {
762             // This class is not instantiable.
763         }
764 
765         /**
766          * @return whether this class can access the methods required to position selection using
767          * hidden platform APIs
768          */
canPositionSelectorForHoveredItem()769         static boolean canPositionSelectorForHoveredItem() {
770             return sHasMethods;
771         }
772 
773         /**
774          * Positions the selector for a hovered item using the same hidden platform APIs as the
775          * platform implementation of DropDownListView.
776          *
777          * @param view the drop-down list view handling the event
778          * @param position the position to select
779          * @param sel the view being selected
780          */
781         @SuppressWarnings("CatchAndPrintStackTrace")
782         @SuppressLint("BanUncheckedReflection") // No public APIs available.
positionSelectorForHoveredItem(DropDownListView view, int position, View sel)783         static void positionSelectorForHoveredItem(DropDownListView view, int position, View sel) {
784             try {
785                 sPositionSelector.invoke(view, position, sel, false, -1, -1);
786                 sSetSelectedPositionInt.invoke(view, position);
787                 sSetNextSelectedPositionInt.invoke(view, position);
788             } catch (IllegalAccessException e) {
789                 e.printStackTrace();
790             } catch (InvocationTargetException e) {
791                 e.printStackTrace();
792             }
793         }
794     }
795 
796     @RequiresApi(21)
797     static class Api21Impl {
Api21Impl()798         private Api21Impl() {
799             // This class is not instantiable.
800         }
801 
drawableHotspotChanged(View view, float x, float y)802         static void drawableHotspotChanged(View view, float x, float y) {
803             view.drawableHotspotChanged(x, y);
804         }
805     }
806 
807     // TODO(b/221852137): Use @DeprecatedSinceApi(33).
808     @SuppressWarnings({"JavaReflectionMemberAccess", "CatchAndPrintStackTrace"})
809     static class PreApi33Impl {
810         private static final Field sIsChildViewEnabled;
811 
812         static {
813             Field isChildViewEnabled = null;
814 
815             try {
816                 isChildViewEnabled = AbsListView.class.getDeclaredField("mIsChildViewEnabled");
817                 isChildViewEnabled.setAccessible(true);
818             } catch (NoSuchFieldException e) {
819                 e.printStackTrace();
820             }
821 
822             sIsChildViewEnabled = isChildViewEnabled;
823         }
824 
PreApi33Impl()825         private PreApi33Impl() {
826             // This class is not instantiable.
827         }
828 
isSelectedChildViewEnabled(AbsListView view)829         static boolean isSelectedChildViewEnabled(AbsListView view) {
830             if (sIsChildViewEnabled != null) {
831                 try {
832                     return sIsChildViewEnabled.getBoolean(view);
833                 } catch (IllegalAccessException e) {
834                     e.printStackTrace();
835                 }
836             }
837 
838             return false;
839         }
840 
setSelectedChildViewEnabled(AbsListView view, boolean enabled)841         static void setSelectedChildViewEnabled(AbsListView view, boolean enabled) {
842             if (sIsChildViewEnabled != null) {
843                 try {
844                     sIsChildViewEnabled.set(view, enabled);
845                 } catch (IllegalAccessException e) {
846                     e.printStackTrace();
847                 }
848             }
849         }
850     }
851 
852     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
853     static class Api33Impl {
Api33Impl()854         private Api33Impl() {
855             // This class is not instantiable.
856         }
857 
isSelectedChildViewEnabled(AbsListView view)858         static boolean isSelectedChildViewEnabled(AbsListView view) {
859             return view.isSelectedChildViewEnabled();
860         }
861 
setSelectedChildViewEnabled(AbsListView view, boolean enabled)862         static void setSelectedChildViewEnabled(AbsListView view, boolean enabled) {
863             view.setSelectedChildViewEnabled(enabled);
864         }
865     }
866 }
867