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