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