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