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