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