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