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