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