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