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