ListPopupWindow.java revision 259c2840691a79634ffd8f63291ec21c21819542
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.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.ObjectAnimator; 22import android.content.Context; 23import android.content.res.TypedArray; 24import android.database.DataSetObserver; 25import android.graphics.Rect; 26import android.graphics.drawable.Drawable; 27import android.os.Handler; 28import android.os.SystemClock; 29import android.text.TextUtils; 30import android.util.AttributeSet; 31import android.util.IntProperty; 32import android.util.Log; 33import android.view.Gravity; 34import android.view.KeyEvent; 35import android.view.MotionEvent; 36import android.view.View; 37import android.view.View.MeasureSpec; 38import android.view.View.OnTouchListener; 39import android.view.ViewConfiguration; 40import android.view.ViewGroup; 41import android.view.ViewParent; 42import android.view.animation.AccelerateDecelerateInterpolator; 43 44import com.android.internal.R; 45import com.android.internal.widget.AutoScrollHelper.AbsListViewAutoScroller; 46 47import java.util.Locale; 48 49/** 50 * A ListPopupWindow anchors itself to a host view and displays a 51 * list of choices. 52 * 53 * <p>ListPopupWindow contains a number of tricky behaviors surrounding 54 * positioning, scrolling parents to fit the dropdown, interacting 55 * sanely with the IME if present, and others. 56 * 57 * @see android.widget.AutoCompleteTextView 58 * @see android.widget.Spinner 59 */ 60public class ListPopupWindow { 61 private static final String TAG = "ListPopupWindow"; 62 private static final boolean DEBUG = false; 63 64 /** 65 * This value controls the length of time that the user 66 * must leave a pointer down without scrolling to expand 67 * the autocomplete dropdown list to cover the IME. 68 */ 69 private static final int EXPAND_LIST_TIMEOUT = 250; 70 71 private Context mContext; 72 private PopupWindow mPopup; 73 private ListAdapter mAdapter; 74 private DropDownListView mDropDownList; 75 76 private int mDropDownHeight = ViewGroup.LayoutParams.WRAP_CONTENT; 77 private int mDropDownWidth = ViewGroup.LayoutParams.WRAP_CONTENT; 78 private int mDropDownHorizontalOffset; 79 private int mDropDownVerticalOffset; 80 private boolean mDropDownVerticalOffsetSet; 81 82 private int mDropDownGravity = Gravity.NO_GRAVITY; 83 84 private boolean mDropDownAlwaysVisible = false; 85 private boolean mForceIgnoreOutsideTouch = false; 86 int mListItemExpandMaximum = Integer.MAX_VALUE; 87 88 private View mPromptView; 89 private int mPromptPosition = POSITION_PROMPT_ABOVE; 90 91 private DataSetObserver mObserver; 92 93 private View mDropDownAnchorView; 94 95 private Drawable mDropDownListHighlight; 96 97 private AdapterView.OnItemClickListener mItemClickListener; 98 private AdapterView.OnItemSelectedListener mItemSelectedListener; 99 100 private final ResizePopupRunnable mResizePopupRunnable = new ResizePopupRunnable(); 101 private final PopupTouchInterceptor mTouchInterceptor = new PopupTouchInterceptor(); 102 private final PopupScrollListener mScrollListener = new PopupScrollListener(); 103 private final ListSelectorHider mHideSelector = new ListSelectorHider(); 104 private Runnable mShowDropDownRunnable; 105 106 private Handler mHandler = new Handler(); 107 108 private Rect mTempRect = new Rect(); 109 110 private boolean mModal; 111 112 private int mLayoutDirection; 113 114 /** 115 * The provided prompt view should appear above list content. 116 * 117 * @see #setPromptPosition(int) 118 * @see #getPromptPosition() 119 * @see #setPromptView(View) 120 */ 121 public static final int POSITION_PROMPT_ABOVE = 0; 122 123 /** 124 * The provided prompt view should appear below list content. 125 * 126 * @see #setPromptPosition(int) 127 * @see #getPromptPosition() 128 * @see #setPromptView(View) 129 */ 130 public static final int POSITION_PROMPT_BELOW = 1; 131 132 /** 133 * Alias for {@link ViewGroup.LayoutParams#MATCH_PARENT}. 134 * If used to specify a popup width, the popup will match the width of the anchor view. 135 * If used to specify a popup height, the popup will fill available space. 136 */ 137 public static final int MATCH_PARENT = ViewGroup.LayoutParams.MATCH_PARENT; 138 139 /** 140 * Alias for {@link ViewGroup.LayoutParams#WRAP_CONTENT}. 141 * If used to specify a popup width, the popup will use the width of its content. 142 */ 143 public static final int WRAP_CONTENT = ViewGroup.LayoutParams.WRAP_CONTENT; 144 145 /** 146 * Mode for {@link #setInputMethodMode(int)}: the requirements for the 147 * input method should be based on the focusability of the popup. That is 148 * if it is focusable than it needs to work with the input method, else 149 * it doesn't. 150 */ 151 public static final int INPUT_METHOD_FROM_FOCUSABLE = PopupWindow.INPUT_METHOD_FROM_FOCUSABLE; 152 153 /** 154 * Mode for {@link #setInputMethodMode(int)}: this popup always needs to 155 * work with an input method, regardless of whether it is focusable. This 156 * means that it will always be displayed so that the user can also operate 157 * the input method while it is shown. 158 */ 159 public static final int INPUT_METHOD_NEEDED = PopupWindow.INPUT_METHOD_NEEDED; 160 161 /** 162 * Mode for {@link #setInputMethodMode(int)}: this popup never needs to 163 * work with an input method, regardless of whether it is focusable. This 164 * means that it will always be displayed to use as much space on the 165 * screen as needed, regardless of whether this covers the input method. 166 */ 167 public static final int INPUT_METHOD_NOT_NEEDED = PopupWindow.INPUT_METHOD_NOT_NEEDED; 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 */ 175 public ListPopupWindow(Context context) { 176 this(context, null, com.android.internal.R.attr.listPopupWindowStyle, 0); 177 } 178 179 /** 180 * Create a new, empty popup window capable of displaying items from a ListAdapter. 181 * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. 182 * 183 * @param context Context used for contained views. 184 * @param attrs Attributes from inflating parent views used to style the popup. 185 */ 186 public ListPopupWindow(Context context, AttributeSet attrs) { 187 this(context, attrs, com.android.internal.R.attr.listPopupWindowStyle, 0); 188 } 189 190 /** 191 * Create a new, empty popup window capable of displaying items from a ListAdapter. 192 * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. 193 * 194 * @param context Context used for contained views. 195 * @param attrs Attributes from inflating parent views used to style the popup. 196 * @param defStyleAttr Default style attribute to use for popup content. 197 */ 198 public ListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr) { 199 this(context, attrs, defStyleAttr, 0); 200 } 201 202 /** 203 * Create a new, empty popup window capable of displaying items from a ListAdapter. 204 * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. 205 * 206 * @param context Context used for contained views. 207 * @param attrs Attributes from inflating parent views used to style the popup. 208 * @param defStyleAttr Style attribute to read for default styling of popup content. 209 * @param defStyleRes Style resource ID to use for default styling of popup content. 210 */ 211 public ListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 212 mContext = context; 213 214 final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ListPopupWindow, 215 defStyleAttr, defStyleRes); 216 mDropDownHorizontalOffset = a.getDimensionPixelOffset( 217 R.styleable.ListPopupWindow_dropDownHorizontalOffset, 0); 218 mDropDownVerticalOffset = a.getDimensionPixelOffset( 219 R.styleable.ListPopupWindow_dropDownVerticalOffset, 0); 220 if (mDropDownVerticalOffset != 0) { 221 mDropDownVerticalOffsetSet = true; 222 } 223 a.recycle(); 224 225 mPopup = new PopupWindow(context, attrs, defStyleAttr, defStyleRes); 226 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); 227 // Set the default layout direction to match the default locale one 228 final Locale locale = mContext.getResources().getConfiguration().locale; 229 mLayoutDirection = TextUtils.getLayoutDirectionFromLocale(locale); 230 } 231 232 /** 233 * Sets the adapter that provides the data and the views to represent the data 234 * in this popup window. 235 * 236 * @param adapter The adapter to use to create this window's content. 237 */ 238 public void setAdapter(ListAdapter adapter) { 239 if (mObserver == null) { 240 mObserver = new PopupDataSetObserver(); 241 } else if (mAdapter != null) { 242 mAdapter.unregisterDataSetObserver(mObserver); 243 } 244 mAdapter = adapter; 245 if (mAdapter != null) { 246 adapter.registerDataSetObserver(mObserver); 247 } 248 249 if (mDropDownList != null) { 250 mDropDownList.setAdapter(mAdapter); 251 } 252 } 253 254 /** 255 * Set where the optional prompt view should appear. The default is 256 * {@link #POSITION_PROMPT_ABOVE}. 257 * 258 * @param position A position constant declaring where the prompt should be displayed. 259 * 260 * @see #POSITION_PROMPT_ABOVE 261 * @see #POSITION_PROMPT_BELOW 262 */ 263 public void setPromptPosition(int position) { 264 mPromptPosition = position; 265 } 266 267 /** 268 * @return Where the optional prompt view should appear. 269 * 270 * @see #POSITION_PROMPT_ABOVE 271 * @see #POSITION_PROMPT_BELOW 272 */ 273 public int getPromptPosition() { 274 return mPromptPosition; 275 } 276 277 /** 278 * Set whether this window should be modal when shown. 279 * 280 * <p>If a popup window is modal, it will receive all touch and key input. 281 * If the user touches outside the popup window's content area the popup window 282 * will be dismissed. 283 * 284 * @param modal {@code true} if the popup window should be modal, {@code false} otherwise. 285 */ 286 public void setModal(boolean modal) { 287 mModal = modal; 288 mPopup.setFocusable(modal); 289 } 290 291 /** 292 * Returns whether the popup window will be modal when shown. 293 * 294 * @return {@code true} if the popup window will be modal, {@code false} otherwise. 295 */ 296 public boolean isModal() { 297 return mModal; 298 } 299 300 /** 301 * Forces outside touches to be ignored. Normally if {@link #isDropDownAlwaysVisible()} is 302 * false, we allow outside touch to dismiss the dropdown. If this is set to true, then we 303 * ignore outside touch even when the drop down is not set to always visible. 304 * 305 * @hide Used only by AutoCompleteTextView to handle some internal special cases. 306 */ 307 public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) { 308 mForceIgnoreOutsideTouch = forceIgnoreOutsideTouch; 309 } 310 311 /** 312 * Sets whether the drop-down should remain visible under certain conditions. 313 * 314 * The drop-down will occupy the entire screen below {@link #getAnchorView} regardless 315 * of the size or content of the list. {@link #getBackground()} will fill any space 316 * that is not used by the list. 317 * 318 * @param dropDownAlwaysVisible Whether to keep the drop-down visible. 319 * 320 * @hide Only used by AutoCompleteTextView under special conditions. 321 */ 322 public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) { 323 mDropDownAlwaysVisible = dropDownAlwaysVisible; 324 } 325 326 /** 327 * @return Whether the drop-down is visible under special conditions. 328 * 329 * @hide Only used by AutoCompleteTextView under special conditions. 330 */ 331 public boolean isDropDownAlwaysVisible() { 332 return mDropDownAlwaysVisible; 333 } 334 335 /** 336 * Sets the operating mode for the soft input area. 337 * 338 * @param mode The desired mode, see 339 * {@link android.view.WindowManager.LayoutParams#softInputMode} 340 * for the full list 341 * 342 * @see android.view.WindowManager.LayoutParams#softInputMode 343 * @see #getSoftInputMode() 344 */ 345 public void setSoftInputMode(int mode) { 346 mPopup.setSoftInputMode(mode); 347 } 348 349 /** 350 * Returns the current value in {@link #setSoftInputMode(int)}. 351 * 352 * @see #setSoftInputMode(int) 353 * @see android.view.WindowManager.LayoutParams#softInputMode 354 */ 355 public int getSoftInputMode() { 356 return mPopup.getSoftInputMode(); 357 } 358 359 /** 360 * Sets a drawable to use as the list item selector. 361 * 362 * @param selector List selector drawable to use in the popup. 363 */ 364 public void setListSelector(Drawable selector) { 365 mDropDownListHighlight = selector; 366 } 367 368 /** 369 * @return The background drawable for the popup window. 370 */ 371 public Drawable getBackground() { 372 return mPopup.getBackground(); 373 } 374 375 /** 376 * Sets a drawable to be the background for the popup window. 377 * 378 * @param d A drawable to set as the background. 379 */ 380 public void setBackgroundDrawable(Drawable d) { 381 mPopup.setBackgroundDrawable(d); 382 } 383 384 /** 385 * Set an animation style to use when the popup window is shown or dismissed. 386 * 387 * @param animationStyle Animation style to use. 388 */ 389 public void setAnimationStyle(int animationStyle) { 390 mPopup.setAnimationStyle(animationStyle); 391 } 392 393 /** 394 * Returns the animation style that will be used when the popup window is 395 * shown or dismissed. 396 * 397 * @return Animation style that will be used. 398 */ 399 public int getAnimationStyle() { 400 return mPopup.getAnimationStyle(); 401 } 402 403 /** 404 * Returns the view that will be used to anchor this popup. 405 * 406 * @return The popup's anchor view 407 */ 408 public View getAnchorView() { 409 return mDropDownAnchorView; 410 } 411 412 /** 413 * Sets the popup's anchor view. This popup will always be positioned relative to 414 * the anchor view when shown. 415 * 416 * @param anchor The view to use as an anchor. 417 */ 418 public void setAnchorView(View anchor) { 419 mDropDownAnchorView = anchor; 420 } 421 422 /** 423 * @return The horizontal offset of the popup from its anchor in pixels. 424 */ 425 public int getHorizontalOffset() { 426 return mDropDownHorizontalOffset; 427 } 428 429 /** 430 * Set the horizontal offset of this popup from its anchor view in pixels. 431 * 432 * @param offset The horizontal offset of the popup from its anchor. 433 */ 434 public void setHorizontalOffset(int offset) { 435 mDropDownHorizontalOffset = offset; 436 } 437 438 /** 439 * @return The vertical offset of the popup from its anchor in pixels. 440 */ 441 public int getVerticalOffset() { 442 if (!mDropDownVerticalOffsetSet) { 443 return 0; 444 } 445 return mDropDownVerticalOffset; 446 } 447 448 /** 449 * Set the vertical offset of this popup from its anchor view in pixels. 450 * 451 * @param offset The vertical offset of the popup from its anchor. 452 */ 453 public void setVerticalOffset(int offset) { 454 mDropDownVerticalOffset = offset; 455 mDropDownVerticalOffsetSet = true; 456 } 457 458 /** 459 * Set the gravity of the dropdown list. This is commonly used to 460 * set gravity to START or END for alignment with the anchor. 461 * 462 * @param gravity Gravity value to use 463 */ 464 public void setDropDownGravity(int gravity) { 465 mDropDownGravity = gravity; 466 } 467 468 /** 469 * @return The width of the popup window in pixels. 470 */ 471 public int getWidth() { 472 return mDropDownWidth; 473 } 474 475 /** 476 * Sets the width of the popup window in pixels. Can also be {@link #MATCH_PARENT} 477 * or {@link #WRAP_CONTENT}. 478 * 479 * @param width Width of the popup window. 480 */ 481 public void setWidth(int width) { 482 mDropDownWidth = width; 483 } 484 485 /** 486 * Sets the width of the popup window by the size of its content. The final width may be 487 * larger to accommodate styled window dressing. 488 * 489 * @param width Desired width of content in pixels. 490 */ 491 public void setContentWidth(int width) { 492 Drawable popupBackground = mPopup.getBackground(); 493 if (popupBackground != null) { 494 popupBackground.getPadding(mTempRect); 495 mDropDownWidth = mTempRect.left + mTempRect.right + width; 496 } else { 497 setWidth(width); 498 } 499 } 500 501 /** 502 * @return The height of the popup window in pixels. 503 */ 504 public int getHeight() { 505 return mDropDownHeight; 506 } 507 508 /** 509 * Sets the height of the popup window in pixels. Can also be {@link #MATCH_PARENT}. 510 * 511 * @param height Height of the popup window. 512 */ 513 public void setHeight(int height) { 514 mDropDownHeight = height; 515 } 516 517 /** 518 * Sets a listener to receive events when a list item is clicked. 519 * 520 * @param clickListener Listener to register 521 * 522 * @see ListView#setOnItemClickListener(android.widget.AdapterView.OnItemClickListener) 523 */ 524 public void setOnItemClickListener(AdapterView.OnItemClickListener clickListener) { 525 mItemClickListener = clickListener; 526 } 527 528 /** 529 * Sets a listener to receive events when a list item is selected. 530 * 531 * @param selectedListener Listener to register. 532 * 533 * @see ListView#setOnItemSelectedListener(android.widget.AdapterView.OnItemSelectedListener) 534 */ 535 public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener selectedListener) { 536 mItemSelectedListener = selectedListener; 537 } 538 539 /** 540 * Set a view to act as a user prompt for this popup window. Where the prompt view will appear 541 * is controlled by {@link #setPromptPosition(int)}. 542 * 543 * @param prompt View to use as an informational prompt. 544 */ 545 public void setPromptView(View prompt) { 546 boolean showing = isShowing(); 547 if (showing) { 548 removePromptView(); 549 } 550 mPromptView = prompt; 551 if (showing) { 552 show(); 553 } 554 } 555 556 /** 557 * Post a {@link #show()} call to the UI thread. 558 */ 559 public void postShow() { 560 mHandler.post(mShowDropDownRunnable); 561 } 562 563 /** 564 * Show the popup list. If the list is already showing, this method 565 * will recalculate the popup's size and position. 566 */ 567 public void show() { 568 int height = buildDropDown(); 569 570 boolean noInputMethod = isInputMethodNotNeeded(); 571 mPopup.setAllowScrollingAnchorParent(!noInputMethod); 572 573 if (mPopup.isShowing()) { 574 final int widthSpec; 575 if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) { 576 // The call to PopupWindow's update method below can accept -1 for any 577 // value you do not want to update. 578 widthSpec = -1; 579 } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { 580 widthSpec = getAnchorView().getWidth(); 581 } else { 582 widthSpec = mDropDownWidth; 583 } 584 585 final int heightSpec; 586 if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { 587 // The call to PopupWindow's update method below can accept -1 for any 588 // value you do not want to update. 589 heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT; 590 if (noInputMethod) { 591 mPopup.setWidth(mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ? 592 ViewGroup.LayoutParams.MATCH_PARENT : 0); 593 mPopup.setHeight(0); 594 } else { 595 mPopup.setWidth(mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ? 596 ViewGroup.LayoutParams.MATCH_PARENT : 0); 597 mPopup.setHeight(ViewGroup.LayoutParams.MATCH_PARENT); 598 } 599 } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { 600 heightSpec = height; 601 } else { 602 heightSpec = mDropDownHeight; 603 } 604 605 mPopup.setWidth(widthSpec); 606 mPopup.setHeight(heightSpec); 607 mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible); 608 609 mPopup.update(getAnchorView(), mDropDownHorizontalOffset, 610 mDropDownVerticalOffset, -1, -1); 611 } else { 612 final int widthSpec; 613 if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) { 614 widthSpec = ViewGroup.LayoutParams.MATCH_PARENT; 615 } else { 616 if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { 617 widthSpec = getAnchorView().getWidth(); 618 } else { 619 widthSpec = mDropDownWidth; 620 } 621 } 622 623 final int heightSpec; 624 if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { 625 heightSpec = ViewGroup.LayoutParams.MATCH_PARENT; 626 } else { 627 if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { 628 heightSpec = height; 629 } else { 630 heightSpec = mDropDownHeight; 631 } 632 } 633 634 mPopup.setWidth(widthSpec); 635 mPopup.setHeight(heightSpec); 636 mPopup.setClipToScreenEnabled(true); 637 638 // use outside touchable to dismiss drop down when touching outside of it, so 639 // only set this if the dropdown is not always visible 640 mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible); 641 mPopup.setTouchInterceptor(mTouchInterceptor); 642 mPopup.showAsDropDown(getAnchorView(), mDropDownHorizontalOffset, 643 mDropDownVerticalOffset, mDropDownGravity); 644 mDropDownList.setSelection(ListView.INVALID_POSITION); 645 646 if (!mModal || mDropDownList.isInTouchMode()) { 647 clearListSelection(); 648 } 649 if (!mModal) { 650 mHandler.post(mHideSelector); 651 } 652 } 653 } 654 655 /** 656 * Dismiss the popup window. 657 */ 658 public void dismiss() { 659 mPopup.dismiss(); 660 removePromptView(); 661 mPopup.setContentView(null); 662 mDropDownList = null; 663 mHandler.removeCallbacks(mResizePopupRunnable); 664 } 665 666 /** 667 * Set a listener to receive a callback when the popup is dismissed. 668 * 669 * @param listener Listener that will be notified when the popup is dismissed. 670 */ 671 public void setOnDismissListener(PopupWindow.OnDismissListener listener) { 672 mPopup.setOnDismissListener(listener); 673 } 674 675 private void removePromptView() { 676 if (mPromptView != null) { 677 final ViewParent parent = mPromptView.getParent(); 678 if (parent instanceof ViewGroup) { 679 final ViewGroup group = (ViewGroup) parent; 680 group.removeView(mPromptView); 681 } 682 } 683 } 684 685 /** 686 * Control how the popup operates with an input method: one of 687 * {@link #INPUT_METHOD_FROM_FOCUSABLE}, {@link #INPUT_METHOD_NEEDED}, 688 * or {@link #INPUT_METHOD_NOT_NEEDED}. 689 * 690 * <p>If the popup is showing, calling this method will take effect only 691 * the next time the popup is shown or through a manual call to the {@link #show()} 692 * method.</p> 693 * 694 * @see #getInputMethodMode() 695 * @see #show() 696 */ 697 public void setInputMethodMode(int mode) { 698 mPopup.setInputMethodMode(mode); 699 } 700 701 /** 702 * Return the current value in {@link #setInputMethodMode(int)}. 703 * 704 * @see #setInputMethodMode(int) 705 */ 706 public int getInputMethodMode() { 707 return mPopup.getInputMethodMode(); 708 } 709 710 /** 711 * Set the selected position of the list. 712 * Only valid when {@link #isShowing()} == {@code true}. 713 * 714 * @param position List position to set as selected. 715 */ 716 public void setSelection(int position) { 717 DropDownListView list = mDropDownList; 718 if (isShowing() && list != null) { 719 list.mListSelectionHidden = false; 720 list.setSelection(position); 721 if (list.getChoiceMode() != ListView.CHOICE_MODE_NONE) { 722 list.setItemChecked(position, true); 723 } 724 } 725 } 726 727 /** 728 * Clear any current list selection. 729 * Only valid when {@link #isShowing()} == {@code true}. 730 */ 731 public void clearListSelection() { 732 final DropDownListView list = mDropDownList; 733 if (list != null) { 734 // WARNING: Please read the comment where mListSelectionHidden is declared 735 list.mListSelectionHidden = true; 736 list.hideSelector(); 737 list.requestLayout(); 738 } 739 } 740 741 /** 742 * @return {@code true} if the popup is currently showing, {@code false} otherwise. 743 */ 744 public boolean isShowing() { 745 return mPopup.isShowing(); 746 } 747 748 /** 749 * @return {@code true} if this popup is configured to assume the user does not need 750 * to interact with the IME while it is showing, {@code false} otherwise. 751 */ 752 public boolean isInputMethodNotNeeded() { 753 return mPopup.getInputMethodMode() == INPUT_METHOD_NOT_NEEDED; 754 } 755 756 /** 757 * Perform an item click operation on the specified list adapter position. 758 * 759 * @param position Adapter position for performing the click 760 * @return true if the click action could be performed, false if not. 761 * (e.g. if the popup was not showing, this method would return false.) 762 */ 763 public boolean performItemClick(int position) { 764 if (isShowing()) { 765 if (mItemClickListener != null) { 766 final DropDownListView list = mDropDownList; 767 final View child = list.getChildAt(position - list.getFirstVisiblePosition()); 768 final ListAdapter adapter = list.getAdapter(); 769 mItemClickListener.onItemClick(list, child, position, adapter.getItemId(position)); 770 } 771 return true; 772 } 773 return false; 774 } 775 776 /** 777 * @return The currently selected item or null if the popup is not showing. 778 */ 779 public Object getSelectedItem() { 780 if (!isShowing()) { 781 return null; 782 } 783 return mDropDownList.getSelectedItem(); 784 } 785 786 /** 787 * @return The position of the currently selected item or {@link ListView#INVALID_POSITION} 788 * if {@link #isShowing()} == {@code false}. 789 * 790 * @see ListView#getSelectedItemPosition() 791 */ 792 public int getSelectedItemPosition() { 793 if (!isShowing()) { 794 return ListView.INVALID_POSITION; 795 } 796 return mDropDownList.getSelectedItemPosition(); 797 } 798 799 /** 800 * @return The ID of the currently selected item or {@link ListView#INVALID_ROW_ID} 801 * if {@link #isShowing()} == {@code false}. 802 * 803 * @see ListView#getSelectedItemId() 804 */ 805 public long getSelectedItemId() { 806 if (!isShowing()) { 807 return ListView.INVALID_ROW_ID; 808 } 809 return mDropDownList.getSelectedItemId(); 810 } 811 812 /** 813 * @return The View for the currently selected item or null if 814 * {@link #isShowing()} == {@code false}. 815 * 816 * @see ListView#getSelectedView() 817 */ 818 public View getSelectedView() { 819 if (!isShowing()) { 820 return null; 821 } 822 return mDropDownList.getSelectedView(); 823 } 824 825 /** 826 * @return The {@link ListView} displayed within the popup window. 827 * Only valid when {@link #isShowing()} == {@code true}. 828 */ 829 public ListView getListView() { 830 return mDropDownList; 831 } 832 833 /** 834 * The maximum number of list items that can be visible and still have 835 * the list expand when touched. 836 * 837 * @param max Max number of items that can be visible and still allow the list to expand. 838 */ 839 void setListItemExpandMax(int max) { 840 mListItemExpandMaximum = max; 841 } 842 843 /** 844 * Filter key down events. By forwarding key down events to this function, 845 * views using non-modal ListPopupWindow can have it handle key selection of items. 846 * 847 * @param keyCode keyCode param passed to the host view's onKeyDown 848 * @param event event param passed to the host view's onKeyDown 849 * @return true if the event was handled, false if it was ignored. 850 * 851 * @see #setModal(boolean) 852 */ 853 public boolean onKeyDown(int keyCode, KeyEvent event) { 854 // when the drop down is shown, we drive it directly 855 if (isShowing()) { 856 // the key events are forwarded to the list in the drop down view 857 // note that ListView handles space but we don't want that to happen 858 // also if selection is not currently in the drop down, then don't 859 // let center or enter presses go there since that would cause it 860 // to select one of its items 861 if (keyCode != KeyEvent.KEYCODE_SPACE 862 && (mDropDownList.getSelectedItemPosition() >= 0 863 || !KeyEvent.isConfirmKey(keyCode))) { 864 int curIndex = mDropDownList.getSelectedItemPosition(); 865 boolean consumed; 866 867 final boolean below = !mPopup.isAboveAnchor(); 868 869 final ListAdapter adapter = mAdapter; 870 871 boolean allEnabled; 872 int firstItem = Integer.MAX_VALUE; 873 int lastItem = Integer.MIN_VALUE; 874 875 if (adapter != null) { 876 allEnabled = adapter.areAllItemsEnabled(); 877 firstItem = allEnabled ? 0 : 878 mDropDownList.lookForSelectablePosition(0, true); 879 lastItem = allEnabled ? adapter.getCount() - 1 : 880 mDropDownList.lookForSelectablePosition(adapter.getCount() - 1, false); 881 } 882 883 if ((below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex <= firstItem) || 884 (!below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN && curIndex >= lastItem)) { 885 // When the selection is at the top, we block the key 886 // event to prevent focus from moving. 887 clearListSelection(); 888 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); 889 show(); 890 return true; 891 } else { 892 // WARNING: Please read the comment where mListSelectionHidden 893 // is declared 894 mDropDownList.mListSelectionHidden = false; 895 } 896 897 consumed = mDropDownList.onKeyDown(keyCode, event); 898 if (DEBUG) Log.v(TAG, "Key down: code=" + keyCode + " list consumed=" + consumed); 899 900 if (consumed) { 901 // If it handled the key event, then the user is 902 // navigating in the list, so we should put it in front. 903 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 904 // Here's a little trick we need to do to make sure that 905 // the list view is actually showing its focus indicator, 906 // by ensuring it has focus and getting its window out 907 // of touch mode. 908 mDropDownList.requestFocusFromTouch(); 909 show(); 910 911 switch (keyCode) { 912 // avoid passing the focus from the text view to the 913 // next component 914 case KeyEvent.KEYCODE_ENTER: 915 case KeyEvent.KEYCODE_DPAD_CENTER: 916 case KeyEvent.KEYCODE_DPAD_DOWN: 917 case KeyEvent.KEYCODE_DPAD_UP: 918 return true; 919 } 920 } else { 921 if (below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { 922 // when the selection is at the bottom, we block the 923 // event to avoid going to the next focusable widget 924 if (curIndex == lastItem) { 925 return true; 926 } 927 } else if (!below && keyCode == KeyEvent.KEYCODE_DPAD_UP && 928 curIndex == firstItem) { 929 return true; 930 } 931 } 932 } 933 } 934 935 return false; 936 } 937 938 /** 939 * Filter key down events. By forwarding key up events to this function, 940 * views using non-modal ListPopupWindow can have it handle key selection of items. 941 * 942 * @param keyCode keyCode param passed to the host view's onKeyUp 943 * @param event event param passed to the host view's onKeyUp 944 * @return true if the event was handled, false if it was ignored. 945 * 946 * @see #setModal(boolean) 947 */ 948 public boolean onKeyUp(int keyCode, KeyEvent event) { 949 if (isShowing() && mDropDownList.getSelectedItemPosition() >= 0) { 950 boolean consumed = mDropDownList.onKeyUp(keyCode, event); 951 if (consumed && KeyEvent.isConfirmKey(keyCode)) { 952 // if the list accepts the key events and the key event was a click, the text view 953 // gets the selected item from the drop down as its content 954 dismiss(); 955 } 956 return consumed; 957 } 958 return false; 959 } 960 961 /** 962 * Filter pre-IME key events. By forwarding {@link View#onKeyPreIme(int, KeyEvent)} 963 * events to this function, views using ListPopupWindow can have it dismiss the popup 964 * when the back key is pressed. 965 * 966 * @param keyCode keyCode param passed to the host view's onKeyPreIme 967 * @param event event param passed to the host view's onKeyPreIme 968 * @return true if the event was handled, false if it was ignored. 969 * 970 * @see #setModal(boolean) 971 */ 972 public boolean onKeyPreIme(int keyCode, KeyEvent event) { 973 if (keyCode == KeyEvent.KEYCODE_BACK && isShowing()) { 974 // special case for the back key, we do not even try to send it 975 // to the drop down list but instead, consume it immediately 976 final View anchorView = mDropDownAnchorView; 977 if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { 978 KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState(); 979 if (state != null) { 980 state.startTracking(event, this); 981 } 982 return true; 983 } else if (event.getAction() == KeyEvent.ACTION_UP) { 984 KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState(); 985 if (state != null) { 986 state.handleUpEvent(event); 987 } 988 if (event.isTracking() && !event.isCanceled()) { 989 dismiss(); 990 return true; 991 } 992 } 993 } 994 return false; 995 } 996 997 /** 998 * Returns an {@link OnTouchListener} that can be added to the source view 999 * to implement drag-to-open behavior. Generally, the source view should be 1000 * the same view that was passed to {@link #setAnchorView}. 1001 * <p> 1002 * When the listener is set on a view, touching that view and dragging 1003 * outside of its bounds will open the popup window. Lifting will select the 1004 * currently touched list item. 1005 * <p> 1006 * Example usage: 1007 * <pre> 1008 * ListPopupWindow myPopup = new ListPopupWindow(context); 1009 * myPopup.setAnchor(myAnchor); 1010 * OnTouchListener dragListener = myPopup.createDragToOpenListener(myAnchor); 1011 * myAnchor.setOnTouchListener(dragListener); 1012 * </pre> 1013 * 1014 * @param src the view on which the resulting listener will be set 1015 * @return a touch listener that controls drag-to-open behavior 1016 */ 1017 public OnTouchListener createDragToOpenListener(View src) { 1018 return new ForwardingListener(src) { 1019 @Override 1020 public ListPopupWindow getPopup() { 1021 return ListPopupWindow.this; 1022 } 1023 }; 1024 } 1025 1026 /** 1027 * <p>Builds the popup window's content and returns the height the popup 1028 * should have. Returns -1 when the content already exists.</p> 1029 * 1030 * @return the content's height or -1 if content already exists 1031 */ 1032 private int buildDropDown() { 1033 ViewGroup dropDownView; 1034 int otherHeights = 0; 1035 1036 if (mDropDownList == null) { 1037 Context context = mContext; 1038 1039 /** 1040 * This Runnable exists for the sole purpose of checking if the view layout has got 1041 * completed and if so call showDropDown to display the drop down. This is used to show 1042 * the drop down as soon as possible after user opens up the search dialog, without 1043 * waiting for the normal UI pipeline to do it's job which is slower than this method. 1044 */ 1045 mShowDropDownRunnable = new Runnable() { 1046 public void run() { 1047 // View layout should be all done before displaying the drop down. 1048 View view = getAnchorView(); 1049 if (view != null && view.getWindowToken() != null) { 1050 show(); 1051 } 1052 } 1053 }; 1054 1055 mDropDownList = new DropDownListView(context, !mModal); 1056 if (mDropDownListHighlight != null) { 1057 mDropDownList.setSelector(mDropDownListHighlight); 1058 } 1059 mDropDownList.setAdapter(mAdapter); 1060 mDropDownList.setOnItemClickListener(mItemClickListener); 1061 mDropDownList.setFocusable(true); 1062 mDropDownList.setFocusableInTouchMode(true); 1063 mDropDownList.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 1064 public void onItemSelected(AdapterView<?> parent, View view, 1065 int position, long id) { 1066 1067 if (position != -1) { 1068 DropDownListView dropDownList = mDropDownList; 1069 1070 if (dropDownList != null) { 1071 dropDownList.mListSelectionHidden = false; 1072 } 1073 } 1074 } 1075 1076 public void onNothingSelected(AdapterView<?> parent) { 1077 } 1078 }); 1079 mDropDownList.setOnScrollListener(mScrollListener); 1080 1081 if (mItemSelectedListener != null) { 1082 mDropDownList.setOnItemSelectedListener(mItemSelectedListener); 1083 } 1084 1085 dropDownView = mDropDownList; 1086 1087 View hintView = mPromptView; 1088 if (hintView != null) { 1089 // if a hint has been specified, we accomodate more space for it and 1090 // add a text view in the drop down menu, at the bottom of the list 1091 LinearLayout hintContainer = new LinearLayout(context); 1092 hintContainer.setOrientation(LinearLayout.VERTICAL); 1093 1094 LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams( 1095 ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f 1096 ); 1097 1098 switch (mPromptPosition) { 1099 case POSITION_PROMPT_BELOW: 1100 hintContainer.addView(dropDownView, hintParams); 1101 hintContainer.addView(hintView); 1102 break; 1103 1104 case POSITION_PROMPT_ABOVE: 1105 hintContainer.addView(hintView); 1106 hintContainer.addView(dropDownView, hintParams); 1107 break; 1108 1109 default: 1110 Log.e(TAG, "Invalid hint position " + mPromptPosition); 1111 break; 1112 } 1113 1114 // measure the hint's height to find how much more vertical space 1115 // we need to add to the drop down's height 1116 int widthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.AT_MOST); 1117 int heightSpec = MeasureSpec.UNSPECIFIED; 1118 hintView.measure(widthSpec, heightSpec); 1119 1120 hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams(); 1121 otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin 1122 + hintParams.bottomMargin; 1123 1124 dropDownView = hintContainer; 1125 } 1126 1127 mPopup.setContentView(dropDownView); 1128 } else { 1129 dropDownView = (ViewGroup) mPopup.getContentView(); 1130 final View view = mPromptView; 1131 if (view != null) { 1132 LinearLayout.LayoutParams hintParams = 1133 (LinearLayout.LayoutParams) view.getLayoutParams(); 1134 otherHeights = view.getMeasuredHeight() + hintParams.topMargin 1135 + hintParams.bottomMargin; 1136 } 1137 } 1138 1139 // getMaxAvailableHeight() subtracts the padding, so we put it back 1140 // to get the available height for the whole window 1141 int padding = 0; 1142 Drawable background = mPopup.getBackground(); 1143 if (background != null) { 1144 background.getPadding(mTempRect); 1145 padding = mTempRect.top + mTempRect.bottom; 1146 1147 // If we don't have an explicit vertical offset, determine one from the window 1148 // background so that content will line up. 1149 if (!mDropDownVerticalOffsetSet) { 1150 mDropDownVerticalOffset = -mTempRect.top; 1151 } 1152 } else { 1153 mTempRect.setEmpty(); 1154 } 1155 1156 // Max height available on the screen for a popup. 1157 boolean ignoreBottomDecorations = 1158 mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED; 1159 final int maxHeight = mPopup.getMaxAvailableHeight( 1160 getAnchorView(), mDropDownVerticalOffset, ignoreBottomDecorations); 1161 1162 if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { 1163 return maxHeight + padding; 1164 } 1165 1166 final int childWidthSpec; 1167 switch (mDropDownWidth) { 1168 case ViewGroup.LayoutParams.WRAP_CONTENT: 1169 childWidthSpec = MeasureSpec.makeMeasureSpec( 1170 mContext.getResources().getDisplayMetrics().widthPixels - 1171 (mTempRect.left + mTempRect.right), 1172 MeasureSpec.AT_MOST); 1173 break; 1174 case ViewGroup.LayoutParams.MATCH_PARENT: 1175 childWidthSpec = MeasureSpec.makeMeasureSpec( 1176 mContext.getResources().getDisplayMetrics().widthPixels - 1177 (mTempRect.left + mTempRect.right), 1178 MeasureSpec.EXACTLY); 1179 break; 1180 default: 1181 childWidthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.EXACTLY); 1182 break; 1183 } 1184 final int listContent = mDropDownList.measureHeightOfChildren(childWidthSpec, 1185 0, ListView.NO_POSITION, maxHeight - otherHeights, -1); 1186 // add padding only if the list has items in it, that way we don't show 1187 // the popup if it is not needed 1188 if (listContent > 0) otherHeights += padding; 1189 1190 return listContent + otherHeights; 1191 } 1192 1193 /** 1194 * Abstract class that forwards touch events to a {@link ListPopupWindow}. 1195 * 1196 * @hide 1197 */ 1198 public static abstract class ForwardingListener 1199 implements View.OnTouchListener, View.OnAttachStateChangeListener { 1200 /** Scaled touch slop, used for detecting movement outside bounds. */ 1201 private final float mScaledTouchSlop; 1202 1203 /** Timeout before disallowing intercept on the source's parent. */ 1204 private final int mTapTimeout; 1205 1206 /** Timeout before accepting a long-press to start forwarding. */ 1207 private final int mLongPressTimeout; 1208 1209 /** Source view from which events are forwarded. */ 1210 private final View mSrc; 1211 1212 /** Runnable used to prevent conflicts with scrolling parents. */ 1213 private Runnable mDisallowIntercept; 1214 1215 /** Runnable used to trigger forwarding on long-press. */ 1216 private Runnable mTriggerLongPress; 1217 1218 /** Whether this listener is currently forwarding touch events. */ 1219 private boolean mForwarding; 1220 1221 /** 1222 * Whether forwarding was initiated by a long-press. If so, we won't 1223 * force the window to dismiss when the touch stream ends. 1224 */ 1225 private boolean mWasLongPress; 1226 1227 /** The id of the first pointer down in the current event stream. */ 1228 private int mActivePointerId; 1229 1230 public ForwardingListener(View src) { 1231 mSrc = src; 1232 mScaledTouchSlop = ViewConfiguration.get(src.getContext()).getScaledTouchSlop(); 1233 mTapTimeout = ViewConfiguration.getTapTimeout(); 1234 1235 // Use a medium-press timeout. Halfway between tap and long-press. 1236 mLongPressTimeout = (mTapTimeout + ViewConfiguration.getLongPressTimeout()) / 2; 1237 1238 src.addOnAttachStateChangeListener(this); 1239 } 1240 1241 /** 1242 * Returns the popup to which this listener is forwarding events. 1243 * <p> 1244 * Override this to return the correct popup. If the popup is displayed 1245 * asynchronously, you may also need to override 1246 * {@link #onForwardingStopped} to prevent premature cancelation of 1247 * forwarding. 1248 * 1249 * @return the popup to which this listener is forwarding events 1250 */ 1251 public abstract ListPopupWindow getPopup(); 1252 1253 @Override 1254 public boolean onTouch(View v, MotionEvent event) { 1255 final boolean wasForwarding = mForwarding; 1256 final boolean forwarding; 1257 if (wasForwarding) { 1258 forwarding = onTouchForwarded(event) || !onForwardingStopped(); 1259 } else { 1260 forwarding = onTouchObserved(event) && onForwardingStarted(); 1261 1262 if (forwarding) { 1263 // Make sure we cancel any ongoing source event stream. 1264 final long now = SystemClock.uptimeMillis(); 1265 final MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 1266 0.0f, 0.0f, 0); 1267 mSrc.onTouchEvent(e); 1268 e.recycle(); 1269 } 1270 } 1271 1272 mForwarding = forwarding; 1273 return forwarding || wasForwarding; 1274 } 1275 1276 @Override 1277 public void onViewAttachedToWindow(View v) { 1278 } 1279 1280 @Override 1281 public void onViewDetachedFromWindow(View v) { 1282 mForwarding = false; 1283 mActivePointerId = MotionEvent.INVALID_POINTER_ID; 1284 1285 if (mDisallowIntercept != null) { 1286 mSrc.removeCallbacks(mDisallowIntercept); 1287 } 1288 } 1289 1290 /** 1291 * Called when forwarding would like to start. 1292 * <p> 1293 * By default, this will show the popup returned by {@link #getPopup()}. 1294 * It may be overridden to perform another action, like clicking the 1295 * source view or preparing the popup before showing it. 1296 * 1297 * @return true to start forwarding, false otherwise 1298 */ 1299 protected boolean onForwardingStarted() { 1300 final ListPopupWindow popup = getPopup(); 1301 if (popup != null && !popup.isShowing()) { 1302 popup.show(); 1303 } 1304 return true; 1305 } 1306 1307 /** 1308 * Called when forwarding would like to stop. 1309 * <p> 1310 * By default, this will dismiss the popup returned by 1311 * {@link #getPopup()}. It may be overridden to perform some other 1312 * action. 1313 * 1314 * @return true to stop forwarding, false otherwise 1315 */ 1316 protected boolean onForwardingStopped() { 1317 final ListPopupWindow popup = getPopup(); 1318 if (popup != null && popup.isShowing()) { 1319 popup.dismiss(); 1320 } 1321 return true; 1322 } 1323 1324 /** 1325 * Observes motion events and determines when to start forwarding. 1326 * 1327 * @param srcEvent motion event in source view coordinates 1328 * @return true to start forwarding motion events, false otherwise 1329 */ 1330 private boolean onTouchObserved(MotionEvent srcEvent) { 1331 final View src = mSrc; 1332 if (!src.isEnabled()) { 1333 return false; 1334 } 1335 1336 final int actionMasked = srcEvent.getActionMasked(); 1337 switch (actionMasked) { 1338 case MotionEvent.ACTION_DOWN: 1339 mActivePointerId = srcEvent.getPointerId(0); 1340 mWasLongPress = false; 1341 1342 if (mDisallowIntercept == null) { 1343 mDisallowIntercept = new DisallowIntercept(); 1344 } 1345 src.postDelayed(mDisallowIntercept, mTapTimeout); 1346 1347 if (mTriggerLongPress == null) { 1348 mTriggerLongPress = new TriggerLongPress(); 1349 } 1350 src.postDelayed(mTriggerLongPress, mLongPressTimeout); 1351 break; 1352 case MotionEvent.ACTION_MOVE: 1353 final int activePointerIndex = srcEvent.findPointerIndex(mActivePointerId); 1354 if (activePointerIndex >= 0) { 1355 final float x = srcEvent.getX(activePointerIndex); 1356 final float y = srcEvent.getY(activePointerIndex); 1357 1358 // Has the pointer has moved outside of the view? 1359 if (!src.pointInView(x, y, mScaledTouchSlop)) { 1360 clearCallbacks(); 1361 1362 // Don't let the parent intercept our events. 1363 src.getParent().requestDisallowInterceptTouchEvent(true); 1364 return true; 1365 } 1366 } 1367 break; 1368 case MotionEvent.ACTION_CANCEL: 1369 case MotionEvent.ACTION_UP: 1370 clearCallbacks(); 1371 break; 1372 } 1373 1374 return false; 1375 } 1376 1377 private void clearCallbacks() { 1378 if (mTriggerLongPress != null) { 1379 mSrc.removeCallbacks(mTriggerLongPress); 1380 } 1381 1382 if (mDisallowIntercept != null) { 1383 mSrc.removeCallbacks(mDisallowIntercept); 1384 } 1385 } 1386 1387 private void onLongPress() { 1388 clearCallbacks(); 1389 1390 final View src = mSrc; 1391 if (!src.isEnabled() || src.isLongClickable()) { 1392 // Ignore long-press if the view is disabled or has its own 1393 // handler. 1394 return; 1395 } 1396 1397 if (!onForwardingStarted()) { 1398 return; 1399 } 1400 1401 // Don't let the parent intercept our events. 1402 src.getParent().requestDisallowInterceptTouchEvent(true); 1403 1404 // Make sure we cancel any ongoing source event stream. 1405 final long now = SystemClock.uptimeMillis(); 1406 final MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0); 1407 src.onTouchEvent(e); 1408 e.recycle(); 1409 1410 mForwarding = true; 1411 mWasLongPress = true; 1412 } 1413 1414 /** 1415 * Handled forwarded motion events and determines when to stop 1416 * forwarding. 1417 * 1418 * @param srcEvent motion event in source view coordinates 1419 * @return true to continue forwarding motion events, false to cancel 1420 */ 1421 private boolean onTouchForwarded(MotionEvent srcEvent) { 1422 final View src = mSrc; 1423 final ListPopupWindow popup = getPopup(); 1424 if (popup == null || !popup.isShowing()) { 1425 return false; 1426 } 1427 1428 final DropDownListView dst = popup.mDropDownList; 1429 if (dst == null || !dst.isShown()) { 1430 return false; 1431 } 1432 1433 // Convert event to destination-local coordinates. 1434 final MotionEvent dstEvent = MotionEvent.obtainNoHistory(srcEvent); 1435 src.toGlobalMotionEvent(dstEvent); 1436 dst.toLocalMotionEvent(dstEvent); 1437 1438 // Forward converted event to destination view, then recycle it. 1439 final boolean handled = dst.onForwardedEvent(dstEvent, mActivePointerId); 1440 dstEvent.recycle(); 1441 1442 // Always cancel forwarding when the touch stream ends. 1443 final int action = srcEvent.getActionMasked(); 1444 final boolean keepForwarding = action != MotionEvent.ACTION_UP 1445 && action != MotionEvent.ACTION_CANCEL; 1446 1447 return handled && keepForwarding; 1448 } 1449 1450 private class DisallowIntercept implements Runnable { 1451 @Override 1452 public void run() { 1453 final ViewParent parent = mSrc.getParent(); 1454 parent.requestDisallowInterceptTouchEvent(true); 1455 } 1456 } 1457 1458 private class TriggerLongPress implements Runnable { 1459 @Override 1460 public void run() { 1461 onLongPress(); 1462 } 1463 } 1464 } 1465 1466 /** 1467 * <p>Wrapper class for a ListView. This wrapper can hijack the focus to 1468 * make sure the list uses the appropriate drawables and states when 1469 * displayed on screen within a drop down. The focus is never actually 1470 * passed to the drop down in this mode; the list only looks focused.</p> 1471 */ 1472 private static class DropDownListView extends ListView { 1473 /** Duration in milliseconds of the drag-to-open click animation. */ 1474 private static final long CLICK_ANIM_DURATION = 150; 1475 1476 /** Target alpha value for drag-to-open click animation. */ 1477 private static final int CLICK_ANIM_ALPHA = 0x80; 1478 1479 /** Wrapper around Drawable's <code>alpha</code> property. */ 1480 private static final IntProperty<Drawable> DRAWABLE_ALPHA = 1481 new IntProperty<Drawable>("alpha") { 1482 @Override 1483 public void setValue(Drawable object, int value) { 1484 object.setAlpha(value); 1485 } 1486 1487 @Override 1488 public Integer get(Drawable object) { 1489 return object.getAlpha(); 1490 } 1491 }; 1492 1493 /* 1494 * WARNING: This is a workaround for a touch mode issue. 1495 * 1496 * Touch mode is propagated lazily to windows. This causes problems in 1497 * the following scenario: 1498 * - Type something in the AutoCompleteTextView and get some results 1499 * - Move down with the d-pad to select an item in the list 1500 * - Move up with the d-pad until the selection disappears 1501 * - Type more text in the AutoCompleteTextView *using the soft keyboard* 1502 * and get new results; you are now in touch mode 1503 * - The selection comes back on the first item in the list, even though 1504 * the list is supposed to be in touch mode 1505 * 1506 * Using the soft keyboard triggers the touch mode change but that change 1507 * is propagated to our window only after the first list layout, therefore 1508 * after the list attempts to resurrect the selection. 1509 * 1510 * The trick to work around this issue is to pretend the list is in touch 1511 * mode when we know that the selection should not appear, that is when 1512 * we know the user moved the selection away from the list. 1513 * 1514 * This boolean is set to true whenever we explicitly hide the list's 1515 * selection and reset to false whenever we know the user moved the 1516 * selection back to the list. 1517 * 1518 * When this boolean is true, isInTouchMode() returns true, otherwise it 1519 * returns super.isInTouchMode(). 1520 */ 1521 private boolean mListSelectionHidden; 1522 1523 /** 1524 * True if this wrapper should fake focus. 1525 */ 1526 private boolean mHijackFocus; 1527 1528 /** Whether to force drawing of the pressed state selector. */ 1529 private boolean mDrawsInPressedState; 1530 1531 /** Current drag-to-open click animation, if any. */ 1532 private Animator mClickAnimation; 1533 1534 /** Helper for drag-to-open auto scrolling. */ 1535 private AbsListViewAutoScroller mScrollHelper; 1536 1537 /** 1538 * <p>Creates a new list view wrapper.</p> 1539 * 1540 * @param context this view's context 1541 */ 1542 public DropDownListView(Context context, boolean hijackFocus) { 1543 super(context, null, com.android.internal.R.attr.dropDownListViewStyle); 1544 mHijackFocus = hijackFocus; 1545 // TODO: Add an API to control this 1546 setCacheColorHint(0); // Transparent, since the background drawable could be anything. 1547 } 1548 1549 /** 1550 * Handles forwarded events. 1551 * 1552 * @param activePointerId id of the pointer that activated forwarding 1553 * @return whether the event was handled 1554 */ 1555 public boolean onForwardedEvent(MotionEvent event, int activePointerId) { 1556 boolean handledEvent = true; 1557 boolean clearPressedItem = false; 1558 1559 final int actionMasked = event.getActionMasked(); 1560 switch (actionMasked) { 1561 case MotionEvent.ACTION_CANCEL: 1562 handledEvent = false; 1563 break; 1564 case MotionEvent.ACTION_UP: 1565 handledEvent = false; 1566 // $FALL-THROUGH$ 1567 case MotionEvent.ACTION_MOVE: 1568 final int activeIndex = event.findPointerIndex(activePointerId); 1569 if (activeIndex < 0) { 1570 handledEvent = false; 1571 break; 1572 } 1573 1574 final int x = (int) event.getX(activeIndex); 1575 final int y = (int) event.getY(activeIndex); 1576 final int position = pointToPosition(x, y); 1577 if (position == INVALID_POSITION) { 1578 clearPressedItem = true; 1579 break; 1580 } 1581 1582 final View child = getChildAt(position - getFirstVisiblePosition()); 1583 setPressedItem(child, position, x, y); 1584 handledEvent = true; 1585 1586 if (actionMasked == MotionEvent.ACTION_UP) { 1587 clickPressedItem(child, position); 1588 } 1589 break; 1590 } 1591 1592 // Failure to handle the event cancels forwarding. 1593 if (!handledEvent || clearPressedItem) { 1594 clearPressedItem(); 1595 } 1596 1597 // Manage automatic scrolling. 1598 if (handledEvent) { 1599 if (mScrollHelper == null) { 1600 mScrollHelper = new AbsListViewAutoScroller(this); 1601 } 1602 mScrollHelper.setEnabled(true); 1603 mScrollHelper.onTouch(this, event); 1604 } else if (mScrollHelper != null) { 1605 mScrollHelper.setEnabled(false); 1606 } 1607 1608 return handledEvent; 1609 } 1610 1611 /** 1612 * Starts an alpha animation on the selector. When the animation ends, 1613 * the list performs a click on the item. 1614 */ 1615 private void clickPressedItem(final View child, final int position) { 1616 final long id = getItemIdAtPosition(position); 1617 final Animator anim = ObjectAnimator.ofInt( 1618 mSelector, DRAWABLE_ALPHA, 0xFF, CLICK_ANIM_ALPHA, 0xFF); 1619 anim.setDuration(CLICK_ANIM_DURATION); 1620 anim.setInterpolator(new AccelerateDecelerateInterpolator()); 1621 anim.addListener(new AnimatorListenerAdapter() { 1622 @Override 1623 public void onAnimationEnd(Animator animation) { 1624 performItemClick(child, position, id); 1625 } 1626 }); 1627 anim.start(); 1628 1629 if (mClickAnimation != null) { 1630 mClickAnimation.cancel(); 1631 } 1632 mClickAnimation = anim; 1633 } 1634 1635 private void clearPressedItem() { 1636 mDrawsInPressedState = false; 1637 setPressed(false); 1638 updateSelectorState(); 1639 1640 final View motionView = getChildAt(mMotionPosition - mFirstPosition); 1641 if (motionView != null) { 1642 motionView.setPressed(false); 1643 } 1644 1645 if (mClickAnimation != null) { 1646 mClickAnimation.cancel(); 1647 mClickAnimation = null; 1648 } 1649 } 1650 1651 private void setPressedItem(View child, int position, float x, float y) { 1652 mDrawsInPressedState = true; 1653 1654 // Ordering is essential. First, update the container's pressed state. 1655 drawableHotspotChanged(x, y); 1656 if (!isPressed()) { 1657 setPressed(true); 1658 } 1659 1660 // Next, run layout if we need to stabilize child positions. 1661 if (mDataChanged) { 1662 layoutChildren(); 1663 } 1664 1665 // Manage the pressed view based on motion position. This allows us to 1666 // play nicely with actual touch and scroll events. 1667 final View motionView = getChildAt(mMotionPosition - mFirstPosition); 1668 if (motionView != null && motionView != child && motionView.isPressed()) { 1669 motionView.setPressed(false); 1670 } 1671 mMotionPosition = position; 1672 1673 // Offset for child coordinates. 1674 final float childX = x - child.getLeft(); 1675 final float childY = y - child.getTop(); 1676 child.drawableHotspotChanged(childX, childY); 1677 if (!child.isPressed()) { 1678 child.setPressed(true); 1679 } 1680 1681 // Ensure that keyboard focus starts from the last touched position. 1682 setSelectedPositionInt(position); 1683 positionSelectorLikeTouch(position, child, x, y); 1684 1685 // Refresh the drawable state to reflect the new pressed state, 1686 // which will also update the selector state. 1687 refreshDrawableState(); 1688 1689 if (mClickAnimation != null) { 1690 mClickAnimation.cancel(); 1691 mClickAnimation = null; 1692 } 1693 } 1694 1695 @Override 1696 boolean touchModeDrawsInPressedState() { 1697 return mDrawsInPressedState || super.touchModeDrawsInPressedState(); 1698 } 1699 1700 /** 1701 * <p>Avoids jarring scrolling effect by ensuring that list elements 1702 * made of a text view fit on a single line.</p> 1703 * 1704 * @param position the item index in the list to get a view for 1705 * @return the view for the specified item 1706 */ 1707 @Override 1708 View obtainView(int position, boolean[] isScrap) { 1709 View view = super.obtainView(position, isScrap); 1710 1711 if (view instanceof TextView) { 1712 ((TextView) view).setHorizontallyScrolling(true); 1713 } 1714 1715 return view; 1716 } 1717 1718 @Override 1719 public boolean isInTouchMode() { 1720 // WARNING: Please read the comment where mListSelectionHidden is declared 1721 return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode(); 1722 } 1723 1724 /** 1725 * <p>Returns the focus state in the drop down.</p> 1726 * 1727 * @return true always if hijacking focus 1728 */ 1729 @Override 1730 public boolean hasWindowFocus() { 1731 return mHijackFocus || super.hasWindowFocus(); 1732 } 1733 1734 /** 1735 * <p>Returns the focus state in the drop down.</p> 1736 * 1737 * @return true always if hijacking focus 1738 */ 1739 @Override 1740 public boolean isFocused() { 1741 return mHijackFocus || super.isFocused(); 1742 } 1743 1744 /** 1745 * <p>Returns the focus state in the drop down.</p> 1746 * 1747 * @return true always if hijacking focus 1748 */ 1749 @Override 1750 public boolean hasFocus() { 1751 return mHijackFocus || super.hasFocus(); 1752 } 1753 } 1754 1755 private class PopupDataSetObserver extends DataSetObserver { 1756 @Override 1757 public void onChanged() { 1758 if (isShowing()) { 1759 // Resize the popup to fit new content 1760 show(); 1761 } 1762 } 1763 1764 @Override 1765 public void onInvalidated() { 1766 dismiss(); 1767 } 1768 } 1769 1770 private class ListSelectorHider implements Runnable { 1771 public void run() { 1772 clearListSelection(); 1773 } 1774 } 1775 1776 private class ResizePopupRunnable implements Runnable { 1777 public void run() { 1778 if (mDropDownList != null && mDropDownList.getCount() > mDropDownList.getChildCount() && 1779 mDropDownList.getChildCount() <= mListItemExpandMaximum) { 1780 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 1781 show(); 1782 } 1783 } 1784 } 1785 1786 private class PopupTouchInterceptor implements OnTouchListener { 1787 public boolean onTouch(View v, MotionEvent event) { 1788 final int action = event.getAction(); 1789 final int x = (int) event.getX(); 1790 final int y = (int) event.getY(); 1791 1792 if (action == MotionEvent.ACTION_DOWN && 1793 mPopup != null && mPopup.isShowing() && 1794 (x >= 0 && x < mPopup.getWidth() && y >= 0 && y < mPopup.getHeight())) { 1795 mHandler.postDelayed(mResizePopupRunnable, EXPAND_LIST_TIMEOUT); 1796 } else if (action == MotionEvent.ACTION_UP) { 1797 mHandler.removeCallbacks(mResizePopupRunnable); 1798 } 1799 return false; 1800 } 1801 } 1802 1803 private class PopupScrollListener implements ListView.OnScrollListener { 1804 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 1805 int totalItemCount) { 1806 1807 } 1808 1809 public void onScrollStateChanged(AbsListView view, int scrollState) { 1810 if (scrollState == SCROLL_STATE_TOUCH_SCROLL && 1811 !isInputMethodNotNeeded() && mPopup.getContentView() != null) { 1812 mHandler.removeCallbacks(mResizePopupRunnable); 1813 mResizePopupRunnable.run(); 1814 } 1815 } 1816 } 1817} 1818