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