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