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