1 /* 2 * Copyright (C) 2014 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 androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; 20 21 import android.content.Context; 22 import android.content.res.TypedArray; 23 import android.database.DataSetObserver; 24 import android.graphics.Rect; 25 import android.graphics.drawable.Drawable; 26 import android.os.Handler; 27 import android.util.AttributeSet; 28 import android.util.Log; 29 import android.view.Gravity; 30 import android.view.KeyEvent; 31 import android.view.MotionEvent; 32 import android.view.View; 33 import android.view.View.MeasureSpec; 34 import android.view.View.OnTouchListener; 35 import android.view.ViewGroup; 36 import android.view.ViewParent; 37 import android.view.WindowManager; 38 import android.widget.AbsListView; 39 import android.widget.AdapterView; 40 import android.widget.AdapterView.OnItemSelectedListener; 41 import android.widget.LinearLayout; 42 import android.widget.ListAdapter; 43 import android.widget.ListView; 44 import android.widget.PopupWindow; 45 46 import androidx.annotation.AttrRes; 47 import androidx.annotation.NonNull; 48 import androidx.annotation.Nullable; 49 import androidx.annotation.RestrictTo; 50 import androidx.annotation.StyleRes; 51 import androidx.appcompat.R; 52 import androidx.appcompat.view.menu.ShowableListMenu; 53 import androidx.core.view.ViewCompat; 54 import androidx.core.widget.PopupWindowCompat; 55 56 import java.lang.reflect.Method; 57 58 /** 59 * Static library support version of the framework's {@link android.widget.ListPopupWindow}. 60 * Used to write apps that run on platforms prior to Android L. When running 61 * on Android L or above, this implementation is still used; it does not try 62 * to switch to the framework's implementation. See the framework SDK 63 * documentation for a class overview. 64 * 65 * @see android.widget.ListPopupWindow 66 */ 67 public class ListPopupWindow implements ShowableListMenu { 68 private static final String TAG = "ListPopupWindow"; 69 private static final boolean DEBUG = false; 70 71 /** 72 * This value controls the length of time that the user 73 * must leave a pointer down without scrolling to expand 74 * the autocomplete dropdown list to cover the IME. 75 */ 76 static final int EXPAND_LIST_TIMEOUT = 250; 77 78 private static Method sClipToWindowEnabledMethod; 79 private static Method sGetMaxAvailableHeightMethod; 80 private static Method sSetEpicenterBoundsMethod; 81 82 static { 83 try { 84 sClipToWindowEnabledMethod = PopupWindow.class.getDeclaredMethod( 85 "setClipToScreenEnabled", boolean.class); 86 } catch (NoSuchMethodException e) { 87 Log.i(TAG, "Could not find method setClipToScreenEnabled() on PopupWindow. Oh well."); 88 } 89 try { 90 sGetMaxAvailableHeightMethod = PopupWindow.class.getDeclaredMethod( 91 "getMaxAvailableHeight", View.class, int.class, boolean.class); 92 } catch (NoSuchMethodException e) { 93 Log.i(TAG, "Could not find method getMaxAvailableHeight(View, int, boolean)" 94 + " on PopupWindow. Oh well."); 95 } 96 try { 97 sSetEpicenterBoundsMethod = PopupWindow.class.getDeclaredMethod( 98 "setEpicenterBounds", Rect.class); 99 } catch (NoSuchMethodException e) { 100 Log.i(TAG, "Could not find method setEpicenterBounds(Rect) on PopupWindow. Oh well."); 101 } 102 } 103 104 private Context mContext; 105 private ListAdapter mAdapter; 106 DropDownListView mDropDownList; 107 108 private int mDropDownHeight = ViewGroup.LayoutParams.WRAP_CONTENT; 109 private int mDropDownWidth = ViewGroup.LayoutParams.WRAP_CONTENT; 110 private int mDropDownHorizontalOffset; 111 private int mDropDownVerticalOffset; 112 private int mDropDownWindowLayoutType = WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL; 113 private boolean mDropDownVerticalOffsetSet; 114 private boolean mIsAnimatedFromAnchor = true; 115 private boolean mOverlapAnchor; 116 private boolean mOverlapAnchorSet; 117 118 private int mDropDownGravity = Gravity.NO_GRAVITY; 119 120 private boolean mDropDownAlwaysVisible = false; 121 private boolean mForceIgnoreOutsideTouch = false; 122 int mListItemExpandMaximum = Integer.MAX_VALUE; 123 124 private View mPromptView; 125 private int mPromptPosition = POSITION_PROMPT_ABOVE; 126 127 private DataSetObserver mObserver; 128 129 private View mDropDownAnchorView; 130 131 private Drawable mDropDownListHighlight; 132 133 private AdapterView.OnItemClickListener mItemClickListener; 134 private OnItemSelectedListener mItemSelectedListener; 135 136 final ResizePopupRunnable mResizePopupRunnable = new ResizePopupRunnable(); 137 private final PopupTouchInterceptor mTouchInterceptor = new PopupTouchInterceptor(); 138 private final PopupScrollListener mScrollListener = new PopupScrollListener(); 139 private final ListSelectorHider mHideSelector = new ListSelectorHider(); 140 private Runnable mShowDropDownRunnable; 141 142 final Handler mHandler; 143 144 private final Rect mTempRect = new Rect(); 145 146 /** 147 * Optional anchor-relative bounds to be used as the transition epicenter. 148 * When {@code null}, the anchor bounds are used as the epicenter. 149 */ 150 private Rect mEpicenterBounds; 151 152 private boolean mModal; 153 154 PopupWindow mPopup; 155 156 /** 157 * The provided prompt view should appear above list content. 158 * 159 * @see #setPromptPosition(int) 160 * @see #getPromptPosition() 161 * @see #setPromptView(View) 162 */ 163 public static final int POSITION_PROMPT_ABOVE = 0; 164 165 /** 166 * The provided prompt view should appear below list content. 167 * 168 * @see #setPromptPosition(int) 169 * @see #getPromptPosition() 170 * @see #setPromptView(View) 171 */ 172 public static final int POSITION_PROMPT_BELOW = 1; 173 174 /** 175 * Alias for {@link ViewGroup.LayoutParams#MATCH_PARENT}. 176 * If used to specify a popup width, the popup will match the width of the anchor view. 177 * If used to specify a popup height, the popup will fill available space. 178 */ 179 public static final int MATCH_PARENT = ViewGroup.LayoutParams.MATCH_PARENT; 180 181 /** 182 * Alias for {@link ViewGroup.LayoutParams#WRAP_CONTENT}. 183 * If used to specify a popup width, the popup will use the width of its content. 184 */ 185 public static final int WRAP_CONTENT = ViewGroup.LayoutParams.WRAP_CONTENT; 186 187 /** 188 * Mode for {@link #setInputMethodMode(int)}: the requirements for the 189 * input method should be based on the focusability of the popup. That is 190 * if it is focusable than it needs to work with the input method, else 191 * it doesn't. 192 */ 193 public static final int INPUT_METHOD_FROM_FOCUSABLE = PopupWindow.INPUT_METHOD_FROM_FOCUSABLE; 194 195 /** 196 * Mode for {@link #setInputMethodMode(int)}: this popup always needs to 197 * work with an input method, regardless of whether it is focusable. This 198 * means that it will always be displayed so that the user can also operate 199 * the input method while it is shown. 200 */ 201 public static final int INPUT_METHOD_NEEDED = PopupWindow.INPUT_METHOD_NEEDED; 202 203 /** 204 * Mode for {@link #setInputMethodMode(int)}: this popup never needs to 205 * work with an input method, regardless of whether it is focusable. This 206 * means that it will always be displayed to use as much space on the 207 * screen as needed, regardless of whether this covers the input method. 208 */ 209 public static final int INPUT_METHOD_NOT_NEEDED = PopupWindow.INPUT_METHOD_NOT_NEEDED; 210 211 /** 212 * Create a new, empty popup window capable of displaying items from a ListAdapter. 213 * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. 214 * 215 * @param context Context used for contained views. 216 */ ListPopupWindow(@onNull Context context)217 public ListPopupWindow(@NonNull Context context) { 218 this(context, null, R.attr.listPopupWindowStyle); 219 } 220 221 /** 222 * Create a new, empty popup window capable of displaying items from a ListAdapter. 223 * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. 224 * 225 * @param context Context used for contained views. 226 * @param attrs Attributes from inflating parent views used to style the popup. 227 */ ListPopupWindow(@onNull Context context, @Nullable AttributeSet attrs)228 public ListPopupWindow(@NonNull Context context, @Nullable AttributeSet attrs) { 229 this(context, attrs, R.attr.listPopupWindowStyle); 230 } 231 232 /** 233 * Create a new, empty popup window capable of displaying items from a ListAdapter. 234 * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. 235 * 236 * @param context Context used for contained views. 237 * @param attrs Attributes from inflating parent views used to style the popup. 238 * @param defStyleAttr Default style attribute to use for popup content. 239 */ ListPopupWindow(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr)240 public ListPopupWindow(@NonNull Context context, @Nullable AttributeSet attrs, 241 @AttrRes int defStyleAttr) { 242 this(context, attrs, defStyleAttr, 0); 243 } 244 245 /** 246 * Create a new, empty popup window capable of displaying items from a ListAdapter. 247 * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. 248 * 249 * @param context Context used for contained views. 250 * @param attrs Attributes from inflating parent views used to style the popup. 251 * @param defStyleAttr Style attribute to read for default styling of popup content. 252 * @param defStyleRes Style resource ID to use for default styling of popup content. 253 */ ListPopupWindow(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes)254 public ListPopupWindow(@NonNull Context context, @Nullable AttributeSet attrs, 255 @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { 256 mContext = context; 257 mHandler = new Handler(context.getMainLooper()); 258 259 final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ListPopupWindow, 260 defStyleAttr, defStyleRes); 261 mDropDownHorizontalOffset = a.getDimensionPixelOffset( 262 R.styleable.ListPopupWindow_android_dropDownHorizontalOffset, 0); 263 mDropDownVerticalOffset = a.getDimensionPixelOffset( 264 R.styleable.ListPopupWindow_android_dropDownVerticalOffset, 0); 265 if (mDropDownVerticalOffset != 0) { 266 mDropDownVerticalOffsetSet = true; 267 } 268 a.recycle(); 269 270 mPopup = new AppCompatPopupWindow(context, attrs, defStyleAttr, defStyleRes); 271 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); 272 } 273 274 /** 275 * Sets the adapter that provides the data and the views to represent the data 276 * in this popup window. 277 * 278 * @param adapter The adapter to use to create this window's content. 279 */ setAdapter(@ullable ListAdapter adapter)280 public void setAdapter(@Nullable ListAdapter adapter) { 281 if (mObserver == null) { 282 mObserver = new PopupDataSetObserver(); 283 } else if (mAdapter != null) { 284 mAdapter.unregisterDataSetObserver(mObserver); 285 } 286 mAdapter = adapter; 287 if (adapter != null) { 288 adapter.registerDataSetObserver(mObserver); 289 } 290 291 if (mDropDownList != null) { 292 mDropDownList.setAdapter(mAdapter); 293 } 294 } 295 296 /** 297 * Set where the optional prompt view should appear. The default is 298 * {@link #POSITION_PROMPT_ABOVE}. 299 * 300 * @param position A position constant declaring where the prompt should be displayed. 301 * 302 * @see #POSITION_PROMPT_ABOVE 303 * @see #POSITION_PROMPT_BELOW 304 */ setPromptPosition(int position)305 public void setPromptPosition(int position) { 306 mPromptPosition = position; 307 } 308 309 /** 310 * @return Where the optional prompt view should appear. 311 * 312 * @see #POSITION_PROMPT_ABOVE 313 * @see #POSITION_PROMPT_BELOW 314 */ getPromptPosition()315 public int getPromptPosition() { 316 return mPromptPosition; 317 } 318 319 /** 320 * Set whether this window should be modal when shown. 321 * 322 * <p>If a popup window is modal, it will receive all touch and key input. 323 * If the user touches outside the popup window's content area the popup window 324 * will be dismissed. 325 * 326 * @param modal {@code true} if the popup window should be modal, {@code false} otherwise. 327 */ setModal(boolean modal)328 public void setModal(boolean modal) { 329 mModal = modal; 330 mPopup.setFocusable(modal); 331 } 332 333 /** 334 * Returns whether the popup window will be modal when shown. 335 * 336 * @return {@code true} if the popup window will be modal, {@code false} otherwise. 337 */ isModal()338 public boolean isModal() { 339 return mModal; 340 } 341 342 /** 343 * Forces outside touches to be ignored. Normally if {@link #isDropDownAlwaysVisible()} is 344 * false, we allow outside touch to dismiss the dropdown. If this is set to true, then we 345 * ignore outside touch even when the drop down is not set to always visible. 346 * 347 * @hide Used only by AutoCompleteTextView to handle some internal special cases. 348 */ 349 @RestrictTo(LIBRARY_GROUP) setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch)350 public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) { 351 mForceIgnoreOutsideTouch = forceIgnoreOutsideTouch; 352 } 353 354 /** 355 * Sets whether the drop-down should remain visible under certain conditions. 356 * 357 * The drop-down will occupy the entire screen below {@link #getAnchorView} regardless 358 * of the size or content of the list. {@link #getBackground()} will fill any space 359 * that is not used by the list. 360 * 361 * @param dropDownAlwaysVisible Whether to keep the drop-down visible. 362 * 363 * @hide Only used by AutoCompleteTextView under special conditions. 364 */ 365 @RestrictTo(LIBRARY_GROUP) setDropDownAlwaysVisible(boolean dropDownAlwaysVisible)366 public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) { 367 mDropDownAlwaysVisible = dropDownAlwaysVisible; 368 } 369 370 /** 371 * @return Whether the drop-down is visible under special conditions. 372 * 373 * @hide Only used by AutoCompleteTextView under special conditions. 374 */ 375 @RestrictTo(LIBRARY_GROUP) isDropDownAlwaysVisible()376 public boolean isDropDownAlwaysVisible() { 377 return mDropDownAlwaysVisible; 378 } 379 380 /** 381 * Sets the operating mode for the soft input area. 382 * 383 * @param mode The desired mode, see 384 * {@link android.view.WindowManager.LayoutParams#softInputMode} 385 * for the full list 386 * 387 * @see android.view.WindowManager.LayoutParams#softInputMode 388 * @see #getSoftInputMode() 389 */ setSoftInputMode(int mode)390 public void setSoftInputMode(int mode) { 391 mPopup.setSoftInputMode(mode); 392 } 393 394 /** 395 * Returns the current value in {@link #setSoftInputMode(int)}. 396 * 397 * @see #setSoftInputMode(int) 398 * @see android.view.WindowManager.LayoutParams#softInputMode 399 */ getSoftInputMode()400 public int getSoftInputMode() { 401 return mPopup.getSoftInputMode(); 402 } 403 404 /** 405 * Sets a drawable to use as the list item selector. 406 * 407 * @param selector List selector drawable to use in the popup. 408 */ setListSelector(Drawable selector)409 public void setListSelector(Drawable selector) { 410 mDropDownListHighlight = selector; 411 } 412 413 /** 414 * @return The background drawable for the popup window. 415 */ getBackground()416 public @Nullable Drawable getBackground() { 417 return mPopup.getBackground(); 418 } 419 420 /** 421 * Sets a drawable to be the background for the popup window. 422 * 423 * @param d A drawable to set as the background. 424 */ setBackgroundDrawable(@ullable Drawable d)425 public void setBackgroundDrawable(@Nullable Drawable d) { 426 mPopup.setBackgroundDrawable(d); 427 } 428 429 /** 430 * Set an animation style to use when the popup window is shown or dismissed. 431 * 432 * @param animationStyle Animation style to use. 433 */ setAnimationStyle(@tyleRes int animationStyle)434 public void setAnimationStyle(@StyleRes int animationStyle) { 435 mPopup.setAnimationStyle(animationStyle); 436 } 437 438 /** 439 * Returns the animation style that will be used when the popup window is 440 * shown or dismissed. 441 * 442 * @return Animation style that will be used. 443 */ getAnimationStyle()444 public @StyleRes int getAnimationStyle() { 445 return mPopup.getAnimationStyle(); 446 } 447 448 /** 449 * Returns the view that will be used to anchor this popup. 450 * 451 * @return The popup's anchor view 452 */ getAnchorView()453 public @Nullable View getAnchorView() { 454 return mDropDownAnchorView; 455 } 456 457 /** 458 * Sets the popup's anchor view. This popup will always be positioned relative to 459 * the anchor view when shown. 460 * 461 * @param anchor The view to use as an anchor. 462 */ setAnchorView(@ullable View anchor)463 public void setAnchorView(@Nullable View anchor) { 464 mDropDownAnchorView = anchor; 465 } 466 467 /** 468 * @return The horizontal offset of the popup from its anchor in pixels. 469 */ getHorizontalOffset()470 public int getHorizontalOffset() { 471 return mDropDownHorizontalOffset; 472 } 473 474 /** 475 * Set the horizontal offset of this popup from its anchor view in pixels. 476 * 477 * @param offset The horizontal offset of the popup from its anchor. 478 */ setHorizontalOffset(int offset)479 public void setHorizontalOffset(int offset) { 480 mDropDownHorizontalOffset = offset; 481 } 482 483 /** 484 * @return The vertical offset of the popup from its anchor in pixels. 485 */ getVerticalOffset()486 public int getVerticalOffset() { 487 if (!mDropDownVerticalOffsetSet) { 488 return 0; 489 } 490 return mDropDownVerticalOffset; 491 } 492 493 /** 494 * Set the vertical offset of this popup from its anchor view in pixels. 495 * 496 * @param offset The vertical offset of the popup from its anchor. 497 */ setVerticalOffset(int offset)498 public void setVerticalOffset(int offset) { 499 mDropDownVerticalOffset = offset; 500 mDropDownVerticalOffsetSet = true; 501 } 502 503 /** 504 * Specifies the anchor-relative bounds of the popup's transition 505 * epicenter. 506 * 507 * @param bounds anchor-relative bounds 508 * @hide 509 */ 510 @RestrictTo(LIBRARY_GROUP) setEpicenterBounds(Rect bounds)511 public void setEpicenterBounds(Rect bounds) { 512 mEpicenterBounds = bounds; 513 } 514 515 /** 516 * Set the gravity of the dropdown list. This is commonly used to 517 * set gravity to START or END for alignment with the anchor. 518 * 519 * @param gravity Gravity value to use 520 */ setDropDownGravity(int gravity)521 public void setDropDownGravity(int gravity) { 522 mDropDownGravity = gravity; 523 } 524 525 /** 526 * @return The width of the popup window in pixels. 527 */ getWidth()528 public int getWidth() { 529 return mDropDownWidth; 530 } 531 532 /** 533 * Sets the width of the popup window in pixels. Can also be {@link #MATCH_PARENT} 534 * or {@link #WRAP_CONTENT}. 535 * 536 * @param width Width of the popup window. 537 */ setWidth(int width)538 public void setWidth(int width) { 539 mDropDownWidth = width; 540 } 541 542 /** 543 * Sets the width of the popup window by the size of its content. The final width may be 544 * larger to accommodate styled window dressing. 545 * 546 * @param width Desired width of content in pixels. 547 */ setContentWidth(int width)548 public void setContentWidth(int width) { 549 Drawable popupBackground = mPopup.getBackground(); 550 if (popupBackground != null) { 551 popupBackground.getPadding(mTempRect); 552 mDropDownWidth = mTempRect.left + mTempRect.right + width; 553 } else { 554 setWidth(width); 555 } 556 } 557 558 /** 559 * @return The height of the popup window in pixels. 560 */ getHeight()561 public int getHeight() { 562 return mDropDownHeight; 563 } 564 565 /** 566 * Sets the height of the popup window in pixels. Can also be {@link #MATCH_PARENT}. 567 * 568 * @param height Height of the popup window must be a positive value, 569 * {@link #MATCH_PARENT}, or {@link #WRAP_CONTENT}. 570 * 571 * @throws IllegalArgumentException if height is set to negative value 572 */ setHeight(int height)573 public void setHeight(int height) { 574 if (height < 0 && ViewGroup.LayoutParams.WRAP_CONTENT != height 575 && ViewGroup.LayoutParams.MATCH_PARENT != height) { 576 throw new IllegalArgumentException( 577 "Invalid height. Must be a positive value, MATCH_PARENT, or WRAP_CONTENT."); 578 } 579 mDropDownHeight = height; 580 } 581 582 /** 583 * Set the layout type for this popup window. 584 * <p> 585 * See {@link WindowManager.LayoutParams#type} for possible values. 586 * 587 * @param layoutType Layout type for this window. 588 * 589 * @see WindowManager.LayoutParams#type 590 */ setWindowLayoutType(int layoutType)591 public void setWindowLayoutType(int layoutType) { 592 mDropDownWindowLayoutType = layoutType; 593 } 594 595 /** 596 * Sets a listener to receive events when a list item is clicked. 597 * 598 * @param clickListener Listener to register 599 * 600 * @see ListView#setOnItemClickListener(android.widget.AdapterView.OnItemClickListener) 601 */ setOnItemClickListener(@ullable AdapterView.OnItemClickListener clickListener)602 public void setOnItemClickListener(@Nullable AdapterView.OnItemClickListener clickListener) { 603 mItemClickListener = clickListener; 604 } 605 606 /** 607 * Sets a listener to receive events when a list item is selected. 608 * 609 * @param selectedListener Listener to register. 610 * 611 * @see ListView#setOnItemSelectedListener(OnItemSelectedListener) 612 */ setOnItemSelectedListener(@ullable OnItemSelectedListener selectedListener)613 public void setOnItemSelectedListener(@Nullable OnItemSelectedListener selectedListener) { 614 mItemSelectedListener = selectedListener; 615 } 616 617 /** 618 * Set a view to act as a user prompt for this popup window. Where the prompt view will appear 619 * is controlled by {@link #setPromptPosition(int)}. 620 * 621 * @param prompt View to use as an informational prompt. 622 */ setPromptView(@ullable View prompt)623 public void setPromptView(@Nullable View prompt) { 624 boolean showing = isShowing(); 625 if (showing) { 626 removePromptView(); 627 } 628 mPromptView = prompt; 629 if (showing) { 630 show(); 631 } 632 } 633 634 /** 635 * Post a {@link #show()} call to the UI thread. 636 */ postShow()637 public void postShow() { 638 mHandler.post(mShowDropDownRunnable); 639 } 640 641 /** 642 * Show the popup list. If the list is already showing, this method 643 * will recalculate the popup's size and position. 644 */ 645 @Override show()646 public void show() { 647 int height = buildDropDown(); 648 649 final boolean noInputMethod = isInputMethodNotNeeded(); 650 PopupWindowCompat.setWindowLayoutType(mPopup, mDropDownWindowLayoutType); 651 652 if (mPopup.isShowing()) { 653 if (!ViewCompat.isAttachedToWindow(getAnchorView())) { 654 //Don't update position if the anchor view is detached from window. 655 return; 656 } 657 final int widthSpec; 658 if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) { 659 // The call to PopupWindow's update method below can accept -1 for any 660 // value you do not want to update. 661 widthSpec = -1; 662 } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { 663 widthSpec = getAnchorView().getWidth(); 664 } else { 665 widthSpec = mDropDownWidth; 666 } 667 668 final int heightSpec; 669 if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { 670 // The call to PopupWindow's update method below can accept -1 for any 671 // value you do not want to update. 672 heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT; 673 if (noInputMethod) { 674 mPopup.setWidth(mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ? 675 ViewGroup.LayoutParams.MATCH_PARENT : 0); 676 mPopup.setHeight(0); 677 } else { 678 mPopup.setWidth(mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ? 679 ViewGroup.LayoutParams.MATCH_PARENT : 0); 680 mPopup.setHeight(ViewGroup.LayoutParams.MATCH_PARENT); 681 } 682 } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { 683 heightSpec = height; 684 } else { 685 heightSpec = mDropDownHeight; 686 } 687 688 mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible); 689 690 mPopup.update(getAnchorView(), mDropDownHorizontalOffset, 691 mDropDownVerticalOffset, (widthSpec < 0)? -1 : widthSpec, 692 (heightSpec < 0)? -1 : heightSpec); 693 } else { 694 final int widthSpec; 695 if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) { 696 widthSpec = ViewGroup.LayoutParams.MATCH_PARENT; 697 } else { 698 if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { 699 widthSpec = getAnchorView().getWidth(); 700 } else { 701 widthSpec = mDropDownWidth; 702 } 703 } 704 705 final int heightSpec; 706 if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { 707 heightSpec = ViewGroup.LayoutParams.MATCH_PARENT; 708 } else { 709 if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { 710 heightSpec = height; 711 } else { 712 heightSpec = mDropDownHeight; 713 } 714 } 715 716 mPopup.setWidth(widthSpec); 717 mPopup.setHeight(heightSpec); 718 setPopupClipToScreenEnabled(true); 719 720 // use outside touchable to dismiss drop down when touching outside of it, so 721 // only set this if the dropdown is not always visible 722 mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible); 723 mPopup.setTouchInterceptor(mTouchInterceptor); 724 if (mOverlapAnchorSet) { 725 PopupWindowCompat.setOverlapAnchor(mPopup, mOverlapAnchor); 726 } 727 if (sSetEpicenterBoundsMethod != null) { 728 try { 729 sSetEpicenterBoundsMethod.invoke(mPopup, mEpicenterBounds); 730 } catch (Exception e) { 731 Log.e(TAG, "Could not invoke setEpicenterBounds on PopupWindow", e); 732 } 733 } 734 PopupWindowCompat.showAsDropDown(mPopup, getAnchorView(), mDropDownHorizontalOffset, 735 mDropDownVerticalOffset, mDropDownGravity); 736 mDropDownList.setSelection(ListView.INVALID_POSITION); 737 738 if (!mModal || mDropDownList.isInTouchMode()) { 739 clearListSelection(); 740 } 741 if (!mModal) { 742 mHandler.post(mHideSelector); 743 } 744 } 745 } 746 747 /** 748 * Dismiss the popup window. 749 */ 750 @Override 751 public void dismiss() { 752 mPopup.dismiss(); 753 removePromptView(); 754 mPopup.setContentView(null); 755 mDropDownList = null; 756 mHandler.removeCallbacks(mResizePopupRunnable); 757 } 758 759 /** 760 * Set a listener to receive a callback when the popup is dismissed. 761 * 762 * @param listener Listener that will be notified when the popup is dismissed. 763 */ 764 public void setOnDismissListener(@Nullable PopupWindow.OnDismissListener listener) { 765 mPopup.setOnDismissListener(listener); 766 } 767 768 private void removePromptView() { 769 if (mPromptView != null) { 770 final ViewParent parent = mPromptView.getParent(); 771 if (parent instanceof ViewGroup) { 772 final ViewGroup group = (ViewGroup) parent; 773 group.removeView(mPromptView); 774 } 775 } 776 } 777 778 /** 779 * Control how the popup operates with an input method: one of 780 * {@link #INPUT_METHOD_FROM_FOCUSABLE}, {@link #INPUT_METHOD_NEEDED}, 781 * or {@link #INPUT_METHOD_NOT_NEEDED}. 782 * 783 * <p>If the popup is showing, calling this method will take effect only 784 * the next time the popup is shown or through a manual call to the {@link #show()} 785 * method.</p> 786 * 787 * @see #getInputMethodMode() 788 * @see #show() 789 */ 790 public void setInputMethodMode(int mode) { 791 mPopup.setInputMethodMode(mode); 792 } 793 794 /** 795 * Return the current value in {@link #setInputMethodMode(int)}. 796 * 797 * @see #setInputMethodMode(int) 798 */ 799 public int getInputMethodMode() { 800 return mPopup.getInputMethodMode(); 801 } 802 803 /** 804 * Set the selected position of the list. 805 * Only valid when {@link #isShowing()} == {@code true}. 806 * 807 * @param position List position to set as selected. 808 */ 809 public void setSelection(int position) { 810 DropDownListView list = mDropDownList; 811 if (isShowing() && list != null) { 812 list.setListSelectionHidden(false); 813 list.setSelection(position); 814 815 if (list.getChoiceMode() != ListView.CHOICE_MODE_NONE) { 816 list.setItemChecked(position, true); 817 } 818 } 819 } 820 821 /** 822 * Clear any current list selection. 823 * Only valid when {@link #isShowing()} == {@code true}. 824 */ 825 public void clearListSelection() { 826 final DropDownListView list = mDropDownList; 827 if (list != null) { 828 // WARNING: Please read the comment where mListSelectionHidden is declared 829 list.setListSelectionHidden(true); 830 //list.hideSelector(); 831 list.requestLayout(); 832 } 833 } 834 835 /** 836 * @return {@code true} if the popup is currently showing, {@code false} otherwise. 837 */ 838 @Override 839 public boolean isShowing() { 840 return mPopup.isShowing(); 841 } 842 843 /** 844 * @return {@code true} if this popup is configured to assume the user does not need 845 * to interact with the IME while it is showing, {@code false} otherwise. 846 */ 847 public boolean isInputMethodNotNeeded() { 848 return mPopup.getInputMethodMode() == INPUT_METHOD_NOT_NEEDED; 849 } 850 851 /** 852 * Perform an item click operation on the specified list adapter position. 853 * 854 * @param position Adapter position for performing the click 855 * @return true if the click action could be performed, false if not. 856 * (e.g. if the popup was not showing, this method would return false.) 857 */ 858 public boolean performItemClick(int position) { 859 if (isShowing()) { 860 if (mItemClickListener != null) { 861 final DropDownListView list = mDropDownList; 862 final View child = list.getChildAt(position - list.getFirstVisiblePosition()); 863 final ListAdapter adapter = list.getAdapter(); 864 mItemClickListener.onItemClick(list, child, position, adapter.getItemId(position)); 865 } 866 return true; 867 } 868 return false; 869 } 870 871 /** 872 * @return The currently selected item or null if the popup is not showing. 873 */ 874 public @Nullable Object getSelectedItem() { 875 if (!isShowing()) { 876 return null; 877 } 878 return mDropDownList.getSelectedItem(); 879 } 880 881 /** 882 * @return The position of the currently selected item or {@link ListView#INVALID_POSITION} 883 * if {@link #isShowing()} == {@code false}. 884 * 885 * @see ListView#getSelectedItemPosition() 886 */ 887 public int getSelectedItemPosition() { 888 if (!isShowing()) { 889 return ListView.INVALID_POSITION; 890 } 891 return mDropDownList.getSelectedItemPosition(); 892 } 893 894 /** 895 * @return The ID of the currently selected item or {@link ListView#INVALID_ROW_ID} 896 * if {@link #isShowing()} == {@code false}. 897 * 898 * @see ListView#getSelectedItemId() 899 */ 900 public long getSelectedItemId() { 901 if (!isShowing()) { 902 return ListView.INVALID_ROW_ID; 903 } 904 return mDropDownList.getSelectedItemId(); 905 } 906 907 /** 908 * @return The View for the currently selected item or null if 909 * {@link #isShowing()} == {@code false}. 910 * 911 * @see ListView#getSelectedView() 912 */ 913 public @Nullable View getSelectedView() { 914 if (!isShowing()) { 915 return null; 916 } 917 return mDropDownList.getSelectedView(); 918 } 919 920 /** 921 * @return The {@link ListView} displayed within the popup window. 922 * Only valid when {@link #isShowing()} == {@code true}. 923 */ 924 @Override 925 public @Nullable ListView getListView() { 926 return mDropDownList; 927 } 928 929 @NonNull DropDownListView createDropDownListView(Context context, boolean hijackFocus) { 930 return new DropDownListView(context, hijackFocus); 931 } 932 933 /** 934 * The maximum number of list items that can be visible and still have 935 * the list expand when touched. 936 * 937 * @param max Max number of items that can be visible and still allow the list to expand. 938 */ 939 void setListItemExpandMax(int max) { 940 mListItemExpandMaximum = max; 941 } 942 943 /** 944 * Filter key down events. By forwarding key down events to this function, 945 * views using non-modal ListPopupWindow can have it handle key selection of items. 946 * 947 * @param keyCode keyCode param passed to the host view's onKeyDown 948 * @param event event param passed to the host view's onKeyDown 949 * @return true if the event was handled, false if it was ignored. 950 * 951 * @see #setModal(boolean) 952 * @see #onKeyUp(int, KeyEvent) 953 */ 954 public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) { 955 // when the drop down is shown, we drive it directly 956 if (isShowing()) { 957 // the key events are forwarded to the list in the drop down view 958 // note that ListView handles space but we don't want that to happen 959 // also if selection is not currently in the drop down, then don't 960 // let center or enter presses go there since that would cause it 961 // to select one of its items 962 if (keyCode != KeyEvent.KEYCODE_SPACE 963 && (mDropDownList.getSelectedItemPosition() >= 0 964 || !isConfirmKey(keyCode))) { 965 int curIndex = mDropDownList.getSelectedItemPosition(); 966 boolean consumed; 967 968 final boolean below = !mPopup.isAboveAnchor(); 969 970 final ListAdapter adapter = mAdapter; 971 972 boolean allEnabled; 973 int firstItem = Integer.MAX_VALUE; 974 int lastItem = Integer.MIN_VALUE; 975 976 if (adapter != null) { 977 allEnabled = adapter.areAllItemsEnabled(); 978 firstItem = allEnabled ? 0 : 979 mDropDownList.lookForSelectablePosition(0, true); 980 lastItem = allEnabled ? adapter.getCount() - 1 : 981 mDropDownList.lookForSelectablePosition(adapter.getCount() - 1, false); 982 } 983 984 if ((below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex <= firstItem) || 985 (!below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN && curIndex >= lastItem)) { 986 // When the selection is at the top, we block the key 987 // event to prevent focus from moving. 988 clearListSelection(); 989 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); 990 show(); 991 return true; 992 } else { 993 // WARNING: Please read the comment where mListSelectionHidden 994 // is declared 995 mDropDownList.setListSelectionHidden(false); 996 } 997 998 consumed = mDropDownList.onKeyDown(keyCode, event); 999 if (DEBUG) Log.v(TAG, "Key down: code=" + keyCode + " list consumed=" + consumed); 1000 1001 if (consumed) { 1002 // If it handled the key event, then the user is 1003 // navigating in the list, so we should put it in front. 1004 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 1005 // Here's a little trick we need to do to make sure that 1006 // the list view is actually showing its focus indicator, 1007 // by ensuring it has focus and getting its window out 1008 // of touch mode. 1009 mDropDownList.requestFocusFromTouch(); 1010 show(); 1011 1012 switch (keyCode) { 1013 // avoid passing the focus from the text view to the 1014 // next component 1015 case KeyEvent.KEYCODE_ENTER: 1016 case KeyEvent.KEYCODE_DPAD_CENTER: 1017 case KeyEvent.KEYCODE_DPAD_DOWN: 1018 case KeyEvent.KEYCODE_DPAD_UP: 1019 return true; 1020 } 1021 } else { 1022 if (below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { 1023 // when the selection is at the bottom, we block the 1024 // event to avoid going to the next focusable widget 1025 if (curIndex == lastItem) { 1026 return true; 1027 } 1028 } else if (!below && keyCode == KeyEvent.KEYCODE_DPAD_UP && 1029 curIndex == firstItem) { 1030 return true; 1031 } 1032 } 1033 } 1034 } 1035 1036 return false; 1037 } 1038 1039 /** 1040 * Filter key up events. By forwarding key up events to this function, 1041 * views using non-modal ListPopupWindow can have it handle key selection of items. 1042 * 1043 * @param keyCode keyCode param passed to the host view's onKeyUp 1044 * @param event event param passed to the host view's onKeyUp 1045 * @return true if the event was handled, false if it was ignored. 1046 * 1047 * @see #setModal(boolean) 1048 * @see #onKeyDown(int, KeyEvent) 1049 */ 1050 public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) { 1051 if (isShowing() && mDropDownList.getSelectedItemPosition() >= 0) { 1052 boolean consumed = mDropDownList.onKeyUp(keyCode, event); 1053 if (consumed && isConfirmKey(keyCode)) { 1054 // if the list accepts the key events and the key event was a click, the text view 1055 // gets the selected item from the drop down as its content 1056 dismiss(); 1057 } 1058 return consumed; 1059 } 1060 return false; 1061 } 1062 1063 /** 1064 * Filter pre-IME key events. By forwarding {@link View#onKeyPreIme(int, KeyEvent)} 1065 * events to this function, views using ListPopupWindow can have it dismiss the popup 1066 * when the back key is pressed. 1067 * 1068 * @param keyCode keyCode param passed to the host view's onKeyPreIme 1069 * @param event event param passed to the host view's onKeyPreIme 1070 * @return true if the event was handled, false if it was ignored. 1071 * 1072 * @see #setModal(boolean) 1073 */ onKeyPreIme(int keyCode, @NonNull KeyEvent event)1074 public boolean onKeyPreIme(int keyCode, @NonNull KeyEvent event) { 1075 if (keyCode == KeyEvent.KEYCODE_BACK && isShowing()) { 1076 // special case for the back key, we do not even try to send it 1077 // to the drop down list but instead, consume it immediately 1078 final View anchorView = mDropDownAnchorView; 1079 if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { 1080 KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState(); 1081 if (state != null) { 1082 state.startTracking(event, this); 1083 } 1084 return true; 1085 } else if (event.getAction() == KeyEvent.ACTION_UP) { 1086 KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState(); 1087 if (state != null) { 1088 state.handleUpEvent(event); 1089 } 1090 if (event.isTracking() && !event.isCanceled()) { 1091 dismiss(); 1092 return true; 1093 } 1094 } 1095 } 1096 return false; 1097 } 1098 1099 /** 1100 * Returns an {@link OnTouchListener} that can be added to the source view 1101 * to implement drag-to-open behavior. Generally, the source view should be 1102 * the same view that was passed to {@link #setAnchorView}. 1103 * <p> 1104 * When the listener is set on a view, touching that view and dragging 1105 * outside of its bounds will open the popup window. Lifting will select the 1106 * currently touched list item. 1107 * <p> 1108 * Example usage: 1109 * <pre> 1110 * ListPopupWindow myPopup = new ListPopupWindow(context); 1111 * myPopup.setAnchor(myAnchor); 1112 * OnTouchListener dragListener = myPopup.createDragToOpenListener(myAnchor); 1113 * myAnchor.setOnTouchListener(dragListener); 1114 * </pre> 1115 * 1116 * @param src the view on which the resulting listener will be set 1117 * @return a touch listener that controls drag-to-open behavior 1118 */ createDragToOpenListener(View src)1119 public OnTouchListener createDragToOpenListener(View src) { 1120 return new ForwardingListener(src) { 1121 @Override 1122 public ListPopupWindow getPopup() { 1123 return ListPopupWindow.this; 1124 } 1125 }; 1126 } 1127 1128 /** 1129 * <p>Builds the popup window's content and returns the height the popup 1130 * should have. Returns -1 when the content already exists.</p> 1131 * 1132 * @return the content's height or -1 if content already exists 1133 */ buildDropDown()1134 private int buildDropDown() { 1135 ViewGroup dropDownView; 1136 int otherHeights = 0; 1137 1138 if (mDropDownList == null) { 1139 Context context = mContext; 1140 1141 /** 1142 * This Runnable exists for the sole purpose of checking if the view layout has got 1143 * completed and if so call showDropDown to display the drop down. This is used to show 1144 * the drop down as soon as possible after user opens up the search dialog, without 1145 * waiting for the normal UI pipeline to do its job which is slower than this method. 1146 */ 1147 mShowDropDownRunnable = new Runnable() { 1148 @Override 1149 public void run() { 1150 // View layout should be all done before displaying the drop down. 1151 View view = getAnchorView(); 1152 if (view != null && view.getWindowToken() != null) { 1153 show(); 1154 } 1155 } 1156 }; 1157 1158 mDropDownList = createDropDownListView(context, !mModal); 1159 if (mDropDownListHighlight != null) { 1160 mDropDownList.setSelector(mDropDownListHighlight); 1161 } 1162 mDropDownList.setAdapter(mAdapter); 1163 mDropDownList.setOnItemClickListener(mItemClickListener); 1164 mDropDownList.setFocusable(true); 1165 mDropDownList.setFocusableInTouchMode(true); 1166 mDropDownList.setOnItemSelectedListener(new OnItemSelectedListener() { 1167 @Override 1168 public void onItemSelected(AdapterView<?> parent, View view, 1169 int position, long id) { 1170 1171 if (position != -1) { 1172 DropDownListView dropDownList = mDropDownList; 1173 1174 if (dropDownList != null) { 1175 dropDownList.setListSelectionHidden(false); 1176 } 1177 } 1178 } 1179 1180 @Override 1181 public void onNothingSelected(AdapterView<?> parent) { 1182 } 1183 }); 1184 mDropDownList.setOnScrollListener(mScrollListener); 1185 1186 if (mItemSelectedListener != null) { 1187 mDropDownList.setOnItemSelectedListener(mItemSelectedListener); 1188 } 1189 1190 dropDownView = mDropDownList; 1191 1192 View hintView = mPromptView; 1193 if (hintView != null) { 1194 // if a hint has been specified, we accommodate more space for it and 1195 // add a text view in the drop down menu, at the bottom of the list 1196 LinearLayout hintContainer = new LinearLayout(context); 1197 hintContainer.setOrientation(LinearLayout.VERTICAL); 1198 1199 LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams( 1200 ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f 1201 ); 1202 1203 switch (mPromptPosition) { 1204 case POSITION_PROMPT_BELOW: 1205 hintContainer.addView(dropDownView, hintParams); 1206 hintContainer.addView(hintView); 1207 break; 1208 1209 case POSITION_PROMPT_ABOVE: 1210 hintContainer.addView(hintView); 1211 hintContainer.addView(dropDownView, hintParams); 1212 break; 1213 1214 default: 1215 Log.e(TAG, "Invalid hint position " + mPromptPosition); 1216 break; 1217 } 1218 1219 // Measure the hint's height to find how much more vertical 1220 // space we need to add to the drop down's height. 1221 final int widthSize; 1222 final int widthMode; 1223 if (mDropDownWidth >= 0) { 1224 widthMode = MeasureSpec.AT_MOST; 1225 widthSize = mDropDownWidth; 1226 } else { 1227 widthMode = MeasureSpec.UNSPECIFIED; 1228 widthSize = 0; 1229 } 1230 final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode); 1231 final int heightSpec = MeasureSpec.UNSPECIFIED; 1232 hintView.measure(widthSpec, heightSpec); 1233 1234 hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams(); 1235 otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin 1236 + hintParams.bottomMargin; 1237 1238 dropDownView = hintContainer; 1239 } 1240 1241 mPopup.setContentView(dropDownView); 1242 } else { 1243 dropDownView = (ViewGroup) mPopup.getContentView(); 1244 final View view = mPromptView; 1245 if (view != null) { 1246 LinearLayout.LayoutParams hintParams = 1247 (LinearLayout.LayoutParams) view.getLayoutParams(); 1248 otherHeights = view.getMeasuredHeight() + hintParams.topMargin 1249 + hintParams.bottomMargin; 1250 } 1251 } 1252 1253 // getMaxAvailableHeight() subtracts the padding, so we put it back 1254 // to get the available height for the whole window. 1255 final int padding; 1256 final Drawable background = mPopup.getBackground(); 1257 if (background != null) { 1258 background.getPadding(mTempRect); 1259 padding = mTempRect.top + mTempRect.bottom; 1260 1261 // If we don't have an explicit vertical offset, determine one from 1262 // the window background so that content will line up. 1263 if (!mDropDownVerticalOffsetSet) { 1264 mDropDownVerticalOffset = -mTempRect.top; 1265 } 1266 } else { 1267 mTempRect.setEmpty(); 1268 padding = 0; 1269 } 1270 1271 // Max height available on the screen for a popup. 1272 final boolean ignoreBottomDecorations = 1273 mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED; 1274 final int maxHeight = getMaxAvailableHeight(getAnchorView(), mDropDownVerticalOffset, 1275 ignoreBottomDecorations); 1276 if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { 1277 return maxHeight + padding; 1278 } 1279 1280 final int childWidthSpec; 1281 switch (mDropDownWidth) { 1282 case ViewGroup.LayoutParams.WRAP_CONTENT: 1283 childWidthSpec = MeasureSpec.makeMeasureSpec( 1284 mContext.getResources().getDisplayMetrics().widthPixels 1285 - (mTempRect.left + mTempRect.right), 1286 MeasureSpec.AT_MOST); 1287 break; 1288 case ViewGroup.LayoutParams.MATCH_PARENT: 1289 childWidthSpec = MeasureSpec.makeMeasureSpec( 1290 mContext.getResources().getDisplayMetrics().widthPixels 1291 - (mTempRect.left + mTempRect.right), 1292 MeasureSpec.EXACTLY); 1293 break; 1294 default: 1295 childWidthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.EXACTLY); 1296 break; 1297 } 1298 1299 // Add padding only if the list has items in it, that way we don't show 1300 // the popup if it is not needed. 1301 final int listContent = mDropDownList.measureHeightOfChildrenCompat(childWidthSpec, 1302 0, DropDownListView.NO_POSITION, maxHeight - otherHeights, -1); 1303 if (listContent > 0) { 1304 final int listPadding = mDropDownList.getPaddingTop() 1305 + mDropDownList.getPaddingBottom(); 1306 otherHeights += padding + listPadding; 1307 } 1308 1309 return listContent + otherHeights; 1310 } 1311 1312 /** 1313 * @hide Only used by {@link androidx.appcompat.view.menu.CascadingMenuPopup} to position 1314 * a submenu correctly. 1315 */ 1316 @RestrictTo(LIBRARY_GROUP) setOverlapAnchor(boolean overlapAnchor)1317 public void setOverlapAnchor(boolean overlapAnchor) { 1318 mOverlapAnchorSet = true; 1319 mOverlapAnchor = overlapAnchor; 1320 } 1321 1322 private class PopupDataSetObserver extends DataSetObserver { PopupDataSetObserver()1323 PopupDataSetObserver() { 1324 } 1325 1326 @Override onChanged()1327 public void onChanged() { 1328 if (isShowing()) { 1329 // Resize the popup to fit new content 1330 show(); 1331 } 1332 } 1333 1334 @Override onInvalidated()1335 public void onInvalidated() { 1336 dismiss(); 1337 } 1338 } 1339 1340 private class ListSelectorHider implements Runnable { ListSelectorHider()1341 ListSelectorHider() { 1342 } 1343 1344 @Override run()1345 public void run() { 1346 clearListSelection(); 1347 } 1348 } 1349 1350 private class ResizePopupRunnable implements Runnable { ResizePopupRunnable()1351 ResizePopupRunnable() { 1352 } 1353 1354 @Override run()1355 public void run() { 1356 if (mDropDownList != null && ViewCompat.isAttachedToWindow(mDropDownList) 1357 && mDropDownList.getCount() > mDropDownList.getChildCount() 1358 && mDropDownList.getChildCount() <= mListItemExpandMaximum) { 1359 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 1360 show(); 1361 } 1362 } 1363 } 1364 1365 private class PopupTouchInterceptor implements OnTouchListener { PopupTouchInterceptor()1366 PopupTouchInterceptor() { 1367 } 1368 1369 @Override onTouch(View v, MotionEvent event)1370 public boolean onTouch(View v, MotionEvent event) { 1371 final int action = event.getAction(); 1372 final int x = (int) event.getX(); 1373 final int y = (int) event.getY(); 1374 1375 if (action == MotionEvent.ACTION_DOWN && 1376 mPopup != null && mPopup.isShowing() && 1377 (x >= 0 && x < mPopup.getWidth() && y >= 0 && y < mPopup.getHeight())) { 1378 mHandler.postDelayed(mResizePopupRunnable, EXPAND_LIST_TIMEOUT); 1379 } else if (action == MotionEvent.ACTION_UP) { 1380 mHandler.removeCallbacks(mResizePopupRunnable); 1381 } 1382 return false; 1383 } 1384 } 1385 1386 private class PopupScrollListener implements ListView.OnScrollListener { PopupScrollListener()1387 PopupScrollListener() { 1388 } 1389 1390 @Override onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)1391 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 1392 int totalItemCount) { 1393 1394 } 1395 1396 @Override onScrollStateChanged(AbsListView view, int scrollState)1397 public void onScrollStateChanged(AbsListView view, int scrollState) { 1398 if (scrollState == SCROLL_STATE_TOUCH_SCROLL && 1399 !isInputMethodNotNeeded() && mPopup.getContentView() != null) { 1400 mHandler.removeCallbacks(mResizePopupRunnable); 1401 mResizePopupRunnable.run(); 1402 } 1403 } 1404 } 1405 isConfirmKey(int keyCode)1406 private static boolean isConfirmKey(int keyCode) { 1407 return keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_CENTER; 1408 } 1409 setPopupClipToScreenEnabled(boolean clip)1410 private void setPopupClipToScreenEnabled(boolean clip) { 1411 if (sClipToWindowEnabledMethod != null) { 1412 try { 1413 sClipToWindowEnabledMethod.invoke(mPopup, clip); 1414 } catch (Exception e) { 1415 Log.i(TAG, "Could not call setClipToScreenEnabled() on PopupWindow. Oh well."); 1416 } 1417 } 1418 } 1419 getMaxAvailableHeight(View anchor, int yOffset, boolean ignoreBottomDecorations)1420 private int getMaxAvailableHeight(View anchor, int yOffset, boolean ignoreBottomDecorations) { 1421 if (sGetMaxAvailableHeightMethod != null) { 1422 try { 1423 return (int) sGetMaxAvailableHeightMethod.invoke(mPopup, anchor, yOffset, 1424 ignoreBottomDecorations); 1425 } catch (Exception e) { 1426 Log.i(TAG, "Could not call getMaxAvailableHeightMethod(View, int, boolean)" 1427 + " on PopupWindow. Using the public version."); 1428 } 1429 } 1430 return mPopup.getMaxAvailableHeight(anchor, yOffset); 1431 } 1432 } 1433