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